@ultimat3/testing 30.5.0 → 30.6.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
@@ -46,10 +46,10 @@ import is `registry-isolation.ts`, own entry point, off the barrel. Full rule: `
46
46
  | Injection | `SqlRunner` and `connect` are parameters, so unit tests need no server |
47
47
  | Fixtures | the preload registers the framework bag — an app registers only what it alone knows (`seed`, `actorFor`) |
48
48
  | Every `e2eTest` is audited | `e2e-a11y.ts` (rules), `e2e-a11y-run.ts` (per-test run, `allow`, dedupe), `ax-node.ts` (the one AX reader); the full tree is read only in `cdp-e2e-page.ts` (`FULL_AX_TREE`, a convention). No switch; `allow` needs a non-blank `because`. And every raw tab: `e2e-a11y-tabs.ts`, checked after each `goto`/`reload` and at the test's end from the preload's `afterEach`; exceptions are `allowA11y([{ role, selector, because }])` |
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) |
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). A job runs ALONE (`exercise-job-run.ts`): a fresh queue, one `tick()`, what it queued left on the caller's queue unrun — never a `drain()` |
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`), its place `crashEffectPlace` (`inside` a step via jobs' `currentStep()`, or `after` one). 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). Its world is `alone`: only the job's own row is claimed, a child is an `enqueue:` effect, never run. 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
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "30.5.0",
3
+ "version": "30.6.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.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"
49
+ "@ultimat3/action": "30.6.0",
50
+ "@ultimat3/cache": "30.6.0",
51
+ "@ultimat3/core": "30.6.0",
52
+ "@ultimat3/db": "30.6.0",
53
+ "@ultimat3/entity": "30.6.0",
54
+ "@ultimat3/http": "30.6.0",
55
+ "@ultimat3/i18n": "30.6.0",
56
+ "@ultimat3/jobs": "30.6.0",
57
+ "@ultimat3/mail": "30.6.0",
58
+ "@ultimat3/policy": "30.6.0",
59
+ "@ultimat3/pwa": "30.6.0",
60
+ "@ultimat3/query": "30.6.0",
61
+ "@ultimat3/realtime": "30.6.0",
62
+ "@ultimat3/render": "30.6.0",
63
+ "@ultimat3/schema": "30.6.0",
64
+ "@ultimat3/time": "30.6.0"
65
65
  }
66
66
  }
