@ultimat3/testing 30.3.0 → 30.5.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/CLAUDE.md CHANGED
@@ -16,7 +16,7 @@ import is `registry-isolation.ts`, own entry point, off the barrel. Full rule: `
16
16
  | No mocks of the DB | clone a template database; `template-db.ts` is the only DB path |
17
17
  | No wall clock | `withFrozenClock` / `advanceClock`; `Date.now()` is frozen by the preload |
18
18
  | Frozen ≠ different | `globalThis.Date` becomes a subclass; `FrozenDate[Symbol.hasInstance]` brands on the `[[DateValue]]` slot (`Date.prototype.getTime.call(value)` throws or not) |
19
- | No unmocked egress | `sealed-network.ts` seals `fetch`; `sealed-sockets.ts` `WebSocket`, `Bun.connect` (not loopback); a miss is `X_TEST_NETWORK_SEALED` |
19
+ | No unmocked egress | `sealed-network.ts` seals `fetch`; `sealed-sockets.ts` `WebSocket`, `Bun.connect` (not loopback); `sealed-dns.ts` the three `lookup`s (a mocked host answers `MOCKED_HOST_ADDRESS`); a miss is `X_TEST_NETWORK_SEALED` |
20
20
  | One `mockFetch`, and it grows | a verb in the match, ordered answers, a pinned dial's name (`mock-route.ts`) and `requestLog` (`request-log.ts`). A stream bound for a real server logs `null` (teeing drains it). `original(input, init)` goes as handed: `tls`, `unix`, `proxy` are in no `Request`. `cassette()`: below |
21
21
  | Self is not egress | a port core's `markListening()` announced passes through — a socket test never unseals |
22
22
  | Offline is a state, not a mock | `network.offline()` / `.drop()` fail every dial, loopback too, as `X_TEST_NETWORK_OFFLINE` before any mock: the app's offline path runs |
@@ -49,15 +49,15 @@ import is `registry-isolation.ts`, own entry point, off the barrel. Full rule: `
49
49
  | The exercise pass runs what is registered | `exercise.ts` (`runExercisePass`): every action, query and job once, its own tenant pair off one `seedGraph`, an actor `exercise-actor.ts` picks, 39's `runExercise` as the verdict. Rows COMMITTED. Overrides only via `defineExercise` (blank `off` refused) |
50
50
  | An override `input` is awaited, as the call's actor | `exercise-override-input.ts`: run inside the call's own request, so `useContext().actor` is the seated actor; the one way to a signed input. `async` ones are left out of `x drill`'s load |
51
51
  | A passkey test signs for real | `softAuthenticator` is `@ultimat3/auth/soft-authenticator`'s, never re-exported here (one value, one path: `flight-copies`) and never a second one (auth's own suites need it, and auth cannot import testing) |
52
- | A reached job is crash-proven | `exercise-crash.ts`: `crashProof` with the control's input and actor, after the tenant proof; only `diverged` is red (`X_JOB_CRASH_DIVERGED`). Committed: job bodies escape transactions |
52
+ | A reached job is crash-proven | `exercise-crash.ts`: `crashProof` with the control's input and actor, after the tenant proof; only `diverged` is red (`X_JOB_CRASH_DIVERGED`), its place `crashEffectPlace` (`inside` a step via jobs' `currentStep()`, or `after` one). Committed: job bodies escape transactions |
53
53
  | The tenant proof is taint only | `tenant-proof.ts`: B, A, B; B is A's role minus `tenancy:cross`. Leak = seeded canary or unhanded id (`tenant-canary.ts`); write = moved digest. Only `crossed` is red |
54
- | Every statement the pass sent is planned | `plan-sweep.ts` (`sweepPlans`): first relation an app table, `PREPARE` in a READ-ONLY transaction with seq/bitmap scans and sorts off, judged by `plan-oracle.ts`; red = `X_QUERY_UNINDEXED` / `X_QUERY_SORT_UNSERVED` with the `indexes:` entry (`plan-sweep-fix.ts`). `scan:` or `expectedSeqScan` is exempt, listed; no generic plan is `X_QUERY_PLAN_UNAVAILABLE`, never a pass |
54
+ | Every statement the pass sent is planned | `plan-sweep.ts` (`sweepPlans`): first relation an app table, `PREPARE` in a READ-ONLY transaction with seq/bitmap scans and sorts off, judged by `plan-oracle.ts`; red = `X_QUERY_UNINDEXED` / `X_QUERY_SORT_UNSERVED` with the `indexes:` entry (`plan-sweep-fix.ts`). `scan:` or `expectedSeqScan` is exempt, listed; credited: a partial unique whose predicate the statement states (one row), a declared equality-then-range index under a limit; no generic plan is `X_QUERY_PLAN_UNAVAILABLE`, never a pass |
55
55
  | A shape that grows with the rows is red | `scale-sweep-run.ts`: two more tenants at `SCALE_SIZES` (2, 8 rows a table), every reached primitive once in each; `judgeScale` (pure) reds `X_QUERY_SCALES_WITH_ROWS`. Under `expectedQueryLoop` it is listed with its reason |
