@ultimat3/testing 29.4.0 → 30.1.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.
Files changed (93) hide show
  1. package/CLAUDE.md +56 -63
  2. package/README.md +281 -6
  3. package/package.json +27 -16
  4. package/src/ax-node.ts +96 -0
  5. package/src/cassette-errors.ts +91 -0
  6. package/src/cassette-format.ts +175 -0
  7. package/src/cassette-record.ts +120 -0
  8. package/src/cassette-secret-scan.ts +77 -0
  9. package/src/cassette.ts +166 -0
  10. package/src/cdp-browser.ts +5 -3
  11. package/src/cdp-e2e-page.ts +91 -3
  12. package/src/cdp-e2e-session.ts +18 -1
  13. package/src/define-exercise.ts +92 -0
  14. package/src/drill-budgets.ts +54 -0
  15. package/src/drill-errors.ts +45 -0
  16. package/src/drill-fleet.ts +251 -0
  17. package/src/drill-jobs.ts +79 -0
  18. package/src/drill-load.ts +166 -0
  19. package/src/drill-probe.ts +68 -0
  20. package/src/drill-run.ts +144 -0
  21. package/src/drill-services.ts +70 -0
  22. package/src/drill-sync.ts +108 -0
  23. package/src/drill-verdict.ts +256 -0
  24. package/src/drill-writes.ts +123 -0
  25. package/src/drills.ts +109 -0
  26. package/src/e2e-a11y-run.ts +189 -0
  27. package/src/e2e-a11y-tabs.ts +113 -0
  28. package/src/e2e-a11y.ts +153 -0
  29. package/src/e2e-driver.ts +46 -10
  30. package/src/e2e-page.ts +28 -3
  31. package/src/e2e-run.ts +1 -0
  32. package/src/e2e-spawn.ts +3 -2
  33. package/src/errors.ts +43 -0
  34. package/src/exercise-actor.ts +124 -0
  35. package/src/exercise-crash.ts +46 -0
  36. package/src/exercise-input.ts +182 -0
  37. package/src/exercise-override-input.ts +34 -0
  38. package/src/exercise-report.ts +282 -0
  39. package/src/exercise-request.ts +38 -0
  40. package/src/exercise-sweeps.ts +100 -0
  41. package/src/exercise.ts +451 -0
  42. package/src/factories.ts +10 -7
  43. package/src/factory-registry.ts +49 -15
  44. package/src/fake-identity-provider.ts +191 -0
  45. package/src/fault-proxy.ts +342 -0
  46. package/src/file-boundary.ts +4 -1
  47. package/src/fixture-jobs.ts +3 -1
  48. package/src/fixture-statements.ts +4 -1
  49. package/src/free-port.ts +59 -0
  50. package/src/index.ts +148 -3
  51. package/src/island-dom.ts +5 -0
  52. package/src/isolated-plugins.ts +1 -1
  53. package/src/job-crash-proof.ts +249 -0
  54. package/src/job-sim-driver.ts +164 -0
  55. package/src/job-sim-effects.ts +142 -0
  56. package/src/job-sim-errors.ts +72 -0
  57. package/src/job-sim-schedule.ts +154 -0
  58. package/src/job-sim-world.ts +198 -0
  59. package/src/job-simulate.ts +179 -0
  60. package/src/mock-route.ts +81 -0
  61. package/src/module-mock-boundary.ts +85 -0
  62. package/src/module-mock-error.ts +28 -0
  63. package/src/plan-oracle.ts +164 -0
  64. package/src/plan-sweep-fix.ts +123 -0
  65. package/src/plan-sweep-report.ts +84 -0
  66. package/src/plan-sweep.ts +148 -0
  67. package/src/preload.ts +24 -4
  68. package/src/registry-snapshot.ts +1 -1
  69. package/src/repro-stand-in.ts +67 -0
  70. package/src/repro.ts +158 -0
  71. package/src/request-log.ts +64 -0
  72. package/src/reusable-database.ts +1 -1
  73. package/src/scale-sweep-graph.ts +82 -0
  74. package/src/scale-sweep-run.ts +134 -0
  75. package/src/scale-sweep.ts +74 -0
  76. package/src/sealed-network.ts +63 -46
  77. package/src/seed-graph-error.ts +50 -0
  78. package/src/seed-graph-order.ts +98 -0
  79. package/src/seed-graph-pass.ts +85 -0
  80. package/src/seed-graph-plan.ts +81 -0
  81. package/src/seed-graph-row.ts +134 -0
  82. package/src/seed-graph-shape.ts +104 -0
  83. package/src/seed-graph-values.ts +135 -0
  84. package/src/seed-graph.ts +250 -0
  85. package/src/seed-writer.ts +180 -0
  86. package/src/subject-proof-error.ts +23 -0
  87. package/src/subject-proof.ts +180 -0
  88. package/src/template-db-roles.ts +51 -0
  89. package/src/template-db.ts +8 -2
  90. package/src/tenant-canary.ts +120 -0
  91. package/src/tenant-proof-report.ts +62 -0
  92. package/src/tenant-proof.ts +177 -0
  93. package/src/test-types.ts +12 -3