@@ -0,0 +1,68 @@
1
+ // A job primitive's one execution, as the exercise pass runs it: ALONE, on a queue that holds nothing
2
+ // else, for one worker pass. What it enqueues is handed back to the caller's queue unrun, exactly as
3
+ // an action's enqueues are — counted there as `enqueue` effects, searched by the tenant proof, and
4
+ // never executed inside the subject's room, so a child's statements are never the subject's.
5
+
6
+ import type { Actor } from '@ultimat3/core';
7
+ import { assert } from '@ultimat3/core';
8
+ import type { AnyJobHandle, JobExecution, JobRecord } from '@ultimat3/jobs';
9
+ import { jobCursor, jobDriver, MAX_JOB_PAGE, registeredJobs } from '@ultimat3/jobs';
10
+ import type { RunJobs } from './fixture-jobs';
11
+ import { testJobs } from './fixture-jobs';
12
+
13
+ const QUEUED: ReadonlySet<string> = new Set(['ready', 'delayed']);
14
+
15
+ /** Every row the AMBIENT driver holds, page by page: what a fixture's queue has, finished or not. */
16
+ export async function ambientJobRows(): Promise<readonly JobRecord[]> {
17
+ const introspect = jobDriver()?.introspect;
18
+ if (introspect === undefined) return [];
19
+ const records: JobRecord[] = [];
20
+ for (let after: string | undefined; ; ) {
21
+ const page = await introspect.list({
22
+ limit: MAX_JOB_PAGE,
23
+ ...(after === undefined ? {} : { after }),
24
+ });
25
+ records.push(...page);
26
+ const last = page[page.length - 1];
27
+ if (last === undefined || page.length < MAX_JOB_PAGE) break;
28
+ after = jobCursor(last);
29
+ }
30
+ return records;
31
+ }
32
+
33
+ /**
34
+ * `handle` run once with `input` in `tenant`, as `actor`'s worker: enqueued on a fresh queue and
35
+ * claimed by ONE pass, so the only run is its own — never a job `into` already held (an earlier
36
+ * call's, an earlier primitive's) and never one it queued. Those it queued land on `into`, unrun.
37
+ */
38
+ export async function runJobAlone(
39
+ handle: AnyJobHandle,
40
+ input: unknown,
41
+ at: { readonly actor: Actor | null; readonly tenant: string },
42
+ into: RunJobs,
43
+ ): Promise<JobExecution | undefined> {
44
+ let own: JobExecution | undefined;
45
+ let queued: readonly JobRecord[];
46
+ {
47
+ await using alone = await testJobs();
48
+ const row = await alone.enqueue(handle, input, { tenantId: at.tenant });
49
+ const trace = await alone.tick(at.actor === null ? {} : { actor: at.actor });
50
+ own = trace.executions.find((run) => run.jobId === row.id);
51
+ queued = (await ambientJobRows()).filter(
52
+ (record) => record.id !== row.id && QUEUED.has(record.state),
53
+ );
54
+ }
55
+ const handles = new Map(registeredJobs().map((one) => [one.name, one]));
56
+ for (const record of queued) {
57
+ const child = handles.get(record.name);
58
+ assert(
59
+ child !== undefined,
60
+ `job "${handle.name}" queued job "${record.name}" but nothing registered it`,
61
+ `import the module that declares job("${record.name}") from the app's api module — the registry is populated by the import, not by the queue`,
62
+ );
63
+ await into.enqueue(child, record.input, {
64
+ ...(record.tenantId === undefined ? {} : { tenantId: record.tenantId }),
65
+ });
66
+ }
67
+ return own;
68
+ }
@@ -1,6 +1,7 @@
1
1
  // The primitives an exercise pass drives, each as one call: every registered action, query and job,
2
2
  // loaded on demand. A job is enqueued in the tenant it is called for, its input SEATED there first
3
- // (`exercise-job-tenant.ts`): a drawn org is a random one, and the enqueue refuses it.
3
+ // (`exercise-job-tenant.ts`): a drawn org is a random one, and the enqueue refuses it. It runs
4
+ // alone (`exercise-job-run.ts`): what it queues is left queued, as an action's is.
4
5
 
5
6
  import type { Actor } from '@ultimat3/core';
6
7
  import { renderCauseValue, renderFixShellArg, UltimateError } from '@ultimat3/core';
@@ -8,6 +9,7 @@ import type { AnyJobHandle, JobExecution } from '@ultimat3/jobs';
8
9
  import { registeredJobs } from '@ultimat3/jobs';
9
10
  import type { Policy } from '@ultimat3/policy';
10
11
  import type { StandardSchemaV1 } from '@ultimat3/schema';
12
+ import { runJobAlone } from './exercise-job-run';
11
13
  import type { JobTenantBinding } from './exercise-job-tenant';
12
14
  import { bindJobTenant } from './exercise-job-tenant';
13
15
  import { withExerciseRequest } from './exercise-request';