56
56
  | An erase is PROVED, not asserted | `subject-proof.ts` (`subjectProof`, run by cli's `exercise-subject.ts`): canaries in every table, one person erased, every stored row searched; a survivor outside `retain` is `X_SUBJECT_ERASE_INCOMPLETE`, naming table and column, never the value |
57
57
  | A captured failure replays as a test | `repro.ts`: `reproduce(bundle, { driver })` seeds the rows it read, freezes the clock at its instant, invokes as its actor; masked fields are stood in (`repro-stand-in.ts`). An unregistered name ANSWERS as the failure. `reproJob(runJobs, failure)` for a dead job. What `x repro --write` emits |
58
58
  | A vendor's answers are a cassette | `cassette.ts`: replay = one `mockFetch` route per recorded URL, seal kept (`X_CASSETTE_MISSING`, `X_CASSETTE_UNUSED`). Record = `ULTIMATE_CASSETTE=record` + `record.hosts`, refused under `CI` (`X_CASSETTE_RECORD_REFUSED`), headers allow-listed, credential keys masked, bytes scanned before disk (`X_CASSETTE_SECRET`). The request side is never written |
59
59
  | One seed writer | `entitySeedWriter({ entities, client \| driver })` (`seed-writer.ts`) writes through each entity's own repository; `snapshot` is required; `entity` loads on the first write, so the barrel stays entity-free |
60
- | Factory values are DECLARED | `factoriesFor` and `seedGraph` share `seed-graph-values.ts`: default, enum, kind, through `$parse`, never the name. `seed-graph*.ts` type-import `entity` only |
60
+ | Factory values are DECLARED | `factoriesFor` and `seedGraph` share `seed-graph-values.ts`: default, enum, kind, a `matches()` pattern (`seed-pattern-sample.ts`, read off the CHECK by `seed-graph-patterns.ts`), `$meta.schema` (sampler `require`d on first use), through `$parse`, never the name. `seed-graph*.ts` type-import `entity` only |
61
61
  | e2e without a driver | `e2eTest` becomes `test.skip`; the gate reports the step SKIPPED (or `X_VERIFY_SUITE_VANISHED` when `x.verify.json` requires it), never passed |
62
62
  | The seam has an inverse | `resetE2eDriver()`: `useE2eDriver` writes MODULE scope, so a file that installs a browser undoes it |
63
63
  | Built vs declared | `clock` `mail` `network` `runJobs` `statements` `subscribe` are built in-process; `page` `budget` `signIn` `deploy` are declared and wait for a driver (`X_TEST_FIXTURE_UNAVAILABLE`). |
package/README.md CHANGED
@@ -34,7 +34,8 @@ 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>, 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 |
37
+ | `define-exercise.ts` | `defineExercise({ <name>: { actor, input?, runs? } \| { off: '<why>' } }, { actor?: <entity>, factories?: { <entity name>: defineFactory(…) }, setup?: () => mockFetch(…) \| cassette(…) })` — 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
+ | `exercise-setup.ts` | `withExerciseSetup(room, setup)`: the app's `setup` run inside the sealed room before the first row, its handle disposed before the room restores the network; an `allowHost` in it is `X_EXERCISE_SETUP_UNSEALED`. Types `ExerciseSetup`, `ExerciseSetupHandle` |
38
39
  | `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
40
  | `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
41
  | `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.3.0",
3
+ "version": "30.5.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.3.0",
50
- "@ultimat3/cache": "30.3.0",
51
- "@ultimat3/core": "30.3.0",
52
- "@ultimat3/db": "30.3.0",
53
- "@ultimat3/entity": "30.3.0",
54
- "@ultimat3/http": "30.3.0",
55
- "@ultimat3/i18n": "30.3.0",
56
- "@ultimat3/jobs": "30.3.0",
57
- "@ultimat3/mail": "30.3.0",
58
- "@ultimat3/policy": "30.3.0",
59
- "@ultimat3/pwa": "30.3.0",
60
- "@ultimat3/query": "30.3.0",
61
- "@ultimat3/realtime": "30.3.0",
62
- "@ultimat3/render": "30.3.0",
63
- "@ultimat3/schema": "30.3.0",
64
- "@ultimat3/time": "30.3.0"
49
+ "@ultimat3/action": "30.5.0",
50
+ "@ultimat3/cache": "30.5.0",
51
+ "@ultimat3/core": "30.5.0",
52
+ "@ultimat3/db": "30.5.0",
53
+ "@ultimat3/entity": "30.5.0",
54
+ "@ultimat3/http": "30.5.0",
55
+ "@ultimat3/i18n": "30.5.0",
56
+ "@ultimat3/jobs": "30.5.0",
57
+ "@ultimat3/mail": "30.5.0",
58
+ "@ultimat3/policy": "30.5.0",
59
+ "@ultimat3/pwa": "30.5.0",
60
+ "@ultimat3/query": "30.5.0",
61
+ "@ultimat3/realtime": "30.5.0",
62
+ "@ultimat3/render": "30.5.0",
63
+ "@ultimat3/schema": "30.5.0",
64
+ "@ultimat3/time": "30.5.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 { ExerciseSetup } from './exercise-setup';
8
9
  import type { SeedFactory } from './seed-graph-plan';
9
10
 
10
11
  /** 39's option type, exactly: `{ actor, input?, runs? }` or `{ off: '<reason>' }`. */
@@ -18,6 +19,8 @@ export interface ExerciseOverrides {
18
19
  readonly actorEntity?: string;
19
20
  /** By entity name: the pass's graph builds those rows with them (`seedGraph`'s `factories`). */
20
21
  readonly factories?: Readonly<Record<string, SeedFactory>>;
22
+ /** The app's hook, run once inside the pass's sealed room (`exercise-setup.ts`). */
23
+ readonly setup?: ExerciseSetup;
21
24
  }
22
25
 
23
26
  /** What the whole pass needs to know of the app, beside the per-primitive entries. */
@@ -34,6 +37,13 @@ export interface ExerciseAppOptions {
34
37
  * refused when the pass runs.
35
38
  */
36
39
  readonly factories?: Readonly<Record<string, SeedFactory>>;
40
+ /**
41
+ * Run in the exercise process once the network is sealed, before the first row: install the
42
+ * `mockFetch` routes or `cassette()` your vendor-calling handlers need, so they run for real.
43
+ * What it returns (a cassette, any `Disposable`) is disposed after the pass. `allowHost` here is
44
+ * `X_EXERCISE_SETUP_UNSEALED` — the pass never sends a call out.
45
+ */
46
+ readonly setup?: ExerciseSetup;
37
47
  }
38
48
 
39
49
  /** `X_CONTRACT_EXERCISE_UNDECLARED`, 39's code for the same silence: an `off` with no reason. */
@@ -59,6 +69,7 @@ export function defineExercise(
59
69
  entries,
60
70
  ...(app.actor === undefined ? {} : { actorEntity: app.actor.$name }),
61
71
  ...(app.factories === undefined ? {} : { factories: app.factories }),
72
+ ...(app.setup === undefined ? {} : { setup: app.setup }),
62
73
  });
63
74
  }
64
75
 
package/src/errors.ts CHANGED
@@ -69,6 +69,7 @@ export const TESTING_ERROR_CODES = [
69
69
  'X_CASSETTE_SECRET',
70
70
  'X_CASSETTE_RECORD_REFUSED',
71
71
  'X_TEST_MODULE_MOCK_UNRESTORABLE',
72
+ 'X_EXERCISE_SETUP_UNSEALED',
72
73
  ] as const;
73
74
 
74
75
  export type TestingErrorCode = (typeof TESTING_ERROR_CODES)[number];
@@ -133,6 +134,7 @@ export const TESTING_ERROR_TITLES: Readonly<Record<TestingErrorCode, string>> =
133
134
  X_CASSETTE_SECRET: 'a cassette about to be written holds a secret value',
134
135
  X_CASSETTE_RECORD_REFUSED: 'a cassette recording was refused',
135
136
  X_TEST_MODULE_MOCK_UNRESTORABLE: 'a mocked module could not be put back at the file boundary',
137
+ X_EXERCISE_SETUP_UNSEALED: 'an exercise setup let a host through the seal',
136
138
  };
137
139
 
138
140
  // Titles must be registered for `format()` to render the contract's first line. Every code above is
@@ -161,7 +163,7 @@ export class NetworkSealedError extends UltimateError {
161
163
  url: string;
162
164
  method: string;
163
165
  allowed: readonly string[];
164
- transport?: 'fetch' | 'socket' | undefined;
166
+ transport?: 'fetch' | 'socket' | 'dns' | undefined;
165
167
  }) {
166
168
  const allowCall = `allowHost('${hostOf(input.url)}')`;
167
169
  const allow = `${allowCall} if it must be real`;
@@ -173,7 +175,9 @@ export class NetworkSealedError extends UltimateError {
173
175
  fix:
174
176
  input.transport === 'socket'
175
177
  ? `${allowCall} # only if it must be real — otherwise inject the transport: pass the code under test its \`connect\`/\`client\` option, or dial a Bun.listen server on 127.0.0.1 (loopback is never sealed)`
176
- : `mockFetch('${input.url}', () => new Response('{}')) — or ${allow}`,
178
+ : input.transport === 'dns'
179
+ ? `mockFetch('https://${hostOf(input.url)}/*', () => new Response('{}')) # a mocked host resolves — or ${allow}`
180
+ : `mockFetch('${input.url}', () => new Response('{}')) — or ${allow}`,
177
181
  });
