mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
e5f01e570a
Marking a stop of a city walk visited or skipped answered with a server error every time: the write that keeps a changed status stamped its time as a function call, which neither SQLite nor Postgres takes. It now uses the clock the database itself spells, so a stop checked off, skipped or taken back stays that way and the day plan of everyone on the trip follows. The Save button of a stop's dialog in the planner did nothing, because it sat outside the dialog's form. It saves the name, stay, importance and protection again. A walk saved with two points that carry one key, as a loop back to the start does, keeps that key on the first point and gives the other one of its own, so checking one off never marks both. A walk put on one day twice is read for an assistant from its first assignment. The server tests cover the visits from the repository to the routes: per day, through a moved and a removed assignment, a copy of the walk and a copied trip without visits, a removed stop taking its visits along, and the refusals for another trip, a stop the walk does not have, a member who may not edit days and an unknown key or body. The MCP progress of a walk, the station fields and counts of the tour list, and the contracts in shared are tested too; the existing tests expect the keys every saved point now carries, the station count of a list row and the new entity. The client tests cover the stop dialog and its pills, the walk themes and categories of the search along a tour, the stop menu of the waypoint list, the walk's timing, routing and type choice, and answer the trip planner's read of the visits in the tests instead of leaving it unanswered.
124 lines
5.9 KiB
TypeScript
124 lines
5.9 KiB
TypeScript
import { ALL_ENTITIES } from '../../../src/db/entities';
|
|
import { TrekRepository } from '../../../src/db/repositories/_shared/trek-repository';
|
|
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
|
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
|
import { EntitySchema, type EntityRepository } from '@mikro-orm/core';
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
const ENTITIES_DIR = path.join(__dirname, '../../../src/db/entities');
|
|
const REPOSITORIES_DIR = path.join(__dirname, '../../../src/db/repositories');
|
|
|
|
describe('ALL_ENTITIES', () => {
|
|
it('ENT-001: lists exactly one schema per *.entity.ts file', () => {
|
|
const files = fs
|
|
.readdirSync(ENTITIES_DIR)
|
|
.filter((f) => f.endsWith('.entity.ts'))
|
|
.sort();
|
|
const names = ALL_ENTITIES.map((s) => s.name).sort();
|
|
const expected = files.map((f) => f.replace(/\.entity\.ts$/, '')).sort();
|
|
expect(names).toEqual(expected);
|
|
});
|
|
|
|
it('ENT-002: every entry is an EntitySchema with a class', () => {
|
|
for (const schema of ALL_ENTITIES) {
|
|
expect(schema).toBeInstanceOf(EntitySchema);
|
|
expect(typeof schema.meta.class).toBe('function');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Plan 2 Task 3: every one of the 135 entities the migrated schema
|
|
* describes (120 rewritten + the 5 document-sync tables added this task,
|
|
* + push_subscriptions and google_api_usage from legacy steps 245 and 254,
|
|
* + tour_types, tours and tour_waypoints from the Tours migration,
|
|
* + scheduler_leases from the cron lease migration
|
|
* + user_sessions, the session rows behind revocable session tokens
|
|
* + ferrata_tiles, the stored via ferrata grid cells
|
|
* + paddle_regions, paddle_features and tour_obstacle_reviews, the paddle obstacle data and decisions
|
|
* + trip_journal_links and trip_journal_link_members, the journal a trip shows and who it is open to
|
|
* + paddle_water_graphs and paddle_region_requests, a region's water network and the regions that wait for an admin
|
|
* + tour_station_visits, how far a city walk got on one of its days)
|
|
* is registered, and no two share a class name (a silent duplicate would
|
|
* shadow one of them in `orm.getMetadata()`, exactly the kind of drift
|
|
* `entity-schema-parity.test.ts` can't see — it iterates the metadata map,
|
|
* which de-duplicates by class name).
|
|
*/
|
|
it('ENT-003: exactly 143 entities, every class name unique', () => {
|
|
expect(ALL_ENTITIES.length).toBe(143);
|
|
const names = ALL_ENTITIES.map((s) => s.meta.className);
|
|
expect(new Set(names).size).toBe(names.length);
|
|
});
|
|
|
|
/**
|
|
* Task 1's review flagged this binding as otherwise unpinned: dropping an
|
|
* entity's `repository: () => XRepository` mutation stayed green in every
|
|
* gate up to here (task-1-review.md, minor 1). `em.getRepository(X)` must
|
|
* return an instance of the exact repository class
|
|
* `src/db/repositories/<Name>.repository.ts` exports, for every entity —
|
|
* the 120 the generator wrote a stub for this task plus the 5 pre-existing,
|
|
* hand-written ones (`Days`, `DayNotes`, `Trips`, `BudgetCategoryOrder`,
|
|
* `VacayUserSettings`) it left untouched.
|
|
*/
|
|
describe('ENT-004: em.getRepository(X) is an instance of X.repository.ts', () => {
|
|
let t: TestOrm;
|
|
const testDb = createSnapshotTestDb();
|
|
|
|
beforeAll(async () => {
|
|
t = await createTestOrm(testDb);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await t.close();
|
|
testDb.close();
|
|
});
|
|
|
|
it('every entity resolves to its own repository class', async () => {
|
|
const failures: string[] = [];
|
|
for (const schema of ALL_ENTITIES) {
|
|
const className = schema.meta.className;
|
|
const repoFile = path.join(REPOSITORIES_DIR, `${className}.repository.ts`);
|
|
if (!fs.existsSync(repoFile)) {
|
|
failures.push(`${className}: no src/db/repositories/${className}.repository.ts file`);
|
|
continue;
|
|
}
|
|
const mod: Record<string, unknown> = await import(
|
|
/* @vite-ignore */ `../../../src/db/repositories/${className}.repository`
|
|
);
|
|
const RepoClass = mod[`${className}Repository`];
|
|
if (typeof RepoClass !== 'function') {
|
|
failures.push(`${className}: repository module has no exported ${className}Repository class`);
|
|
continue;
|
|
}
|
|
const repo: EntityRepository<object> = t.em.getRepository(schema.class as new () => object);
|
|
if (!(repo instanceof (RepoClass as abstract new (...args: never[]) => unknown))) {
|
|
failures.push(`${className}: em.getRepository() did not return a ${className}Repository instance`);
|
|
}
|
|
// Task 7 review, H2 carry-list item 1: the `extends EntityRepository`
|
|
// ratchet made self-enforcing — every bound repository must also be
|
|
// a TrekRepository (validateRequestContext + the disableIdentityMap
|
|
// read default), not just an instance of its own named class.
|
|
if (!(repo instanceof TrekRepository)) {
|
|
failures.push(`${className}: em.getRepository() did not return a TrekRepository instance`);
|
|
}
|
|
}
|
|
expect(failures).toEqual([]);
|
|
});
|
|
|
|
// Task 7 review, H2: a source-scan companion to the instanceof check
|
|
// above — belt and suspenders against a repository that extends
|
|
// TrekRepository indirectly (or not through this loop's import path).
|
|
it('SCAN-001: exactly one repository file extends EntityRepository directly — the base class itself', () => {
|
|
const files = fs.readdirSync(REPOSITORIES_DIR).filter((f) => f.endsWith('.repository.ts'));
|
|
const hits: string[] = [];
|
|
for (const file of files) {
|
|
const content = fs.readFileSync(path.join(REPOSITORIES_DIR, file), 'utf-8');
|
|
if (/\bextends\s+EntityRepository\b/.test(content)) hits.push(file);
|
|
}
|
|
expect(hits).toEqual([]); // the base class lives in _shared/, not matched by *.repository.ts
|
|
});
|
|
});
|
|
});
|