@@ -84,9 +86,9 @@ const jobSubject = (handle: AnyJobHandle): Subject => ({
84
86
  handle,
85
87
  seat: (input, tenant) => bindJobTenant(handle, input, tenant),
86
88
  call: async (input, actor, tenant, jobs) => {
87
- const queued = await jobs.enqueue(handle, input, { tenantId: tenant });
88
- const trace = await jobs.drain(actor === null ? {} : { actor });
89
- const own = trace.executions.find((run) => run.jobId === queued.id);
89
+ // Its own execution and nothing else: a drain of `jobs` ran what it queued (one child per row,
90
+ // which the scale sweep read as the job's loop) and whatever an earlier call had left there.
91
+ const own = await runJobAlone(handle, input, { actor, tenant }, jobs);
90
92
  // The worker's failure is on the row, never a throw: the attempt's error and its code.
91
93
  // Suspended is a body that ran and parked (`step.sleep`, `waitForEvent`): reached.
92
94
  return own !== undefined && ANSWERED.has(own.outcome)
package/src/exercise.ts CHANGED
@@ -8,7 +8,6 @@
8
8
 
9
9
  import type { Actor, ExerciseAttempt, ExerciseVerdict } from '@ultimat3/core';
10
10
  import { isUltimateError, renderThrowable, runExercise } from '@ultimat3/core';
11
- import type { JobExecution, JobRecord } from '@ultimat3/jobs';
12
11
  import { registeredJobs } from '@ultimat3/jobs';
13
12
  import type { ExerciseOverride, ExerciseOverrides } from './define-exercise';
14
13
  import { assertOverridesKnown, unknownDeclaration } from './define-exercise';
@@ -25,6 +24,7 @@ import {
25
24
  namesActorEntity,
26
25
  seededIds,
27
26
  } from './exercise-input';
27
+ import { ambientJobRows } from './exercise-job-run';
28
28
  import type { OverrideInput } from './exercise-override-input';
29
29
  import { overrideInput } from './exercise-override-input';
30
30
  import type {
@@ -76,13 +76,15 @@ const codeOf = (error: unknown): string | undefined => {
76
76
  const reasonOf = (error: unknown): string =>
77
77
  isUltimateError(error) ? error.cause : typeof error === 'string' ? error : renderThrowable(error);
78
78
 
79
- /** Jobs queued or run by the primitive, the primitive's own job excepted. */
80
- async function enqueued(subject: Subject, jobs: RunJobs, runs: readonly JobExecution[]) {
79
+ /**
80
+ * Jobs the primitive queued, its own job excepted. Never run: a job's children are left queued as
81
+ * an action's are (`exercise-job-run.ts`), so their statements are theirs, never this record's.
82
+ */
83
+ async function enqueued(subject: Subject, jobs: RunJobs) {
81
84
  const effects: ExerciseEffect[] = [];
82
85
  for (const handle of registeredJobs()) {
83
86
  if (subject.kind === 'job' && handle.name === subject.name) continue;
84
- const ran = new Set(runs.filter((run) => run.job === handle.name).map((r) => r.jobId)).size;
85
- const count = ran + (await jobs.depth(handle));
87
+ const count = await jobs.depth(handle);
86
88
  if (count > 0) effects.push({ kind: 'enqueue', job: handle.name, count });
87
89
  }
88
90
  return effects;
@@ -93,21 +95,7 @@ async function enqueued(subject: Subject, jobs: RunJobs, runs: readonly JobExecu
93
95
  * input is what B was handed. Read off the leg's own queue, page by page.
94
96
  */
95
97
  async function queuedPayloads(subject: Subject): Promise<readonly EffectPayload[]> {
96
- const jobs = await import('@ultimat3/jobs');
97
- const introspect = jobs.jobDriver()?.introspect;
98
- if (introspect === undefined) return [];
99
- const records: JobRecord[] = [];
100
- for (let after: string | undefined; ; ) {
101
- const page = await introspect.list({
102
- limit: jobs.MAX_JOB_PAGE,
103
- ...(after === undefined ? {} : { after }),
104
- });
105
- records.push(...page);
106
- const last = page[page.length - 1];
107
- if (last === undefined || page.length < jobs.MAX_JOB_PAGE) break;
108
- after = jobs.jobCursor(last);
109
- }
110
- return records
98
+ return (await ambientJobRows())
111
99
  .filter((record) => !(subject.kind === 'job' && record.name === subject.name))
112
100
  .map((record, i) => ({
113
101
  path: `effect.enqueue[${i}]`,
@@ -174,7 +162,6 @@ async function control(
174
162
  let lastError: unknown;
175
163
  let lastInput: unknown;
176
164
  let output: unknown;
177
- let executions: readonly JobExecution[] = [];
178
165
  room.begin(name);
179
166
  const started = performance.now();
180
167
  const verdict = await runExercise({
@@ -209,9 +196,8 @@ async function control(
209
196
  statusOf: (await import('@ultimat3/http')).statusFor,
210
197
  });
211
198
  const ms = Math.round(performance.now() - started);
212
- if (kind === 'job') executions = (await jobs.drain()).executions;
213
199
  const observed = room.end();
214
- const effects = [...observed.effects, ...(await enqueued(subject, jobs, executions))];
200
+ const effects = [...observed.effects, ...(await enqueued(subject, jobs))];
215
201
  const verdictOf = judged(subject, { verdict, answered, lastError, lastInput }, observed);
216
202
  // A 5xx's cause, statement and fields: the code alone sent the author to reproduce it by hand.
217
203
  const defect =
@@ -69,6 +69,11 @@ export interface RunJobs extends AsyncDisposable {
69
69
  enqueue<I>(handle: JobHandle<I>, input: I, options?: JobEnqueueAs): Promise<EnqueueResult>;
70
70
  /** Claim and execute everything due at the current instant, until nothing is. */
71
71
  drain(options?: JobDrainAs): Promise<JobRunTrace>;
72
+ /**
73
+ * ONE worker pass: what is due now runs, and what those runs enqueue waits for the next pass —
74
+ * "it queued one child per row" asserted without running a child.
75
+ */
76
+ tick(options?: JobDrainAs): Promise<JobRunTrace>;
72
77
  /** Live jobs — ready, delayed, running or suspended — optionally for one job only. */
73
78
  depth(handle?: AnyJobHandle): Promise<number>;
74
79
  /** Live jobs claimable right now. `clock.advance()` is what turns delayed into due. */
@@ -194,14 +199,20 @@ export async function testJobs(): Promise<RunJobs> {
194
199
  /** Every execution this fixture has driven, because the trace is cumulative. */
195
200
  const history: JobExecution[] = [];
196
201
 
202
+ const ctxFor = (options: JobDrainAs): Ctx =>
203
+ options.actor === undefined ? anonymousWorker : ctxOf({ role: 'worker', actor: options.actor });
204
+ const traced = (): JobRunTrace => ({ executions: [...history], steps: tallyOf(history) });
205
+
206
+ const tick = async (options: JobDrainAs = {}): Promise<JobRunTrace> => {
207
+ history.push(...(await round(ctxFor(options))));
208
+ return traced();
209
+ };
210
+
197
211
  const drain = async (options: JobDrainAs = {}): Promise<JobRunTrace> => {
198
- const ctx =
199
- options.actor === undefined
200
- ? anonymousWorker
201
- : ctxOf({ role: 'worker', actor: options.actor });
212
+ const ctx = ctxFor(options);
202
213
  for (let rounds = 0; rounds < MAX_ROUNDS; rounds += 1) {
203
214
  const batch = await round(ctx);
204
- if (batch.length === 0) return { executions: [...history], steps: tallyOf(history) };
215
+ if (batch.length === 0) return traced();
205
216
  history.push(...batch);
206
217
  }
207
218
  assert(
@@ -239,6 +250,7 @@ export async function testJobs(): Promise<RunJobs> {
239
250
  return Object.assign(enqueueThenDrain, {
240
251
  enqueue,
241
252
  drain,
253
+ tick,
242
254
  depth: async (handle?: AnyJobHandle) => (await live(handle?.name)).length,
243
255
  due: async () =>
244
256
  (await live()).filter(
@@ -134,7 +134,10 @@ async function runOnce<I>(
134
134
  if (op.step.startsWith(COMPENSATE_PREFIX)) compensated.push(op.step);
135
135
  else if (op.step !== UNWINDING_STEP) forward.add(op.step);
136
136
  };
137
+ // Alone: a child the job queues is its enqueue effect, never run — the clean run that ran it
138
+ // first wrote its rows, and every killed run after found them there (0× against 6×).
137
139
  await using world = await openSimWorld({
140
+ alone: true,
138
141
  seed: finiteCount('crashProof', 'seed', options.seed ?? 1),
139
142
  decide,
140
143
  landed,
@@ -31,6 +31,8 @@ export const SIM_VISIBILITY_MS = 30_000;
31
31
  const RENEW_MS = 1;
32
32
  const CLAIM_LIMIT = 64;
33
33
  const LIVE: ReadonlySet<string> = new Set(['ready', 'delayed', 'running', 'suspended']);
34
+ /** Where an `alone` world files the harness's row: no body enqueues here, so no worker claims theirs. */
35
+ const HARNESS_QUEUE = 'x-sim-harness';
34
36
 
35
37
  export interface SimWorldOptions {
36
38
  /** Seeds `Math.random` for the run: a body's jitter and draws are the seed's. */
@@ -40,6 +42,11 @@ export interface SimWorldOptions {
40
42
  decide(op: SimOp): SimDecision | Promise<SimDecision>;
41
43
  /** Told every landed call, after the world has read it. */
42
44
  landed?(op: SimOp, answer: unknown): void;
45
+ /**
46
+ * Only the row the harness filed is ever claimed: what a body enqueues lands, dedupes and is an
47
+ * `enqueue:` effect, and never runs — its effects are its own job's, not this run's.
48
+ */
49
+ readonly alone?: boolean;
43
50
  }
44
51
 
45
52
  export interface SimWorld extends AsyncDisposable {
@@ -115,9 +122,11 @@ export async function openSimWorld(options: SimWorldOptions): Promise<SimWorld>
115
122
  const anonymous = ctxOf({ role: 'worker' });
116
123
  const context = options.context ?? (() => anonymous);
117
124
 
118
- const queues = (): readonly string[] => [
119
- ...new Set([jobs.DEFAULT_QUEUE, ...jobs.registeredJobs().map((handle) => handle.queue)]),
120
- ];
125
+ const queues = (): readonly string[] =>
126
+ options.alone === true
127
+ ? [HARNESS_QUEUE]
128
+ : [...new Set([jobs.DEFAULT_QUEUE, ...jobs.registeredJobs().map((handle) => handle.queue)])];
129
+ const claimable = new Set(queues());
121
130
 
122
131
  const rows = async (): Promise<readonly JobRecord[]> => {
123
132
  const all: JobRecord[] = [];
@@ -146,7 +155,7 @@ export async function openSimWorld(options: SimWorldOptions): Promise<SimWorld>
146
155
  enqueue: async (handle, input) => {
147
156
  const result = await base.enqueue({
148
157
  name: handle.name,
149
- queue: handle.queue,
158
+ queue: options.alone === true ? HARNESS_QUEUE : handle.queue,
150
159
  input,
151
160
  idempotencyKey: handle.idempotencyKeyFor(input),
152
161
  maxAttempts: jobs.enqueuedAttempts(handle.retry, handle.compensates),
@@ -179,7 +188,9 @@ export async function openSimWorld(options: SimWorldOptions): Promise<SimWorld>
179
188
  rows,
180
189
  nextDue: async () => {
181
190
  const due = (await rows())
182
- .filter((row) => LIVE.has(row.state))
191
+ .filter(
192
+ (row) => LIVE.has(row.state) && (options.alone !== true || claimable.has(row.queue)),
193
+ )
183
194
  .map((row) => (row.state === 'running' ? (row.visibleAt ?? row.runAt) : row.runAt));
184
195
  return due.length === 0 ? undefined : Math.min(...due);
185
196
  },