178
182
  }
179
183
  }
@@ -18,6 +18,7 @@ const findingOf = (divergence: CrashDivergence): CrashFinding => {
18
18
  clean: divergence.clean,
19
19
  killed: divergence.killed,
20
20
  ...(typeof after === 'string' ? { after } : {}),
21
+ ...(divergence.inside === undefined ? {} : { inside: divergence.inside }),
21
22
  };
22
23
  };
23
24
 
@@ -0,0 +1,71 @@
1
+ // What a 5xx tells the author beyond its code: the coded error's cause, the statement or the fields
2
+ // it names, every value the call's input held under a credential key or a `.sensitive()` field
3
+ // masked out of the text. Without it, every X_DB_STATEMENT_FAILED in the pass was rerun by hand.
4
+
5
+ import { captureValue, isUltimateError, REDACTED, stringField } from '@ultimat3/core';
6
+ import type { StandardSchemaV1 } from '@ultimat3/schema';
7
+
8
+ /** What `ExerciseRecord.defect` holds. */
9
+ export interface ExerciseDefect {
10
+ /** The error's own cause, credential values masked, cut at `MAX_DEFECT_CAUSE`. */
11
+ readonly cause: string;
12
+ /** The SQL a database refusal names (`… — statement: <sql>`), parameters as `$n`. */
13
+ readonly statement?: string;
14
+ /** The paths a validation failure names (`items.0.sku`). */
15
+ readonly fields?: readonly string[];
16
+ }
17
+
18
+ /** A cause long enough for a SQLSTATE, its message and a capped statement; past it, cut. */
19
+ export const MAX_DEFECT_CAUSE = 600;
20
+
21
+ const STATEMENT = / — statement: (.+)$/s;
22
+
23
+ /** Every scalar the masked copy replaced: what must not appear in the cause either. */
24
+ function maskedScalars(original: unknown, masked: unknown, into: string[]): void {
25
+ if (masked === REDACTED) {
26
+ if (typeof original === 'string' || typeof original === 'number') into.push(String(original));
27
+ return;
28
+ }
29
+ if (typeof original !== 'object' || original === null) return;
30
+ if (typeof masked !== 'object' || masked === null) return;
31
+ for (const [key, entry] of Object.entries(original)) {
32
+ maskedScalars(entry, Reflect.get(masked, key), into);
33
+ }
34
+ }
35
+
36
+ const fieldsOf = (meta: Readonly<Record<string, unknown>> | undefined): readonly string[] => {
37
+ const issues = meta?.['issues'];
38
+ if (!Array.isArray(issues)) return [];
39
+ return issues.flatMap((issue: unknown) => {
40
+ const path = stringField(issue, 'path');
41
+ return path === undefined || path === '' ? [] : [path];
42
+ });
43
+ };
44
+
45
+ /**
46
+ * The detail of a CODED failure, or `undefined` for an uncoded throw — whose reason already is the
47
+ * throw. `schema` is the primitive's input: its `.sensitive()` fields are masked as `redactKeys`
48
+ * ones are (core's `captureValue`), so the record holds no value the audit log would not.
49
+ */
50
+ export async function defectDetail(
51
+ error: unknown,
52
+ input: unknown,
53
+ schema: StandardSchemaV1,
54
+ ): Promise<ExerciseDefect | undefined> {
55
+ if (!isUltimateError(error)) return undefined;
56
+ // On demand: `@ultimat3/schema` is a dynamic-only dependency of this barrel (`CLAUDE.md`).
57
+ const { maskSensitive } = await import('@ultimat3/schema');
58
+ const secrets: string[] = [];
59
+ maskedScalars(input, captureValue(maskSensitive(schema, input, REDACTED)).value, secrets);
60
+ let cause = error.cause;
61
+ for (const value of [...new Set(secrets)].sort((a, b) => b.length - a.length)) {
62
+ if (value !== '') cause = cause.split(value).join(REDACTED);
63
+ }
64
+ const statement = STATEMENT.exec(cause)?.[1]?.trim();
65
+ const fields = fieldsOf(error.meta);
66
+ return {
67
+ cause: cause.length > MAX_DEFECT_CAUSE ? `${cause.slice(0, MAX_DEFECT_CAUSE - 1)}…` : cause,
68
+ ...(statement === undefined || statement === '' ? {} : { statement }),
69
+ ...(fields.length === 0 ? {} : { fields }),
70
+ };
71
+ }
@@ -31,8 +31,15 @@ const answer = (handle: TenantOf, input: unknown): string | undefined => {
31
31
  }
