@ultimat3/testing 30.0.0 → 30.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ unitTest('the clock moves only when the test moves it', ({ clock }) => {
34
34
  | `seed-graph-error.ts` | `X_SEED_GRAPH_UNSATISFIED`, its fix naming the `defineFactory` to write; `X_SEED_GRAPH_ENTITY_UNKNOWN`, a plan and a writer built from two entity sets |
35
35
  | `seed-writer.ts` | `entitySeedWriter({ entities, client })` or `({ entities, driver? })` — the `SeedWriter` that writes a graph through each entity's own repository: Postgres or PGlite through a client, or a driver. `@ultimat3/entity` loads on the first write. `snapshot(rows)`, required on every `SeedWriter`, reads rows back as one digest each (`md5(t::text)` over a client), `null` for a row gone |
36
36
  | `exercise.ts` | `runExercisePass({ entities, writer, roles, seed?, overrides?, only? })` — every registered action, query and job once, each on its own committed tenant pair, one `ExerciseRecord` per primitive, the tenant proof's verdict on its `tenant`. What `x exercise` runs |
37
- | `define-exercise.ts` | `defineExercise({ <name>: { actor, input?, runs? } \| { off: '<why>' } }, { actor?: <entity> })` — an app's overrides (the second argument names the entity whose rows are the actors), `apps/web/exercise.ts`'s one export; a blank `off` is `X_CONTRACT_EXERCISE_UNDECLARED`, an unknown name `X_DECLARATION_UNKNOWN` when the pass runs |
37
+ | `define-exercise.ts` | `defineExercise({ <name>: { actor, input?, runs? } \| { off: '<why>' } }, { actor?: <entity>, factories?: { <entity name>: defineFactory(…) } })` — an app's overrides (the second argument names the entity whose rows are the actors, and the factories that build a table no generated row satisfies — a cross-column `satisfies` — keyed by entity name, unknown ones `X_DECLARATION_UNKNOWN`), `apps/web/exercise.ts`'s one export; a blank `off` is `X_CONTRACT_EXERCISE_UNDECLARED`, an unknown name `X_DECLARATION_UNKNOWN` when the pass runs |
38
38
  | `exercise-actor.ts` · `exercise-input.ts` · `exercise-report.ts` | the first `x policy explain` actor the policy allows, minted in the primitive's tenant · the input: override, then a seeded id by field stem, then the minimal draw (`@ultimat3/schema/sample`, loaded dynamically) · the room one run executes in (network sealed, mail captured, clock frozen) and its record, `overrideLine` the entry to paste |
39
39
  | `tenant-proof.ts` | internal (`CROSS_TENANT_SCOPE` is `@ultimat3/entity`'s): `proveTenant` — B with A's ids, A's own call, B again; each B leg between two snapshots of A's rows. `withoutCrossTenant` strips `tenancy:cross` (`CROSS_TENANT_SCOPE`) from B; `publicReason` decides `public` |
40
40
  | `tenant-canary.ts` | internal: `tenantTaint` — one tenant's rows, ids and marks (row canaries, key parts of `MIN_TAINT_ID_LENGTH` 8 or more no other tenant shares); `findTaint` — every path holding one |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "30.0.0",
3
+ "version": "30.2.0",
4
4
  "description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -46,21 +46,21 @@
46
46
  "test": "bun test"
47
47
  },
48
48
  "dependencies": {
49
- "@ultimat3/action": "30.0.0",
50
- "@ultimat3/cache": "30.0.0",
51
- "@ultimat3/core": "30.0.0",
52
- "@ultimat3/db": "30.0.0",
53
- "@ultimat3/entity": "30.0.0",
54
- "@ultimat3/http": "30.0.0",
55
- "@ultimat3/i18n": "30.0.0",
56
- "@ultimat3/jobs": "30.0.0",
57
- "@ultimat3/mail": "30.0.0",
58
- "@ultimat3/policy": "30.0.0",
59
- "@ultimat3/pwa": "30.0.0",
60
- "@ultimat3/query": "30.0.0",
61
- "@ultimat3/realtime": "30.0.0",
62
- "@ultimat3/render": "30.0.0",
63
- "@ultimat3/schema": "30.0.0",
64
- "@ultimat3/time": "30.0.0"
49
+ "@ultimat3/action": "30.2.0",
50
+ "@ultimat3/cache": "30.2.0",
51
+ "@ultimat3/core": "30.2.0",
52
+ "@ultimat3/db": "30.2.0",
53
+ "@ultimat3/entity": "30.2.0",
54
+ "@ultimat3/http": "30.2.0",
55
+ "@ultimat3/i18n": "30.2.0",
56
+ "@ultimat3/jobs": "30.2.0",
57
+ "@ultimat3/mail": "30.2.0",
58
+ "@ultimat3/policy": "30.2.0",
59
+ "@ultimat3/pwa": "30.2.0",
60
+ "@ultimat3/query": "30.2.0",
61
+ "@ultimat3/realtime": "30.2.0",
62
+ "@ultimat3/render": "30.2.0",
63
+ "@ultimat3/schema": "30.2.0",
64
+ "@ultimat3/time": "30.2.0"
65
65
  }
66
66
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ExerciseOptions } from '@ultimat3/action';
7
7
  import { renderCauseValue, renderFixLiteral, UltimateError } from '@ultimat3/core';
8
+ import type { SeedFactory } from './seed-graph-plan';
8
9
 
9
10
  /** 39's option type, exactly: `{ actor, input?, runs? }` or `{ off: '<reason>' }`. */
10
11
  export type ExerciseOverride = ExerciseOptions;
@@ -15,6 +16,8 @@ export interface ExerciseOverrides {
15
16
  readonly entries: ReadonlyMap<string, ExerciseOverride>;
16
17
  /** The entity an actor is a row of, by name: its id is bound to a seeded row. */
17
18
  readonly actorEntity?: string;
19
+ /** By entity name: the pass's graph builds those rows with them (`seedGraph`'s `factories`). */
20
+ readonly factories?: Readonly<Record<string, SeedFactory>>;
18
21
  }
19
22
 
20
23
  /** What the whole pass needs to know of the app, beside the per-primitive entries. */
@@ -25,6 +28,12 @@ export interface ExerciseAppOptions {
25
28
  * writes a row that exists. Omitted, an actor's id is its role's name.
26
29
  */
27
30
  readonly actor?: { readonly $name: string };
31
+ /**
32
+ * By entity name, `seedGraph`'s own option: a table whose rows a generated value cannot satisfy
33
+ * (an `invariant` across columns) is built by the app's `defineFactory`. An unknown name is
34
+ * refused when the pass runs.
35
+ */
36
+ readonly factories?: Readonly<Record<string, SeedFactory>>;
28
37
  }
29
38
 
30
39
  /** `X_CONTRACT_EXERCISE_UNDECLARED`, 39's code for the same silence: an `off` with no reason. */
@@ -49,6 +58,7 @@ export function defineExercise(
49
58
  kind: 'exercise-overrides',
50
59
  entries,
51
60
  ...(app.actor === undefined ? {} : { actorEntity: app.actor.$name }),
61
+ ...(app.factories === undefined ? {} : { factories: app.factories }),
52
62
  });