@@ -0,0 +1,84 @@
1
+ // The plan and scale sweeps, as `x exercise` reports them: one coded finding per statement no index
2
+ // serves (every primitive that sent it named) and per shape that grows with the rows, each with the
3
+ // edit that ends it — the `indexes:` entry read off the entity, or the `preload` — and one line each.
4
+
5
+ import { renderCauseValue, renderFixLiteral, renderFixShellArg } from '@ultimat3/core';
6
+ import type { ExerciseRecord } from './exercise-report';
7
+ import type { PlanFinding } from './plan-sweep-fix';
8
+
9
+ export interface SweepFinding {
10
+ readonly code: 'X_QUERY_UNINDEXED' | 'X_QUERY_SORT_UNSERVED' | 'X_QUERY_SCALES_WITH_ROWS';
11
+ readonly cause: string;
12
+ readonly fix: string;
13
+ /** The primitive, by kind and name: the registry records no declaring file. */
14
+ readonly at: string;
15
+ }
16
+
17
+ const primitive = (record: ExerciseRecord): string => `${record.kind} ${record.name}`;
18
+
19
+ const STATEMENT_CHARS = 160;
20
+ const excerpt = (statement: string): string =>
21
+ statement.length <= STATEMENT_CHARS ? statement : `${statement.slice(0, STATEMENT_CHARS)}…`;
22
+
23
+ /** `indexes: [{ on: ['orgId', 'authorId'] }]` — the line to paste into the entity. */
24
+ const indexLine = (finding: PlanFinding): string =>
25
+ `indexes: [{ on: [${finding.index.map((p) => `'${p}'`).join(', ')}] }]`;
26
+
27
+ function planFinding(finding: PlanFinding, senders: readonly ExerciseRecord[]): SweepFinding {
28
+ const by = senders.map(primitive).join(', ');
29
+ const entity = renderFixLiteral(finding.entity, '<entity>');
30
+ const name = renderFixShellArg(`${finding.table}-${finding.index.join('-')}-index`, '<name>');
31
+ const what =
32
+ finding.code === 'X_QUERY_SORT_UNSERVED'
33
+ ? `sorts its rows to answer a limited read (${finding.node} under Limit)`
34
+ : `reads ${finding.table} whole (${finding.node})`;
35
+ const scan =
36
+ finding.code === 'X_QUERY_UNINDEXED'
37
+ ? `; or scan: '<why it stays small>' in that entity()`
38
+ : '';
39
+ return {
40
+ code: finding.code,
41
+ cause: `${by} sent ${renderCauseValue(excerpt(finding.statement))}, which ${what}: no index on ${finding.table} serves it, so it slows with every row the table gains`,
42
+ fix: `x db gen ${name} # after ${indexLine(finding)} in entity(${entity})${scan}`,
43
+ at: senders[0] === undefined ? finding.entity : primitive(senders[0]),
44
+ };
45
+ }
46
+
47
+ /** One finding per statement and red node, naming every primitive that sent it. */
48
+ export function planSweepFindings(records: readonly ExerciseRecord[]): readonly SweepFinding[] {
49
+ const groups = new Map<string, { finding: PlanFinding; senders: ExerciseRecord[] }>();
50
+ for (const record of records) {
51
+ for (const finding of record.plans?.findings ?? []) {
52
+ const key = `${finding.code}\u0000${finding.table}\u0000${finding.statement}`;
53
+ const group = groups.get(key) ?? { finding, senders: [] };
54
+ group.senders.push(record);
55
+ groups.set(key, group);
56
+ }
57
+ }
58
+ const plans = [...groups.values()].map(({ finding, senders }) => planFinding(finding, senders));
59
+ const scale = records.flatMap((record): SweepFinding[] => {
60
+ if (record.scale?.verdict !== 'grows') return [];
61
+ const name = renderFixShellArg(record.name, '<primitive>');
62
+ return record.scale.shapes.map((shape) => ({
63
+ code: 'X_QUERY_SCALES_WITH_ROWS',
64
+ cause: `${primitive(record)} sent ${renderCauseValue(shape.fingerprint)} ${shape.small}× at 2 rows a table and ${shape.large}× at 8: one statement per row, a loop no test named`,
65
+ fix: `x exercise ${name} --json # after preload() of the relation the loop reads, or expectedQueryLoop('<why>', fn) around a deliberate loop`,
66
+ at: primitive(record),
67
+ }));
68
+ });
69
+ return [...plans, ...scale];
70
+ }
71
+
72
+ /** One line per red: `action listPosts · X_QUERY_UNINDEXED posts on [orgId, authorId]`. */
73
+ export function planSweepLines(records: readonly ExerciseRecord[]): readonly string[] {
74
+ return records.flatMap((record) => [
75
+ ...(record.plans?.findings ?? []).map(
76
+ (finding) =>
77
+ `${primitive(record)} · ${finding.code} ${finding.table} on [${finding.index.join(', ')}]`,
78
+ ),
79
+ ...(record.scale?.verdict === 'grows' ? record.scale.shapes : []).map(
80
+ (shape) =>
81
+ `${primitive(record)} · X_QUERY_SCALES_WITH_ROWS ${shape.fingerprint} ${shape.small}→${shape.large}`,
82
+ ),
83
+ ]);
84
+ }
@@ -0,0 +1,148 @@
1
+ // The plan sweep: every distinct statement the exercise pass sent whose first relation is an app
2
+ // table, planned on the migrated database with sequential scans, bitmap scans and sorts disabled,
3
+ // judged by `plan-oracle.ts`, each red node named as the `indexes:` entry that answers it.
4
+ // Read-only: `PREPARE` in a read-only transaction whose settings die with it, then `DEALLOCATE`.
5
+
6
+ import { renderThrowable, UltimateError } from '@ultimat3/core';
7
+ import type { DbClient } from '@ultimat3/db';
8
+ import { isReservable, raw, sql, withReadTransaction } from '@ultimat3/db';
9
+ import './errors';
10
+ import type { PlanTableColumns } from './plan-oracle';
11
+ import { judgePlan } from './plan-oracle';
12
+ import type { PlanFinding } from './plan-sweep-fix';
13
+ import { planFindings } from './plan-sweep-fix';
14
+ import type { SeedEntity } from './seed-graph-shape';
15
+
16
+ /** What the pass's room kept of one statement text: who sent it, under which scan reason. */
17
+ export interface SentStatement {
18
+ readonly primitives: ReadonlySet<string>;
19
+ /** The `expectedSeqScan` reason in force when it was sent. */
20
+ readonly scan?: string;
21
+ }
22
+
23
+ export interface PlanExempt {
24
+ readonly statement: string;
25
+ readonly reason: string;
26
+ }
27
+
28
+ /** One primitive's statements, planned. Only `findings` is red. */
29
+ export interface PlanProof {
30
+ readonly planned: number;
31
+ readonly findings: readonly PlanFinding[];
32
+ /** A table that declared `scan`, or a statement under `expectedSeqScan`: why it may scan. */
33
+ readonly exempt: readonly PlanExempt[];
34
+ /** Generic planning refused it (an untyped parameter): listed and counted, never red. */
35
+ readonly unplannable: readonly PlanExempt[];
36
+ }
37
+
38
+ /** The server cannot hand back a generic plan: no statement was judged, which is never a pass. */
39
+ export class QueryPlanUnavailableError extends UltimateError {
40
+ constructor(version: string) {
41
+ super({
42
+ code: 'X_QUERY_PLAN_UNAVAILABLE',
43
+ cause: `the gate's database answered server_version_num ${version}: below Postgres 12 there is no plan_cache_mode, so the plan sweep cannot read a generic plan and no statement was judged`,
44
+ fix: 'DATABASE_URL= x verify --only exercise # the embedded database answers every plan',
45
+ meta: { version },
46
+ });
47
+ }
48
+ }
49
+
50
+ /** The planner's choices the sweep removes: what remains is what the schema can serve. */
51
+ const SETTINGS = {
52
+ enable_seqscan: 'off',
53
+ enable_bitmapscan: 'off',
54
+ enable_sort: 'off',
55
+ plan_cache_mode: 'force_generic_plan',
56
+ } as const;
57
+ const PREPARED = 'x_plan_sweep';
58
+ const FIRST_RELATION = /\b(?:from|into|update|join)\s+(?:"?\w+"?\.)?"?([A-Za-z_][\w$]*)"?/i;
59
+
60
+ /** `$1` … `$n`: the highest one is how many nulls `execute` passes. */
61
+ const parameterCount = (statement: string): number =>
62
+ Math.max(0, ...[...statement.matchAll(/\$(\d+)/g)].map((match) => Number(match[1])));
63
+
64
+ interface Planned {
65
+ readonly plan?: unknown;
66
+ readonly refused?: string;
67
+ }
68
+
69
+ async function explain(client: DbClient, statement: string): Promise<Planned> {
70
+ const connection = isReservable(client) ? await client.reserve() : undefined;
71
+ const on = connection ?? client;
72
+ let prepared = false;
73
+ try {
74
+ const n = parameterCount(statement);
75
+ const args = n === 0 ? '' : `(${Array.from({ length: n }, () => 'null').join(', ')})`;
76
+ const rows = await withReadTransaction(
77
+ async (tx) => {
78
+ await tx.execute(raw(`prepare ${PREPARED} as ${statement}`));
79
+ prepared = true;
80
+ return tx.query<Record<string, unknown>>(
81
+ raw(`explain (format json) execute ${PREPARED}${args}`),
82
+ );
83
+ },
84
+ { settings: SETTINGS, client: on },
85
+ );
86
+ return { plan: rows[0]?.['QUERY PLAN'] };
87
+ } catch (error) {
88
+ return { refused: renderThrowable(error) };
89
+ } finally {
90
+ // A prepared statement outlives the transaction: left, the connection's next sweep collides.
91
+ if (prepared) await on.execute(raw(`deallocate ${PREPARED}`));
92
+ connection?.release();
93
+ }
94
+ }
95
+
96
+ interface ProofBuilder {
97
+ planned: number;
98
+ readonly findings: PlanFinding[];
99
+ readonly exempt: PlanExempt[];
100
+ readonly unplannable: PlanExempt[];
101
+ }
102
+
103
+ /** Plan every statement `sent` holds, once each, and hand each primitive the verdicts of its own. */
104
+ export async function sweepPlans(
105
+ client: DbClient,
106
+ sent: ReadonlyMap<string, SentStatement>,
107
+ entities: readonly SeedEntity[],
108
+ ): Promise<ReadonlyMap<string, PlanProof>> {
109
+ const tables = entities.filter((entity) => entity.$describe().view === undefined);
110
+ const byTable = new Map(tables.map((entity) => [entity.$table, entity]));
111
+ const judged: PlanTableColumns = new Map(
112
+ tables
113
+ .filter((entity) => entity.$describe().scan === undefined)
114
+ .map((entity) => [entity.$table, new Set(entity.$describe().columns.map((c) => c.column))]),
115
+ );
116
+ const version = await client.one<{ readonly v: string }>(
117
+ sql`select current_setting('server_version_num') as v`,
118
+ );
119
+ if (Number(version?.v ?? 0) < 120_000) throw new QueryPlanUnavailableError(version?.v ?? '?');
120
+
121
+ const proofs = new Map<string, ProofBuilder>();
122
+ const proofOf = (name: string): ProofBuilder => {
123
+ const found = proofs.get(name);
124
+ if (found !== undefined) return found;
125
+ const made: ProofBuilder = { planned: 0, findings: [], exempt: [], unplannable: [] };
126
+ proofs.set(name, made);
127
+ return made;
128
+ };
129
+ for (const [statement, { primitives, scan }] of sent) {
130
+ const table = FIRST_RELATION.exec(statement)?.[1];
131
+ const entity = table === undefined ? undefined : byTable.get(table);
132
+ if (entity === undefined) continue;
133
+ const declared = entity.$describe().scan;
134
+ const exemptBy = scan ?? declared;
135
+ const planned = exemptBy === undefined ? await explain(client, statement) : {};
136
+ for (const name of primitives) {
137
+ const proof = proofOf(name);
138
+ proof.planned += 1;
139
+ if (exemptBy !== undefined) proof.exempt.push({ statement, reason: exemptBy });
140
+ else if (planned.refused !== undefined) {
141
+ proof.unplannable.push({ statement, reason: planned.refused });
142
+ } else {
143
+ proof.findings.push(...planFindings(statement, judgePlan(planned.plan, judged), byTable));
144
+ }
145
+ }
146
+ }
147
+ return proofs;
148
+ }
package/src/preload.ts CHANGED
@@ -7,11 +7,13 @@
7
7
 