32
32
  };
33
33
 
34
+ /** How deep an absent parent is filled in to read past it: `payload.input.orgId` is two. */
35
+ const ABSENT_DEPTH = 4;
36
+
37
+ /** The value at `path`, `undefined` when any step of it is absent. */
38
+ const getAt = (input: unknown, path: Path): unknown =>
39
+ path.reduce<unknown>((value, key) => (isPlainObject(value) ? value[key] : undefined), input);
40
+
34
41
  /** Every property path `tenantFor` read, at any depth of plain objects — a missing one included. */
35
- function pathsRead(handle: TenantOf, input: unknown): readonly Path[] {
42
+ function readOnce(handle: TenantOf, input: unknown): readonly Path[] {
36
43
  const read: Path[] = [];
37
44
  const watch = (value: unknown, at: Path): unknown =>
38
45
  !isPlainObject(value)
@@ -49,8 +56,28 @@ function pathsRead(handle: TenantOf, input: unknown): readonly Path[] {
49
56
  return read;
50
57
  }
51
58
 
59
+ /**
60
+ * Every path read, past absent parents too: a read that found nothing is filled with `{}` and read
61
+ * again, so `payload.input.orgId` — a scoped backfill's tenant, its `input` left out of the
62
+ * minimal draw — is found, not lost to the throw reading `orgId` of `undefined`.
63
+ */
64
+ function pathsRead(handle: TenantOf, input: unknown, depth = 0): readonly Path[] {
65
+ const reads = readOnce(handle, input);
66
+ if (depth >= ABSENT_DEPTH) return reads;
67
+ const deeper = reads
68
+ .filter((path) => getAt(input, path) === undefined)
69
+ .flatMap((path) => pathsRead(handle, setAt(input, path, {}), depth + 1));
70
+ const seen = new Set<string>();
71
+ return [...reads, ...deeper].filter((path) => {
72
+ const key = JSON.stringify(path);
73
+ if (seen.has(key)) return false;
74
+ seen.add(key);
75
+ return true;
76
+ });
77
+ }
78
+
52
79
  /** `input` with `path` set to `value`, copied along the path only. */
53
- function setAt(input: unknown, path: Path, value: string): unknown {
80
+ function setAt(input: unknown, path: Path, value: unknown): unknown {
54
81
  const [head, ...rest] = path;
55
82
  if (head === undefined) return value;
56
83
  const base = isPlainObject(input) ? input : {};
@@ -12,7 +12,9 @@ import {
12
12
  } from '@ultimat3/db';
13
13
  import type { RowObserver } from '@ultimat3/entity';
14
14
  import { captureDeterminism, installDeterminism, restoreCapturedDeterminism } from './determinism';
15
+ import type { ExerciseDefect } from './exercise-defect';
15
16
  import { testMail } from './fixture-mail';
17
+ import { routeMatches } from './mock-route';
16
18
  import type { PlanProof, SentStatement } from './plan-sweep';
17
19
  import type { ScaleProof, StatementTally } from './scale-sweep';
18
20
  import {
@@ -71,8 +73,10 @@ export interface CrashFinding {
71
73
  readonly effect: string;
72
74
  readonly clean: number;
73
75
  readonly killed: number;
74
- /** The last step the clean run had written before the effect; absent, before the first. */
76
+ /** The last step the clean run had completed before the effect; absent, before the first. */
75
77
  readonly after?: string;
78
+ /** The step whose body made the effect, when it was made inside one: it runs again on a retry. */
79
+ readonly inside?: string;
76
80
  }
77
81
 
78
82
  /** The crash proof's verdict on a reached job: killed at every step boundary (`crashProof`). */
@@ -94,6 +98,8 @@ export interface ExerciseRecord {
94
98
  readonly outcome: ExerciseOutcome;
95
99
  readonly code?: string;
96
100
  readonly reason?: string;
101
+ /** On an `unreached` one that answered a 5xx: its cause, statement and fields, values masked. */
102
+ readonly defect?: ExerciseDefect;
97
103
  readonly statements: number;
98
104
  readonly effects: readonly ExerciseEffect[];
99
105
  readonly ms: number;
@@ -228,6 +234,10 @@ export async function openExerciseRoom(): Promise<ExerciseRoom> {
228
234
  },
229
235
  end: (): Observed => {
230
236
  const calls = requestLog().slice(logFrom);
237
+ // A call a route answered never left the process: the app's `setup` installed it.
238
+ const { mocks } = captureNetwork();
239
+ const refused = (call: (typeof calls)[number]): boolean =>
240
+ !local(call.url) && !mocks.some((route) => routeMatches(route, call));
231
241
  const effects: ExerciseEffect[] = [
232
242
  ...[...tally(rows)].map(([key, count]): ExerciseEffect => {
233
243
  const [entity = '', op = ''] = key.split('\u0000');
@@ -241,11 +251,11 @@ export async function openExerciseRoom(): Promise<ExerciseRoom> {
241
251
  kind: 'network',
242
252
  method: call.method,
243
253
  host: hostOf(call.url),
244
- refused: !local(call.url),
254
+ refused: refused(call),
245
255
  }),
246
256
  ),
247
257
  ];
248
- const refusedHosts = calls.filter((call) => !local(call.url)).map((c) => hostOf(c.url));
258
+ const refusedHosts = calls.filter(refused).map((c) => hostOf(c.url));
249
259
  const payloads: EffectPayload[] = [
250
260
  ...mail.outbox().map(({ message }, i) => ({
251
261
  path: `effect.mail[${i}]`,
@@ -0,0 +1,68 @@
1
+ // `defineExercise(…, { setup })`, run: the app's hook inside the pass's sealed room, before the
2
+ // first row is seeded, disposed before the room restores the network. What it installs — a
3
+ // `mockFetch` route, a `cassette()` — answers a vendor call; `allowHost` would send one for real.
4
+
5
+ import { renderCauseValue, UltimateError } from '@ultimat3/core';
6
+ import type { ExerciseRoom } from './exercise-report';
7
+ import { captureNetwork } from './sealed-network';
8
+
9
+ /** What a setup may hand back to be undone after the pass: a `cassette()`, a `using` resource. */
10
+ export type ExerciseSetupHandle = Disposable | AsyncDisposable;
11
+
12
+ /**
13
+ * Runs once per pass, in the exercise process, after the seal and before any primitive. Returns
14
+ * nothing when there is nothing to undo — a `mockFetch` route goes with the room.
15
+ */
16
+ export type ExerciseSetup = () =>
17
+ | void
18
+ | ExerciseSetupHandle
19
+ | Promise<void>
20
+ | Promise<ExerciseSetupHandle>
21
+ | Promise<ExerciseSetupHandle | undefined>;
22
+
23
+ const setupUnsealed = (hosts: readonly string[]): UltimateError =>
24
+ new UltimateError({
25
+ code: 'X_EXERCISE_SETUP_UNSEALED',
26
+ cause: `defineExercise(…, { setup }) called allowHost(${hosts.map((host) => renderCauseValue(host)).join(', ')}): the pass would send a primitive's call out for real, where every vendor is answered by mockFetch or cassette()`,
27
+ fix: "mockFetch('https://<host>/*', handler) # or cassette(import.meta.dir, '<name>') — in apps/web/exercise.ts's setup, in place of allowHost('<host>')",
28
+ meta: { hosts },
29
+ });
30
+
31
+ async function release(handle: unknown): Promise<void> {
32
+ if (typeof handle !== 'object' || handle === null) return;
33
+ if (Symbol.asyncDispose in handle) await (handle as AsyncDisposable)[Symbol.asyncDispose]();
34
+ else if (Symbol.dispose in handle) (handle as Disposable)[Symbol.dispose]();
35
+ }
36
+
37
+ /**
38
+ * `room` with `setup` run inside it. Its dispose releases what setup returned FIRST — a cassette's
39
+ * own check reads the routes the pass used — then the room. A setup that throws, or lets a host
40
+ * through, disposes both before the pass refuses.
41
+ */
42
+ export async function withExerciseSetup(
43
+ room: ExerciseRoom,
44
+ setup: ExerciseSetup | undefined,
45
+ ): Promise<ExerciseRoom> {
46
+ if (setup === undefined) return room;
47
+ const before = new Set(captureNetwork().allowed);
48
+ let handle: unknown;
49
+ try {
50
+ handle = await setup();
51
+ } catch (error) {
52
+ await room[Symbol.asyncDispose]();
53
+ throw error;
54
+ }
55
+ const dispose = async (): Promise<void> => {
56
+ try {
57
+ await release(handle);
58
+ } finally {
59
+ await room[Symbol.asyncDispose]();
60
+ }
61
+ };
62
+ const opened = captureNetwork().allowed.filter((host) => !before.has(host));
63
+ if (opened.length > 0) {
64
+ await dispose();
65
+ throw setupUnsealed(opened);
66
+ }
67
+ return { ...room, [Symbol.asyncDispose]: dispose };
68
+ }
@@ -3,7 +3,7 @@
3
3
  // (`exercise-job-tenant.ts`): a drawn org is a random one, and the enqueue refuses it.
4
4
 
5
5
  import type { Actor } from '@ultimat3/core';
6
- import { renderCauseValue, renderFixShellArg } from '@ultimat3/core';
6
+ import { renderCauseValue, renderFixShellArg, UltimateError } from '@ultimat3/core';
7
7
  import type { AnyJobHandle, JobExecution } from '@ultimat3/jobs';
8
8
  import { registeredJobs } from '@ultimat3/jobs';
9
9
  import type { Policy } from '@ultimat3/policy';
@@ -58,8 +58,22 @@ export function fixedTenant(name: string, declared: string | undefined) {
58
58
 
59
59
  const ANSWERED: ReadonlySet<string> = new Set(['completed', 'suspended']);
60
60
 
61
- const statusOfJobFailure = (run: JobExecution | undefined): string =>
62
- run === undefined ? 'the job never ran' : (run.error ?? `the run ended ${run.outcome}`);
61
+ /**
62
+ * What a failed run hands the verdict. A run whose error carried a code is that code again, so
63
+ * core's `exerciseDefect` judges it by the one rule an action's refusal meets: a coded refusal
64
+ * under 500 is the job answering "no", a 5xx or an uncoded throw is a defect. The message alone
65
+ * read every coded refusal — an app's 409 included — as an uncoded throw: X_CONTRACT_DEFECT.
66
+ */
67
+ const jobFailure = (run: JobExecution | undefined): unknown => {
68
+ if (run === undefined) return 'the job never ran';
69
+ const message = run.error ?? `the run ended ${run.outcome}`;
70
+ if (run.code === undefined) return message;
71
+ return new UltimateError({
72
+ code: run.code,
73
+ cause: message,
74
+ fix: `x errors explain ${renderFixShellArg(run.code, '<code>')} --json`,
75
+ });
76
+ };
63
77
 
64
78
  const jobSubject = (handle: AnyJobHandle): Subject => ({
65
79
  kind: 'job',
@@ -73,11 +87,11 @@ const jobSubject = (handle: AnyJobHandle): Subject => ({
73
87
  const queued = await jobs.enqueue(handle, input, { tenantId: tenant });
74
88
  const trace = await jobs.drain(actor === null ? {} : { actor });
75
89
  const own = trace.executions.find((run) => run.jobId === queued.id);
76
- // The worker's failure is a string on the row, never a throw: it is the attempt's error.
90
+ // The worker's failure is on the row, never a throw: the attempt's error and its code.
77
91
  // Suspended is a body that ran and parked (`step.sleep`, `waitForEvent`): reached.
78
92
  return own !== undefined && ANSWERED.has(own.outcome)
79
93
  ? { output: own.result }
80
- : { failure: statusOfJobFailure(own) };
94
+ : { failure: jobFailure(own) };
81
95
  },
82
96
  });
83
97
 
package/src/exercise.ts CHANGED
@@ -15,6 +15,7 @@ import { assertOverridesKnown, unknownDeclaration } from './define-exercise';
15
15
  import type { RoleGrants } from './exercise-actor';
16
16
  import { chooseActor } from './exercise-actor';
17
17
  import { proveCrash } from './exercise-crash';
18
+ import { defectDetail } from './exercise-defect';
18
19
  import type { IdLookup, Sampler } from './exercise-input';
19
20
  import {
20
21
  bindIds,
@@ -34,12 +35,13 @@ import type {
34
35
  Observed,
35
36
  } from './exercise-report';
36
37
  import { openExerciseRoom, overrideLine } from './exercise-report';
38
+ import { withExerciseSetup } from './exercise-setup';
37
39
  import type { Subject } from './exercise-subjects';
38
40
  import { fixedTenant, seatIn, subjectsOf } from './exercise-subjects';
39
41
  import { actorRowsOf, attachSweeps, seated, sizedCall } from './exercise-sweeps';
40
42
  import type { RunJobs } from './fixture-jobs';
41
43
  import { testJobs } from './fixture-jobs';
42
- import { pairGraph, seedPass, tenantPair } from './seed-graph-pass';
44
+ import { deferredMayStayNull, pairGraph, seedPass, tenantPair } from './seed-graph-pass';
43
45
  import type { SeedGraph, SeedWriter } from './seed-graph-plan';
44
46
  import type { SeedEntity } from './seed-graph-shape';
45
47
  import { tenantTaint } from './tenant-canary';
@@ -211,7 +213,20 @@ async function control(
211
213
  const observed = room.end();
212
214
  const effects = [...observed.effects, ...(await enqueued(subject, jobs, executions))];
213
215
  const verdictOf = judged(subject, { verdict, answered, lastError, lastInput }, observed);
214
- const record = { kind, name, statements: observed.statements, effects, ms, ...verdictOf };
216
+ // A 5xx's cause, statement and fields: the code alone sent the author to reproduce it by hand.
217
+ const defect =
218
+ verdict.kind === 'defect' && verdictOf.outcome === 'unreached'
219
+ ? await defectDetail(verdict.error, verdict.input, subject.input)
220
+ : undefined;
221
+ const record = {
222
+ kind,
223
+ name,
224
+ statements: observed.statements,
225
+ effects,
226
+ ms,
227
+ ...verdictOf,
228
+ ...(defect === undefined ? {} : { defect }),
229
+ };
215
230
  return { record, output, input: lastInput };
216
231
  }
217
232
 
@@ -343,7 +358,8 @@ export async function runExercisePass(
343
358
  const sampler = await loadSampler();
344
359
  // Not `Math.random`: the frozen RNG answers the same sequence in every process.
345
360
  const passSeed = options.seed ?? (crypto.getRandomValues(new Uint32Array(1))[0] as number);
346
- const room = await openExerciseRoom();
361
+ // The app's setup runs inside the seal, before the first row: its routes answer every call.
362
+ const room = await withExerciseSetup(await openExerciseRoom(), options.overrides?.setup);
347
363
  const live = chosen.filter((subject) => !isOff(options.overrides, subject.name));
348
364
  const records: ExerciseRecord[] = [];
349
365
  let graph: SeedGraph;
@@ -372,7 +388,8 @@ export async function runExercisePass(
372
388
  return attachSweeps(records, {
373
389
  subjects: chosen.filter((subject) => reached.has(subject.name)),
374
390
  entities: options.entities,
375
- writer: options.writer,
391
+ // The sized graphs close their cycles as the pass's did: a refused deferred key stays null.
392
+ writer: deferredMayStayNull(options.writer),
376
393
  main: graph,
377
394
  seed: passSeed,
378
395
  room,
@@ -5,7 +5,8 @@
5
5
  import type { EntityLike, Factory, FactoryIds } from './factories';
6
6
  import { defineFactory } from './factories';
7
7
  import { seedGraphUnsatisfied } from './seed-graph-error';
8
- import { SEED_SOFT_DELETE_PROPERTY } from './seed-graph-shape';
8
+ import { declaredPatterns } from './seed-graph-patterns';
9
+ import { SEED_SOFT_DELETE_PROPERTY, type SeedEntity } from './seed-graph-shape';
9
10
  import { candidatesFor, isSeedColumn } from './seed-graph-values';
10
11
 
11
12
  export type EntityRegistry = Readonly<Record<string, EntityLike>>;
@@ -25,13 +26,14 @@ export function factoriesFor<TRegistry extends EntityRegistry>(
25
26
  ): FactoryRegistry<TRegistry> {
26
27
  const out: Record<string, Factory<Record<string, unknown>>> = {};
27
28
  for (const [name, entity] of Object.entries(registry)) {
29
+ const patterns = hasInvariants(entity) ? declaredPatterns(entity) : new Map();
28
30
  out[name] = defineFactory(entity, {
29
31
  ...(seed === undefined ? {} : { seed }),
30
32
  defaults: (index, ids): Record<string, unknown> => {
31
33
  const row: Record<string, unknown> = {};
32
34
  Object.entries(entity.$columns).forEach(([property, column], position) => {
33
35
  row[property] = defaultFor(
34
- { table: entity.$table, property, column, position },
36
+ { table: entity.$table, property, column, position, patterns: patterns.get(property) },
35
37
  index,
36
38
  ids,
37
39
  );
@@ -43,12 +45,21 @@ export function factoriesFor<TRegistry extends EntityRegistry>(
43
45
  return out as FactoryRegistry<TRegistry>;
44
46
  }
45
47
 
48
+ /** An `entity()` value, which declares rules; a hand-built `EntityLike` may declare none. */
49
+ const hasInvariants = (entity: object): entity is Pick<SeedEntity, '$invariants' | '$describe'> =>
50
+ '$invariants' in entity &&
51
+ Array.isArray(entity.$invariants) &&
52
+ '$describe' in entity &&
53
+ typeof entity.$describe === 'function';
54
+
46
55
  /** One column of one entity: what `defaultFor` reads. The name labels a refusal, nothing more. */
47
56
  export interface DefaultColumn {
48
57
  readonly table: string;
49
58
  readonly property: string;
50
59
  readonly column: unknown;
51
60
  readonly position: number;
61
+ /** The `matches()` patterns the entity holds this column to (`declaredPatterns`). */
62
+ readonly patterns?: readonly RegExp[] | undefined;
52
63
  }
53
64
 
54
65
  /**
@@ -68,6 +79,7 @@ export function defaultFor(target: DefaultColumn, index: number, ids: FactoryIds
68
79
  ordinal: index,
69
80
  canary: String(index),
70
81
  position: target.position,
82
+ patterns: target.patterns,
71
83
  uuid: () => ids.uuid(),
72
84
  })
73
85
  : [];
package/src/index.ts CHANGED
@@ -200,6 +200,7 @@ export {
200
200
  export type { ExercisePassOptions } from './exercise';
201
201
  export { runExercisePass } from './exercise';
202
202
  export type { ActorChoice, RoleGrants } from './exercise-actor';
203
+ export type { ExerciseDefect } from './exercise-defect';
203
204
  export type {
204
205
  CrashFinding,
205
206
  CrashProof,
@@ -212,6 +213,7 @@ export type {
212
213
  TenantProof,
213
214
  } from './exercise-report';
214
215
  export { overrideLine } from './exercise-report';
216
+ export type { ExerciseSetup, ExerciseSetupHandle } from './exercise-setup';
215
217
  export type {
216
218
  Association,
217
219
  AssociationMap,
@@ -375,6 +377,7 @@ export { crashProof } from './job-crash-proof';
375
377
  export { jobDriverConformance } from './job-driver-conformance';
376
378
  export type { SimEffect } from './job-sim-effects';
377
379
  export {
380
+ crashEffectPlace,
378
381
  JobCrashDivergedError,
379
382
  JobSimInvariantError,
380
383
  JobSimNondeterministicError,
@@ -443,6 +446,7 @@ export {
443
446
  allowHost,
444
447
  captureNetwork,
445
448
  isNetworkSealed,
449
+ MOCKED_HOST_ADDRESS,
446
450
  mockFetch,
447
451
  mockJson,
448
452
  networkState,