53
63
  }
54
64
 
package/src/exercise.ts CHANGED
@@ -418,6 +418,9 @@ export async function runExercisePass(
418
418
  graph = await seedPass(options.entities, options.writer, {
419
419
  primitives: live.length,
420
420
  seed: passSeed,
421
+ ...(options.overrides?.factories === undefined
422
+ ? {}
423
+ : { factories: options.overrides.factories }),
421
424
  });
422
425
  // In order, one at a time: the seams the room installs are the process's, not the primitive's.
423
426
  for (const subject of chosen) {
@@ -438,6 +441,7 @@ export async function runExercisePass(
438
441
  main: graph,
439
442
  seed: passSeed,
440
443
  room,
444
+ factories: options.overrides?.factories,
441
445
  setup: async (subject, sized, tenant, self) => {
442
446
  const full = chosen.find((one) => one.name === subject.name);
443
447
  return full === undefined
@@ -3,7 +3,7 @@
3
3
  // every reference to one is pointed at the pass's own row in the same position. Pure.
4
4
 
5
5
  import { seedGraph } from './seed-graph';
6
- import type { SeedGraph } from './seed-graph-plan';
6
+ import type { SeedFactory, SeedGraph } from './seed-graph-plan';
7
7
  import type { SeedEntity } from './seed-graph-shape';
8
8
 
9
9
  export interface ScaleGraph {
@@ -23,12 +23,18 @@ export interface ScaleGraph {
23
23
  export function scaleGraph(
24
24
  main: SeedGraph,
25
25
  entities: readonly SeedEntity[],
26
- options: { readonly perTable: number; readonly seed: number },
26
+ options: {
27
+ readonly perTable: number;
28
+ readonly seed: number;
29
+ /** The app's `defineExercise` factories: a table only they satisfy is built by them here too. */
30
+ readonly factories?: Readonly<Record<string, SeedFactory>> | undefined;
31
+ },
27
32
  ): ScaleGraph {
28
33
  const sized = seedGraph(Object.fromEntries(entities.map((entity) => [entity.$name, entity])), {
29
34
  tenants: 1,
30
35
  perTable: options.perTable,
31
36
  seed: options.seed,
37
+ ...(options.factories === undefined ? {} : { factories: options.factories }),
32
38
  });
33
39
  const shared = (graph: SeedGraph) => graph.inserts.filter((insert) => insert.tenant === null);
34
40
  const theirs = shared(main);
@@ -10,7 +10,7 @@ import { testJobs } from './fixture-jobs';
10
10
  import type { ScaleProof, StatementTally } from './scale-sweep';
11
11
  import { judgeScale, SCALE_SIZES } from './scale-sweep';
12
12
  import { scaleGraph } from './scale-sweep-graph';
13
- import type { SeedGraph, SeedWriter } from './seed-graph-plan';
13
+ import type { SeedFactory, SeedGraph, SeedWriter } from './seed-graph-plan';
14
14
  import { insertSeedGraph } from './seed-graph-plan';
15
15
  import type { SeedEntity } from './seed-graph-shape';
16
16
 
@@ -45,6 +45,8 @@ export interface ScaleSweepOptions {
45
45
  readonly seed: number;
46
46
  readonly room: ExerciseRoom;
47
47
  readonly setup: ScaleSetup;
48
+ /** `defineExercise`'s factories, handed to every sized graph as to the pass's own. */
49
+ readonly factories?: Readonly<Record<string, SeedFactory>> | undefined;
48
50
  }
49
51
 
50
52
  type Call = { readonly shapes: readonly StatementTally[] } | { readonly failed: string };
@@ -88,6 +90,7 @@ export async function scaleSweep(
88
90
  scaleGraph(options.main, options.entities, {
89
91
  perTable,
90
92
  seed: fnv1a(`scale:${options.seed}:${i}`) || 1,
93
+ factories: options.factories,
91
94
  }),
92
95
  );
93
96
  try {
@@ -27,7 +27,7 @@ export class SeedGraphUnsatisfiedError extends UltimateError {
27
27
  super({
28
28
  code: 'X_SEED_GRAPH_UNSATISFIED',
29
29
  cause: `${input.entity}: ${input.what}`,
30
- fix: `defineFactory(<the ${input.entity} entity>, { defaults: (index, ids) => ({ ${body} }) }), then seedGraph(entities, { factories: { ${name}: thatFactory } })`,
30
+ fix: `defineFactory(<the ${input.entity} entity>, { defaults: (index, ids) => ({ ${body} }) }), then seedGraph(entities, { factories: { ${name}: thatFactory } }) — for x exercise, defineExercise({}, { factories: { ${name}: thatFactory } }) in apps/web/exercise.ts`,
31
31
  });
32
32
  }
33
33
  }
@@ -3,10 +3,10 @@
3
3
  // owns a fresh tenant pair of its own. One graph also means one set of unique keys: per-primitive
4
4
  // graphs re-inserted the shared rows and collided on their primary keys.
5
5
 
6
- import { isUltimateError, renderThrowable } from '@ultimat3/core';
6
+ import { isUltimateError, renderCauseValue, renderThrowable, UltimateError } from '@ultimat3/core';
7
7
  import { seedGraph } from './seed-graph';
8
8
  import { seedGraphUnsatisfied } from './seed-graph-error';
9
- import type { SeedGraph, SeedRow, SeedUpdate, SeedWriter } from './seed-graph-plan';
9
+ import type { SeedFactory, SeedGraph, SeedRow, SeedUpdate, SeedWriter } from './seed-graph-plan';
10
10
  import { insertSeedGraph } from './seed-graph-plan';
11
11
  import type { SeedEntity } from './seed-graph-shape';
12
12
 
@@ -50,6 +50,15 @@ export const tenantPair = (graph: SeedGraph, index: number): readonly [string, s
50
50
  graph.tenants[index * TENANTS_PER_PRIMITIVE + 1] as string,
51
51
  ];
52
52
 
53
+ /** A factory keyed by a name no seeded entity has would build nothing, and the red would stay. */
54
+ const unknownFactory = (names: readonly string[], known: ReadonlySet<string>): UltimateError =>
55
+ new UltimateError({
56
+ code: 'X_DECLARATION_UNKNOWN',
57
+ cause: `defineExercise() factories name ${names.map((name) => renderCauseValue(name)).join(', ')}, which no seeded entity is called (seeded: ${renderCauseValue([...known].sort().join(', '))})`,
58
+ fix: "defineExercise({}, { factories: { '<the name entity(...) was declared with>': thatFactory } }) # in apps/web/exercise.ts; the seeded names are in the cause",
59
+ meta: { unknown: names, registered: [...known].sort() },
60
+ });
61
+
53
62
  /**
54
63
  * Plan and write the pass's graph: shared rows once, `TENANTS_PER_PRIMITIVE` tenants per
55
64
  * primitive. Same seed, same rows. Before any primitive runs, so a failure refuses the pass.
@@ -57,11 +66,19 @@ export const tenantPair = (graph: SeedGraph, index: number): readonly [string, s
57
66
  export async function seedPass(
58
67
  entities: readonly SeedEntity[],
59
68
  writer: SeedWriter,
60
- options: { readonly primitives: number; readonly seed: number },
69
+ options: {
70
+ readonly primitives: number;
71
+ readonly seed: number;
72
+ readonly factories?: Readonly<Record<string, SeedFactory>>;
73
+ },
61
74
  ): Promise<SeedGraph> {
75
+ const known = new Set(entities.map((entity) => entity.$name));
76
+ const unknown = Object.keys(options.factories ?? {}).filter((name) => !known.has(name));
77
+ if (unknown.length > 0) throw unknownFactory(unknown, known);
62
78
  const graph = seedGraph(Object.fromEntries(entities.map((entity) => [entity.$name, entity])), {
63
79
  tenants: Math.max(1, options.primitives) * TENANTS_PER_PRIMITIVE,
64
80
  seed: options.seed,
81
+ ...(options.factories === undefined ? {} : { factories: options.factories }),
65
82
  });
66
83
  await insertSeedGraph(graph, coded(writer));
67
84
  return graph;
@@ -6,6 +6,13 @@
6
6
  export interface SeedFactory {
7
7
  build(): object;
8
8
  reset(): void;
9
+ /**
10
+ * Columns that follow from what the GRAPH chose — its keys and references, which no factory
11
+ * builds: `memorial_templates.scopeKey` from `ownerOrgId`. Called on each candidate row before
12
+ * its rules are judged; what it answers wins. A `defineFactory` with it:
13
+ * `{ build: () => f.build(), reset: () => f.reset(), derive: (row) => ({ … }) }`.
14
+ */
15
+ derive?(row: Readonly<Record<string, unknown>>): object;
9
16
  }
10
17
 
11
18
  export interface SeedGraphOptions {
@@ -17,6 +17,8 @@ export interface RowProblem {
17
17
  readonly uniques: readonly (readonly string[])[];
18
18
  /** Per entry of `uniques`, the keys earlier rows already hold. */
19
19
  readonly taken: readonly ReadonlySet<string>[];
20
+ /** A factory's `derive`: columns that follow from the picked ones, applied before any rule. */
21
+ readonly derive?: ((row: Readonly<Record<string, unknown>>) => object) | undefined;
20
22
  }
21
23
 
22
24
  export type RowSolution =
@@ -77,7 +79,7 @@ const build = (problem: RowProblem, picks: readonly number[]): Record<string, un
77
79
  problem.columns.forEach(([property, candidates], index) => {
78
80
  row[property] = candidates[picks[index] ?? 0];
79
81
  });
80
- return row;
82
+ return problem.derive === undefined ? row : { ...row, ...problem.derive(row) };
81
83
  };
82
84
 
83
85
  /** Odometer over the varied columns, last one fastest: deterministic and exhaustive up to the cap. */
@@ -26,6 +26,12 @@ export interface EntityShape {
26
26
  readonly references: ReadonlyMap<string, ReferenceDescription>;
27
27
  readonly checks: readonly RowCheck[];
28
28
  readonly uniques: readonly (readonly string[])[];
29
+ /**
30
+ * The uniques with a `where`: SQL the generator cannot evaluate, so held as total — stricter than
31
+ * the table — unless a factory builds every one of their columns, when the database judges them.
32
+ * `orgs: unique(kind) where kind = 'staff'` otherwise ran an enum out of values (`X_SEED_GRAPH_UNSATISFIED`).
33
+ */
34
+ readonly partialUniques: readonly (readonly string[])[];
29
35
  /**
30
36
  * The soft-delete property, or `null`. Seeded null unless a factory builds it: a row born deleted
31
37
  * is invisible to every read.
@@ -83,15 +89,25 @@ export function shapeOf(entity: SeedEntity): EntityShape {
83
89
  holds: (row) => invariant.holds(row),
84
90
  }),
85
91
  );
86
- // A partial unique (`where`) is treated as total: stricter than the table, never looser.
87
92
  const uniques = dedupe([
88
93
  entity.$primaryKey,
89
94
  ...description.columns.filter((column) => column.unique).map((c) => toProperties([c.column])),
90
95
  ...entity.$invariants
91
- .filter((invariant) => invariant.kind === 'unique')
96
+ .filter((invariant) => invariant.kind === 'unique' && invariant.where === undefined)
92
97
  .map((invariant) => toProperties(invariant.columns)),
93
- ...description.indexes.filter((index) => index.unique).map((i) => toProperties(i.columns)),
98
+ ...description.indexes
99
+ .filter((index) => index.unique && index.where === null)
100
+ .map((i) => toProperties(i.columns)),
94
101
  ]);
102
+ const total = new Set(uniques.map((set) => set.join('\u0000')));
103
+ const partialUniques = dedupe([
104
+ ...entity.$invariants
105
+ .filter((invariant) => invariant.kind === 'unique' && invariant.where !== undefined)
106
+ .map((invariant) => toProperties(invariant.columns)),
107
+ ...description.indexes
108
+ .filter((index) => index.unique && index.where !== null)
109
+ .map((i) => toProperties(i.columns)),
110
+ ]).filter((set) => !total.has(set.join('\u0000')));
95
111
  return {
96
112
  entity,
97
113
  name: entity.$name,
@@ -99,6 +115,7 @@ export function shapeOf(entity: SeedEntity): EntityShape {
99
115
  references: new Map(entity.$references().map((reference) => [reference.property, reference])),
100
116
  checks,
101
117
  uniques,
118
+ partialUniques,
102
119
  softDelete: description.softDelete ? SEED_SOFT_DELETE_PROPERTY : null,
103
120
  };
104
121
  }
package/src/seed-graph.ts CHANGED
@@ -135,7 +135,8 @@ export function seedGraph(
135
135
  const factory = options.factories?.[name];
136
136
  const rows: Placed[] = [];
137
137
  placed.set(name, rows);
138
- const taken = shape.uniques.map(() => new Set<string>());
138
+ const allUniques = [...shape.uniques, ...shape.partialUniques];
139
+ const taken = allUniques.map(() => new Set<string>());
139
140
  const rootProperty = roots.get(name);
140
141
  const buckets: readonly (string | null)[] = scoped(name) ? tenants : [null];
141
142
  let ordinal = 0;
@@ -176,11 +177,19 @@ export function seedGraph(
176
177
  candidates.length === 0 ? refuseEmpty(shape, property, tenant) : candidates,
177
178
  ] as const;
178
179
  });
180
+ // A partial unique whose every column the factory built is the database's to judge.
181
+ const held = allUniques
182
+ .map((set, i) => [set, taken[i] as Set<string>] as const)
183
+ .filter(
184
+ ([set], i) =>
185
+ i < shape.uniques.length || !set.every((property) => Object.hasOwn(built, property)),
186
+ );
179
187
  const problem: RowProblem = {
180
188
  columns,
181
189
  checks: shape.checks,
182
- uniques: shape.uniques,
183
- taken,
190
+ uniques: held.map(([set]) => set),
191
+ taken: held.map(([, set]) => set),
192
+ derive: factory?.derive?.bind(factory),
184
193
  };
185
194
  const solved = solveRow(problem);
186
195
  if (!solved.ok) {
@@ -197,7 +206,7 @@ export function seedGraph(
197
206
  });
198
207
  }
199
208
  const row = { ...solved.row };
200
- shape.uniques.forEach((set, i) => {
209
+ allUniques.forEach((set, i) => {
201
210
  const key = uniqueKey(row, set);
202
211
  if (key !== undefined) taken[i]?.add(key);
203
212
  });
@@ -8,7 +8,7 @@ import { ctxOf, finiteOption, runWithContext, serviceActor } from '@ultimat3/cor
8
8
  import type { Subject, SubjectPlan } from '@ultimat3/jobs';
9
9
  import { memoryStepStore, planSubject, stepRunner, subjectEraseTables } from '@ultimat3/jobs';
10
10
  import { seedGraph } from './seed-graph';
11
- import type { SeedGraph, SeedWriter } from './seed-graph-plan';
11
+ import type { SeedFactory, SeedGraph, SeedWriter } from './seed-graph-plan';
12
12
  import { insertSeedGraph } from './seed-graph-plan';
13
13
  import type { SeedEntity } from './seed-graph-shape';
14
14
  import { SubjectEraseIncompleteError } from './subject-proof-error';
@@ -22,6 +22,8 @@ export interface SubjectProofOptions {
22
22
  /** Every stored row of one table, as the database holds it, unscoped. */
23
23
  readonly read: (entity: SeedEntity) => Promise<readonly Stored[]>;
24
24
  readonly seed?: number;
25
+ /** `defineExercise`'s factories: a table only they satisfy is built by them for the proof too. */
26
+ readonly factories?: Readonly<Record<string, SeedFactory>> | undefined;
25
27
  }
26
28
 
27
29
  export interface SubjectProofResult {
@@ -107,6 +109,7 @@ export async function subjectProof<I>(
107
109
  const graph = seedGraph(Object.fromEntries(entities), {
108
110
  seed: finiteOption('subjectProof', 'seed', options.seed ?? 1),
109
111
  tenants: 2,
112
+ ...(options.factories === undefined ? {} : { factories: options.factories }),
110
113
  });
111
114
  await insertSeedGraph(graph, options.writer);
112
115
  const tenant = graph.tenants[0] ?? null;