8
8
  // A green run prints the reporter and nothing else: the process logger's lines go to a sink.
9
9
  import './quiet-logs';
10
- import { installDeterminism } from './determinism';
10
+ import { captureDeterminism, installDeterminism, restoreCapturedDeterminism } from './determinism';
11
11
  import { registerFrameworkFixtures } from './framework-fixtures';
12
12
  import './matchers';
13
+ import { installTabA11yAudit } from './e2e-a11y-tabs';
13
14
  import { onFileBoundary } from './file-boundary';
14
15
  import { ISOLATED_ENV, releasePluginsAfterIsolatedFile } from './isolated-plugins';
16
+ import { installModuleMockBoundary } from './module-mock-boundary';
15
17
  import { installPerTestReset } from './per-test-reset';
16
18
  import { installRegistryLeakGuard } from './registry-leak-guard';
17
19
  import { sealNetwork } from './sealed-network';
@@ -32,6 +34,11 @@ installDeterminism({
32
34
 
33
35
  registerFrameworkFixtures();
34
36
 
37
+ // And the CLOCK, per FILE: the frozen instant is one process', so a file that advanced it and never
38
+ // put it back handed every later file on the worker a moved clock (`clock-file-boundary.test.ts`).
39
+ const installedClock = captureDeterminism();
40
+ onFileBoundary(() => restoreCapturedDeterminism(installedClock));
41
+
35
42
  // Inside a test process the machine's test slots are already accounted for — by the `x test` /
36
43
  // `x verify` that spawned it, or by nobody when `bun test` ran bare. An `x test` a test runs in
37
44
  // process (the framework's own suites do) must never lease from the real pool: it would wait on
@@ -46,6 +53,14 @@ installRegistryLeakGuard();
46
53
  // resumed the next test's waiting run (`per-test-reset.ts`).
47
54
  installPerTestReset();
48
55
 
56
+ // And every `mock.module`: Bun keeps one past `mock.restore()` and past the file, so the next file
57
+ // on a shared worker read the fake. Put back at the boundary (`module-mock-boundary.ts`).
58
+ installModuleMockBoundary();
59
+
60
+ // And every browser tab a suite drives itself is audited like an `e2eTest`'s page: its findings
61
+ // fail the test from this one `afterEach`, so no suite has a line to forget (`e2e-a11y-tabs.ts`).
62
+ installTabA11yAudit();
63
+
49
64
  // A shared worker only (an isolated file is a fresh registry anyway): the app's
50
65
  // `defineApi({ pathStyle })` evaluates once per worker, in whichever file first imports it, and
51
66
  // must not be refused because an EARLIER file derived action paths under the default style.
@@ -55,13 +70,18 @@ installPerTestReset();
55
70
  // was red whenever another file calling that action ran first. Each file starts with full buckets,
56
71
  // the store back to one process' memory, as a file run alone would have it.
57
72
  //
73
+ // And the METER store, for the same reason one period longer: a file that drew a seeded org's
74
+ // daily allowance left it drawn, so the next file's "the 101st draft is refused" test was red or
75
+ // green by file order (`meter-file-boundary.test.ts`).
76
+ //
58
77
  // And the LIFECYCLE, which is one process' too: a drain is terminal, so a file that drained the
59
78
  // process — or that emitted a real SIGINT at it, as a signal test does — left every later file on
60
79
  // the worker answering 503 X_DRAINING for a reason nothing in that file explains. Each file starts
61
80
  // from a live lifecycle, as a file run alone would have it.
62
81
  if (Bun.env[ISOLATED_ENV] !== '1') {
63
- const { forgetHandedOutActionPaths } = await import('@ultimat3/action');
82
+ const { forgetHandedOutActionPaths, resetMeterStore } = await import('@ultimat3/action');
64
83
  onFileBoundary(forgetHandedOutActionPaths);
84
+ onFileBoundary(resetMeterStore);
65
85
  const { resetRateLimitStore } = await import('@ultimat3/http');
66
86
  onFileBoundary(resetRateLimitStore);
67
87
  const { resetLifecycle } = await import('@ultimat3/core');
@@ -81,10 +101,10 @@ releasePluginsAfterIsolatedFile();
81
101
  // Every `.tsx` compiles with the framework's JSX factory from the first file on, in every test
82
102
  // process — an app root or any workspace below it. Bun's own compile of a `.tsx` is wrong on every
83
103
  // Bun (`@ultimat3/render`'s `module-loader.ts`): through 1.4.2 a cached `React.createElement`
84
- // (`React is not defined`, 18 notificado.co tests), from 1.4.3 a link-time `SyntaxError: Export
104
+ // (`React is not defined`, 18 of one adopter's tests), from 1.4.3 a link-time `SyntaxError: Export
85
105
  // named 'jsxDEV' not found` in any file that merely imports a component (issue #883). It was
86
106
  // installed only where `app.config.ts` sat beside the run, so a workspace package's own tests
87
- // needed a hand-written preload (castlefight.online, ai-maxxing).
107
+ // needed a hand-written preload (one adopter, and another).
88
108
  const { installRenderLoader } = await import('@ultimat3/render/server');
89
109
  installRenderLoader();
90
110
 
@@ -139,7 +139,7 @@ function restoreCatalogs(snapshot: readonly (readonly [Locale, Catalog])[]): voi
139
139
  * declared, plus what this file inherited. Since 22.7 a worker runs many files in one module
140
140
  * registry, so a module a file imports evaluates ONCE — a later file that resets the permission
141
141
  * set at module scope would otherwise hand every file after it a registry missing declarations no
142
- * cached module will ever make again (measured on notificado.co: "admin:read is not in the
142
+ * cached module will ever make again (measured on one adopter's app: "admin:read is not in the
143
143
  * permission set (8 known)"). Newer wins on a conflict, so a file's own redefinition still stands
144
144
  * for that file.
145
145
  */
@@ -0,0 +1,67 @@
1
+ // Single responsibility: a value for every field a failure bundle masked. The bundle never holds a
2
+ // secret, so a replay needs one that the schema accepts: the masked paths are filled from a seeded
3
+ // `arbitrary()` value of the same schema (input), or from the column's own candidates (a row).
4
+
5
+ import { REDACTED, uuidV7 } from '@ultimat3/core';
6
+ import type { EntityCore } from '@ultimat3/entity';
7
+ import { arbitrary } from '@ultimat3/schema/sample';
8
+ import { candidatesFor, type SeedColumn } from './seed-graph-values';
9
+
10
+ /** The seed every stand-in draws from: the same bundle, the same replay. */
11
+ const STAND_IN_SEED = 0x5eed;
12
+
13
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
14
+ typeof value === 'object' && value !== null && !Array.isArray(value);
15
+
16
+ /** Every `REDACTED` leaf of `value` replaced by the leaf at the same place in `sample`. */
17
+ function fill(value: unknown, sample: unknown): unknown {
18
+ if (value === REDACTED) return sample;
19
+ if (Array.isArray(value)) {
20
+ const items = Array.isArray(sample) ? sample : [];
21
+ return value.map((entry, index) => fill(entry, items[index] ?? items[0]));
22
+ }
23
+ if (!isRecord(value)) return value;
24
+ const out: Record<string, unknown> = {};
25
+ for (const [key, entry] of Object.entries(value)) {
26
+ Object.defineProperty(out, key, {
27
+ value: fill(entry, isRecord(sample) ? sample[key] : undefined),
28
+ enumerable: true,
29
+ writable: true,
30
+ configurable: true,
31
+ });
32
+ }
33
+ return out;
34
+ }
35
+
36
+ /** The input with every masked field replaced by a value `schema` accepts. */
37
+ export function inputStandIn(schema: unknown, input: unknown, masked: readonly string[]): unknown {
38
+ if (masked.length === 0) return input;
39
+ // `arbitrary` reads the schema's IR; a schema without one yields `{}`, and the replay says so.
40
+ const sample = arbitrary(schema as Parameters<typeof arbitrary>[0], {
41
+ seed: STAND_IN_SEED,
42
+ run: 0,
43
+ });
44
+ return fill(input, sample);
45
+ }
46
+
47
+ /** The row with every masked column replaced by the first value that column stores. */
48
+ export function rowStandIn(
49
+ entity: EntityCore<unknown>,
50
+ row: Readonly<Record<string, unknown>>,
51
+ masked: readonly string[],
52
+ ): Record<string, unknown> {
53
+ const out: Record<string, unknown> = { ...row };
54
+ const columns = entity.$columns as Readonly<Record<string, SeedColumn>>;
55
+ for (const [position, property] of masked.entries()) {
56
+ const column = columns[property];
57
+ if (column === undefined) continue;
58
+ const [first] = candidatesFor(property, column, {
59
+ ordinal: 1,
60
+ canary: '5eed',
61
+ position,
62
+ uuid: () => uuidV7(),
63
+ });
64
+ out[property] = first;
65
+ }
66
+ return out;
67
+ }
package/src/repro.ts ADDED
@@ -0,0 +1,158 @@
1
+ // `reproduce(bundle)` and `reproJob(runJobs, failure)` — what a test `x repro --write` generated
2
+ // calls. Request: seed the rows the failing call read, freeze the clock at its instant, invoke as
3
+ // its actor with its input (masked fields stood in for). Job: the stored input, the completed steps
4
+ // replayed from their output, one drain. Each ANSWERS the outcome; the test asserts on it.
5
+
6
+ import type { Actor, FailureBundle, FailureRead } from '@ultimat3/core';
7
+ import {
8
+ agentActor,
9
+ anonymousActor,
10
+ ctxOf,
11
+ frozenClock,
12
+ isUltimateError,
13
+ renderThrowable,
14
+ serviceActor,
15
+ userActor,
16
+ } from '@ultimat3/core';
17
+ import type { Driver, EntityCore, RowWrite } from '@ultimat3/entity';
18
+ import type { JobHandle } from '@ultimat3/jobs';
19
+ import type { RunJobs } from './fixture-jobs';
20
+
21
+ /** What a replay ended in. `failure` is `CODE: cause` (or the thrown value rendered), else `null`. */
22
+ export interface ReproOutcome {
23
+ readonly code: string | null;
24
+ readonly failure: string | null;
25
+ /** A job's: the step whose record ended `failed`, `null` for a request or a body-level throw. */
26
+ readonly step: string | null;
27
+ }
28
+
29
+ /** A dead job, as `x repro <jobId>` read it: everything a replay needs and nothing it does not. */
30
+ export interface JobFailure {
31
+ readonly jobId: string;
32
+ readonly name: string;
33
+ readonly input: unknown;
34
+ readonly masked: readonly string[];
35
+ /** Completed steps, in order, with the output each returned: replayed, never re-executed. */
36
+ readonly steps: readonly { readonly name: string; readonly output: unknown }[];
37
+ readonly code: string | null;
38
+ readonly step: string | null;
39
+ }
40
+
41
+ export interface ReproduceOptions {
42
+ /** The app's own driver (`@<app>/db`'s `driver`): captured rows go where the code reads. */
43
+ readonly driver?: Driver | undefined;
44
+ }
45
+
46
+ const outcomeOf = (error: unknown): ReproOutcome => ({
47
+ code: isUltimateError(error) ? error.code : 'X_INTERNAL',
48
+ failure: isUltimateError(error) ? `${error.code}: ${error.cause}` : renderThrowable(error),
49
+ step: null,
50
+ });
51
+
52
+ const PASSED: ReproOutcome = { code: null, failure: null, step: null };
53
+
54
+ function actorFrom(captured: FailureBundle['actor']): Actor {
55
+ if (captured === null || captured.kind === 'anonymous') return anonymousActor();
56
+ const init = {
57
+ id: captured.id,
58
+ orgId: captured.orgId ?? undefined,
59
+ roles: captured.roles,
60
+ scopes: captured.scopes,
61
+ permissions: captured.permissions,
62
+ };
63
+ if (captured.kind === 'service') return serviceActor(init);
64
+ if (captured.kind === 'agent') return agentActor(init);
65
+ return userActor(init);
66
+ }
67
+
68
+ /** Each captured row written through its entity's repository, replay-safe (`onMatch: 'nothing'`). */
69
+ async function seedReads(
70
+ reads: readonly FailureRead[],
71
+ driver: Driver,
72
+ ): Promise<readonly string[]> {
73
+ const { getEntity } = await import('@ultimat3/entity');
74
+ const { rowStandIn } = await import('./repro-stand-in');
75
+ const unknown: string[] = [];
76
+ for (const read of reads) {
77
+ const entity = getEntity(read.entity)?.core as EntityCore<unknown> | undefined;
78
+ if (entity === undefined) {
79
+ unknown.push(read.entity);
80
+ continue;
81
+ }
82
+ // `$parse` answers the entity's own row; the registry erased its type, the write restores it.
83
+ const row = entity.$parse(rowStandIn(entity, read.row, read.masked)) as RowWrite<unknown>;
84
+ await driver.repo(entity).upsertAll([row], {
85
+ onConflict: [...entity.$primaryKey] as never,
86
+ onMatch: 'nothing',
87
+ });
88
+ }
89
+ return unknown;
90
+ }
91
+
92
+ /**
93
+ * Replay one captured request. A bundle naming an action or query this process has not registered
94
+ * answers that as its failure rather than throwing: the written test is red with the reason.
95
+ */
96
+ export async function reproduce(
97
+ bundle: FailureBundle,
98
+ options: ReproduceOptions = {},
99
+ ): Promise<ReproOutcome> {
100
+ if (options.driver !== undefined) {
101
+ options.driver.reset?.();
102
+ await seedReads(bundle.reads, options.driver);
103
+ }
104
+ const ctx = ctxOf({ actor: actorFrom(bundle.actor), clock: frozenClock(bundle.at) });
105
+ const { inputStandIn } = await import('./repro-stand-in');
106
+ try {
107
+ if (bundle.primitive === 'action') {
108
+ const { invoke, listActions } = await import('@ultimat3/action');
109
+ const target = listActions().find((candidate) => candidate.name === bundle.name);
110
+ if (target === undefined) return unregistered('action', bundle.name);
111
+ await invoke(target, inputStandIn(target.input, bundle.input, bundle.masked), { ctx });
112
+ } else {
113
+ const { listQueries, runQuery } = await import('@ultimat3/query');
114
+ const target = listQueries().find((candidate) => candidate.name === bundle.name);
115
+ if (target === undefined) return unregistered('query', bundle.name);
116
+ await runQuery(target, inputStandIn(target.input, bundle.input, bundle.masked), { ctx });
117
+ }
118
+ return PASSED;
119
+ } catch (error) {
120
+ return outcomeOf(error);
121
+ }
122
+ }
123
+
124
+ const unregistered = (kind: string, name: string): ReproOutcome => ({
125
+ code: null,
126
+ step: null,
127
+ failure: `no ${kind} named "${name}" is registered in this test process — import the module that declares it from the written test`,
128
+ });
129
+
130
+ /** Replay one dead job through the `runJobs` fixture: its input, its completed steps, one drain. */
131
+ export async function reproJob(runJobs: RunJobs, failure: JobFailure): Promise<ReproOutcome> {
132
+ const jobs = await import('@ultimat3/jobs');
133
+ const handle = jobs.registeredJobs().find((candidate) => candidate.name === failure.name);
134
+ if (handle === undefined) return unregistered('job', failure.name);
135
+ const { inputStandIn } = await import('./repro-stand-in');
136
+ const input = inputStandIn(handle.input, failure.input, failure.masked);
137
+ const { runId } = await runJobs.enqueue(handle as JobHandle<unknown>, input);
138
+ const store = jobs.jobDriver()?.steps;
139
+ for (const step of failure.steps) {
140
+ await store?.put({
141
+ runId,
142
+ name: step.name,
143
+ status: 'completed',
144
+ output: step.output,
145
+ startedAt: 0,
146
+ completedAt: 0,
147
+ attempts: 1,
148
+ });
149
+ }
150
+ const trace = await runJobs.drain();
151
+ const run = trace.executions.find((execution) => execution.error !== undefined);
152
+ if (run?.error === undefined) return PASSED;
153
+ return {
154
+ code: /X_[A-Z0-9_]+/.exec(run.error)?.[0] ?? 'X_INTERNAL',
155
+ failure: run.error,
156
+ step: run.steps.find((step) => step.status === 'failed')?.name ?? null,
157
+ };
158
+ }
@@ -0,0 +1,64 @@
1
+ // One `fetch` call as `requestLog()` records it. A probe of the request is read, never the request:
2
+ // the handler and the real dial each still get a body of their own.
3
+
4
+ /** One dial, as attempted. A socket dial has no headers and no body. Unredacted, process-local. */
5
+ export interface LoggedRequest {
6
+ readonly method: string;
7
+ readonly url: string;
8
+ /** Lowercase names. */
9
+ readonly headers: Readonly<Record<string, string>>;
10
+ /** Decoded as UTF-8. `null`: no body, an unreadable one, or a stream a real server is reading. */
11
+ readonly body: string | null;
12
+ }
13
+
14
+ /** A stream read here would be drained ahead of its consumer, which is its backpressure gone. */
15
+ export const hasStreamBody = (init: RequestInit | undefined): boolean => {
16
+ const body = init?.body ?? null;
17
+ return !(
18
+ body === null ||
19
+ typeof body === 'string' ||
20
+ body instanceof ArrayBuffer ||
21
+ ArrayBuffer.isView(body) ||
22
+ body instanceof Blob ||
23
+ body instanceof FormData ||
24
+ body instanceof URLSearchParams
25
+ );
26
+ };
27
+
28
+ /**
29
+ * A body with no end the log can wait for: a stream, or a Request's own, which may be one. Its
30
+ * capture runs beside the call, so a mock that answers 413 at once is not held to the stream's end.
31
+ */
32
+ export const isOpenEnded = (input: RequestInfo | URL, init: RequestInit | undefined): boolean =>
33
+ hasStreamBody(init) || (init?.body == null && input instanceof Request && input.body !== null);
34
+
35
+ /**
36
+ * The call's own request, for reading only: a stream body stays out of it, unlocked. `undefined`
37
+ * when no Request can hold the call, so the refusal the test is owed stays the seal's or the real
38
+ * dial's rather than this module's `TypeError`.
39
+ */
40
+ export const requestProbe = (
41
+ input: RequestInfo | URL,
42
+ init: RequestInit | undefined,
43
+ ): Request | undefined => {
44
+ try {
45
+ if (input instanceof Request && init === undefined) return input.clone();
46
+ const source = input instanceof Request ? input.clone() : input;
47
+ return new Request(source, hasStreamBody(init) ? { ...init, body: null } : init);
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ };
52
+
53
+ export const headersOf = (probe: Request | undefined): Readonly<Record<string, string>> =>
54
+ probe === undefined ? {} : Object.fromEntries(probe.headers);
55
+
56
+ /** Never rejects: a body that cannot be read is the real call's failure to report, not the log's. */
57
+ export const readBody = async (request: Request | undefined): Promise<string | null> => {
58
+ if (request === undefined) return null;
59
+ try {
60
+ return request.body === null ? null : await request.text();
61
+ } catch {
62
+ return null;
63
+ }
64
+ };
@@ -3,7 +3,7 @@
3
3
  // Booting an embedded Postgres (PGlite) from a migrated template costs 2.4-7 s of CPU on a
4
4
  // 12-core box (measured, 64 MB template: 2.7-7.2 s cold, 2.4-2.9 s with the WASM module and fs
5
5
  // bundle already compiled — the untar and the postgres start are the cost, not the compile). A
6
- // suite that boots one per file pays that in every file: notificado.co's unit tier has 236 such
6
+ // suite that boots one per file pays that in every file: one adopter's unit tier has 236 such
7
7
  // files, ~700 s of CPU, which is most of its wall time at any width.
8
8
  //
9
9
  // Since 22.7 a worker runs many files in one process (no `--isolate`), so the database can outlive