@ultimat3/testing 29.3.2 → 30.0.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 +82 -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 +448 -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 +68 -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,191 @@
1
+ // A fake OpenID provider, served through `mockFetch` with no socket: a discovery document, a JWKS
2
+ // from a key generated per instance, `/authorize` auto-approving as a chosen user, `/token` (PKCE
3
+ // checked) and `/userinfo`. `tamper()` mints the failures that matter — wrong audience, expired,
4
+ // wrong key, no `email_verified` — so a sign-in test proves the refusals, not just the happy path.
5
+
6
+ import type { Clock } from '@ultimat3/core';
7
+ import { assert, systemClock, timingSafeEqual } from '@ultimat3/core';
8
+ import { mockFetch } from './sealed-network';
9
+
10
+ export interface FakeIdpUser {
11
+ readonly sub: string;
12
+ readonly email: string;
13
+ /** Defaults to `true`. `tamper({ noEmailVerified })` drops the claim entirely. */
14
+ readonly emailVerified?: boolean | undefined;
15
+ readonly name?: string | undefined;
16
+ }
17
+
18
+ export interface FakeIdpTamper {
19
+ /** `aud` names another client. */
20
+ readonly wrongAudience?: boolean | undefined;
21
+ /** `exp` an hour ago. */
22
+ readonly expired?: boolean | undefined;
23
+ /** Signed by a key the JWKS does not publish, under the same `kid`. */
24
+ readonly wrongKey?: boolean | undefined;
25
+ /** No `email_verified` claim, in the id token or `/userinfo`. */
26
+ readonly noEmailVerified?: boolean | undefined;
27
+ }
28
+
29
+ export interface FakeIdentityProviderInput {
30
+ /** Defaults to `https://idp.test`. */
31
+ readonly issuer?: string | undefined;
32
+ readonly users: readonly FakeIdpUser[];
33
+ readonly clock?: Clock | undefined;
34
+ /** Defaults to `fake-client`. */
35
+ readonly clientId?: string | undefined;
36
+ }
37
+
38
+ export interface FakeIdentityProvider {
39
+ readonly issuer: string;
40
+ readonly clientId: string;
41
+ readonly clientSecret: string;
42
+ readonly jwksUri: string;
43
+ /** Who `/authorize` approves as next. Defaults to the first user. */
44
+ signInAs(sub: string): void;
45
+ /** Applies to every token minted after it; `tamper({})` restores honesty. */
46
+ tamper(flags: FakeIdpTamper): void;
47
+ /** An id token minted directly, for a test that skips the redirect leg. */
48
+ idToken(input: { readonly sub: string; readonly nonce?: string | undefined }): Promise<string>;
49
+ }
50
+
51
+ type KeyPair = CryptoKeyPair;
52
+ const KID = 'fake-idp-key';
53
+ const encoder = new TextEncoder();
54
+
55
+ const b64url = (bytes: Uint8Array): string => Buffer.from(bytes).toString('base64url');
56
+ const segment = (value: unknown): string => b64url(encoder.encode(JSON.stringify(value)));
57
+ const random = (bytes = 24): string => b64url(crypto.getRandomValues(new Uint8Array(bytes)));
58
+ const es256 = (): Promise<KeyPair> =>
59
+ crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
60
+
61
+ interface PendingCode {
62
+ readonly sub: string;
63
+ readonly nonce: string | null;
64
+ readonly challenge: string | null;
65
+ }
66
+
67
+ export async function fakeIdentityProvider(
68
+ input: FakeIdentityProviderInput,
69
+ ): Promise<FakeIdentityProvider> {
70
+ assert(
71
+ input.users.length > 0,
72
+ 'fakeIdentityProvider was given no users',
73
+ 'pass users: [{ sub, email }]',
74
+ );
75
+ const issuer = (input.issuer ?? 'https://idp.test').replace(/\/+$/, '');
76
+ const clientId = input.clientId ?? 'fake-client';
77
+ const clientSecret = random();
78
+ const clock = input.clock ?? systemClock;
79
+ const [published, rogue] = await Promise.all([es256(), es256()]);
80
+ const jwk = await crypto.subtle.exportKey('jwk', published.publicKey);
81
+ const users = new Map(input.users.map((user) => [user.sub, user]));
82
+ const codes = new Map<string, PendingCode>();
83
+ const accessTokens = new Map<string, string>();
84
+ let chosen = input.users[0]?.sub ?? '';
85
+ let flags: FakeIdpTamper = {};
86
+
87
+ const claimsOf = (user: FakeIdpUser) => ({
88
+ email: user.email,
89
+ ...(flags.noEmailVerified === true ? {} : { email_verified: user.emailVerified ?? true }),
90
+ ...(user.name === undefined ? {} : { name: user.name }),
91
+ });
92
+
93
+ async function idToken(token: { sub: string; nonce?: string | undefined }): Promise<string> {
94
+ const user = users.get(token.sub);
95
+ assert(user !== undefined, `fake IdP has no user ${token.sub}`, 'add it to users: [...]');
96
+ const now = Math.floor(clock.now().getTime() / 1000);
97
+ const body = `${segment({ alg: 'ES256', kid: KID, typ: 'JWT' })}.${segment({
98
+ iss: issuer,
99
+ sub: user.sub,
100
+ aud: flags.wrongAudience === true ? 'another-client' : clientId,
101
+ iat: now,
102
+ exp: flags.expired === true ? now - 3600 : now + 3600,
103
+ ...(token.nonce === undefined ? {} : { nonce: token.nonce }),
104
+ ...claimsOf(user),
105
+ })}`;
106
+ const key = (flags.wrongKey === true ? rogue : published).privateKey;
107
+ const signature = await crypto.subtle.sign(
108
+ { name: 'ECDSA', hash: 'SHA-256' },
109
+ key,
110
+ encoder.encode(body),
111
+ );
112
+ return `${body}.${b64url(new Uint8Array(signature))}`;
113
+ }
114
+
115
+ const json = (body: unknown, status = 200) => Response.json(body, { status });
116
+ mockFetch(`GET ${issuer}/.well-known/openid-configuration`, () =>
117
+ json({
118
+ issuer,
119
+ authorization_endpoint: `${issuer}/authorize`,
120
+ token_endpoint: `${issuer}/token`,
121
+ userinfo_endpoint: `${issuer}/userinfo`,
122
+ jwks_uri: `${issuer}/jwks`,
123
+ response_types_supported: ['code'],
124
+ subject_types_supported: ['public'],
125
+ id_token_signing_alg_values_supported: ['ES256'],
126
+ code_challenge_methods_supported: ['S256'],
127
+ }),
128
+ );
129
+ mockFetch(`GET ${issuer}/jwks`, () =>
130
+ json({ keys: [{ ...jwk, kid: KID, alg: 'ES256', use: 'sig' }] }),
131
+ );
132
+ mockFetch(`GET ${issuer}/authorize*`, (request) => {
133
+ const params = new URL(request.url).searchParams;
134
+ const redirect = params.get('redirect_uri');
135
+ if (redirect === null || params.get('client_id') !== clientId) {
136
+ return json({ error: 'invalid_request' }, 400);
137
+ }
138
+ const code = random();
139
+ codes.set(code, {
140
+ sub: chosen,
141
+ nonce: params.get('nonce'),
142
+ challenge: params.get('code_challenge'),
143
+ });
144
+ const back = new URL(redirect);
145
+ back.searchParams.set('code', code);
146
+ const state = params.get('state');
147
+ if (state !== null) back.searchParams.set('state', state);
148
+ return new Response(null, { status: 302, headers: { location: back.toString() } });
149
+ });
150
+ mockFetch(`POST ${issuer}/token`, async (request) => {
151
+ const form = new URLSearchParams(await request.text());
152
+ const pending = codes.get(form.get('code') ?? '');
153
+ codes.delete(form.get('code') ?? '');
154
+ if (pending === undefined) return json({ error: 'invalid_grant' }, 400);
155
+ const verifier = form.get('code_verifier') ?? '';
156
+ const digest = new Bun.CryptoHasher('sha256').update(verifier).digest('base64url');
157
+ if (pending.challenge !== null && !timingSafeEqual(digest, pending.challenge)) {
158
+ return json({ error: 'invalid_grant' }, 400);
159
+ }
160
+ const access = random();
161
+ accessTokens.set(access, pending.sub);
162
+ return json({
163
+ access_token: access,
164
+ token_type: 'Bearer',
165
+ expires_in: 3600,
166
+ scope: 'openid email profile',
167
+ id_token: await idToken({ sub: pending.sub, nonce: pending.nonce ?? undefined }),
168
+ });
169
+ });
170
+ mockFetch(`GET ${issuer}/userinfo`, (request) => {
171
+ const access = (request.headers.get('authorization') ?? '').replace(/^Bearer /i, '');
172
+ const user = users.get(accessTokens.get(access) ?? '');
173
+ if (user === undefined) return json({ error: 'invalid_token' }, 401);
174
+ return json({ sub: user.sub, ...claimsOf(user) });
175
+ });
176
+
177
+ return {
178
+ issuer,
179
+ clientId,
180
+ clientSecret,
181
+ jwksUri: `${issuer}/jwks`,
182
+ signInAs(sub) {
183
+ assert(users.has(sub), `fake IdP has no user ${sub}`, 'add it to users: [...]');
184
+ chosen = sub;
185
+ },
186
+ tamper(next) {
187
+ flags = { ...next };
188
+ },
189
+ idToken,
190
+ };
191
+ }
@@ -0,0 +1,342 @@
1
+ // A TCP proxy in front of one real service, with the fault switchable mid-run: what `x drill` puts
2
+ // between an app's roles and its Postgres, Redis and NATS. Bun's own sockets, no container and no
3
+ // kernel rule — so a drill runs the same on a laptop and a free CI runner. The faults are the
4
+ // outages this plan reproduced by hand: an unanswered path, a slow one, a reset, a refusal.
5
+
6
+ import { assert, finiteCount, MAX_TIMER_MS } from '@ultimat3/core';
7
+ import type { Socket, TCPSocketListener } from 'bun';
8
+
9
+ /**
10
+ * `blackhole` accepts and never answers — a node loss, where the server is healthy and the path is
11
+ * not. Bytes that arrive meanwhile are HELD, never dropped, and cross once the mode lets them.
12
+ */
13
+ export type FaultMode =
14
+ | { readonly kind: 'pass' }
15
+ | { readonly kind: 'delay'; readonly ms: number }
16
+ | { readonly kind: 'blackhole' }
17
+ | { readonly kind: 'reset' }
18
+ | { readonly kind: 'refuse' };
19
+
20
+ export type FaultModeKind = FaultMode['kind'];
21
+
22
+ export const FAULT_MODE_KINDS: readonly FaultModeKind[] = [
23
+ 'pass',
24
+ 'delay',
25
+ 'blackhole',
26
+ 'reset',
27
+ 'refuse',
28
+ ];
29
+
30
+ export interface FaultProxyCounts {
31
+ /** Connections the proxy accepted and dialled through. */
32
+ readonly accepted: number;
33
+ /** Connections cut: by a `reset` or `refuse`, or accepted under `reset`. */
34
+ readonly reset: number;
35
+ /** Connections whose dial to the real service failed. */
36
+ readonly upstreamFailed: number;
37
+ /** Connections open right now. */
38
+ readonly open: number;
39
+ readonly bytesUp: number;
40
+ readonly bytesDown: number;
41
+ /** Bytes held by a `blackhole`, not yet delivered. */
42
+ readonly held: number;
43
+ }
44
+
45
+ export interface FaultProxy {
46
+ /** The loopback port the app dials instead of the service's. Kept across a `refuse`. */
47
+ readonly port: number;
48
+ readonly mode: FaultMode;
49
+ /** Switch the fault. Takes effect on the next byte; held bytes are re-judged under `mode`. */
50
+ set(mode: FaultMode): Promise<void>;
51
+ counts(): FaultProxyCounts;
52
+ /** Cut every connection and stop listening. Idempotent. */
53
+ close(): void;
54
+ }
55
+
56
+ export interface FaultProxyOptions {
57
+ readonly target: { readonly hostname: string; readonly port: number };
58
+ readonly mode?: FaultMode | undefined;
59
+ /** Listen here rather than on a port of the OS's choosing. */
60
+ readonly port?: number | undefined;
61
+ /** How a `delay` waits. A test injects a gate it opens; a drill takes the wall clock's. */
62
+ readonly sleep?: ((ms: number) => Promise<void>) | undefined;
63
+ }
64
+
65
+ type Direction = 'up' | 'down';
66
+
67
+ /** A socket's unsent bytes, flushed on `drain`: a write the kernel half-took is never lost. */
68
+ interface Outbox {
69
+ readonly queue: Uint8Array[];
70
+ }
71
+
72
+ interface Pair {
73
+ readonly client: Socket<Leg>;
74
+ upstream: Socket<Leg> | undefined;
75
+ /** Client bytes that arrived before the dial landed. */
76
+ readonly early: Uint8Array[];
77
+ readonly held: Map<Direction, Uint8Array[]>;
78
+ /** Per direction, the delivery a `delay` chains on, so order survives the wait. */
79
+ readonly chain: Map<Direction, Promise<void>>;
80
+ }
81
+
82
+ interface Leg {
83
+ pair: Pair | undefined;
84
+ readonly out: Outbox;
85
+ }
86
+
87
+ const LOOPBACK = '127.0.0.1';
88
+
89
+ /** The mode, screened: a `delay` of NaN would arm `setTimeout(…, 0)` and drill nothing. */
90
+ export function screenFaultMode(mode: FaultMode): FaultMode {
91
+ assert(
92
+ FAULT_MODE_KINDS.includes(mode.kind),
93
+ `faultProxy was given the mode ${JSON.stringify(mode.kind)}, which is none of ${FAULT_MODE_KINDS.join(', ')}`,
94
+ "proxy.set({ kind: 'blackhole' }) # one of pass, delay, blackhole, reset, refuse",
95
+ );
96
+ if (mode.kind === 'delay') {
97
+ finiteCount('faultProxy', 'delay ms', mode.ms);
98
+ assert(
99
+ mode.ms <= MAX_TIMER_MS,
100
+ `faultProxy delay ms is ${String(mode.ms)}, past the longest a timer holds`,
101
+ "proxy.set({ kind: 'blackhole' }) # a delay no timer can hold is a blackhole; say so",
102
+ );
103
+ }
104
+ return mode;
105
+ }
106
+
107
+ function send(socket: Socket<Leg>, bytes: Uint8Array): void {
108
+ const out = socket.data.out;
109
+ if (out.queue.length > 0) {
110
+ out.queue.push(bytes);
111
+ return;
112
+ }
113
+ const written = socket.write(bytes);
114
+ if (written < bytes.length) out.queue.push(bytes.subarray(Math.max(0, written)));
115
+ }
116
+
117
+ function flush(socket: Socket<Leg>): void {
118
+ const queue = socket.data.out.queue;
119
+ while (queue.length > 0) {
120
+ const head = queue[0];
121
+ if (head === undefined) return;
122
+ const written = socket.write(head);
123
+ if (written < head.length) {
124
+ queue[0] = head.subarray(Math.max(0, written));
125
+ return;
126
+ }
127
+ queue.shift();
128
+ }
129
+ }
130
+
131
+ /** `{ target }` behind a loopback port whose fault `set` switches. */
132
+ export async function faultProxy(options: FaultProxyOptions): Promise<FaultProxy> {
133
+ let mode = screenFaultMode(options.mode ?? { kind: 'pass' });
134
+ const sleep = options.sleep ?? ((ms: number) => Bun.sleep(ms));
135
+ const pairs = new Set<Pair>();
136
+ const counts = { accepted: 0, reset: 0, upstreamFailed: 0, bytesUp: 0, bytesDown: 0 };
137
+ let closed = false;
138
+
139
+ const heldBytes = (): number => {
140
+ let total = 0;
141
+ for (const pair of pairs) {
142
+ for (const chunks of pair.held.values()) {
143
+ for (const chunk of chunks) total += chunk.length;
144
+ }
145
+ }
146
+ return total;
147
+ };
148
+
149
+ const cut = (pair: Pair): void => {
150
+ if (!pairs.delete(pair)) return;
151
+ counts.reset += 1;
152
+ pair.client.terminate();
153
+ pair.upstream?.terminate();
154
+ };
155
+
156
+ const write = (pair: Pair, direction: Direction, bytes: Uint8Array): void => {
157
+ if (!pairs.has(pair)) return;
158
+ if (direction === 'down') {
159
+ send(pair.client, bytes);
160
+ return;
161
+ }
162
+ if (pair.upstream === undefined) pair.early.push(bytes);
163
+ else send(pair.upstream, bytes);
164
+ };
165
+
166
+ /** One chunk, judged by the mode in force NOW — so a byte a delay held re-meets a blackhole. */
167
+ const route = (pair: Pair, direction: Direction, bytes: Uint8Array): void => {
168
+ if (!pairs.has(pair)) return;
169
+ if (mode.kind === 'blackhole') {
170
+ pair.held.get(direction)?.push(bytes);
171
+ return;
172
+ }
173
+ if (mode.kind === 'delay') {
174
+ const timer = sleep(mode.ms);
175
+ const previous = pair.chain.get(direction) ?? Promise.resolve();
176
+ const next = Promise.all([previous, timer]).then(() => {
177
+ // A switch to blackhole while this waited holds it; a pass writes it.
178
+ if (mode.kind === 'blackhole') pair.held.get(direction)?.push(bytes);
179
+ else write(pair, direction, bytes);
180
+ });
181
+ pair.chain.set(direction, next);
182
+ return;
183
+ }
184
+ write(pair, direction, bytes);
185
+ };
186
+
187
+ const dial = (pair: Pair): void => {
188
+ void Bun.connect<Leg>({
189
+ hostname: options.target.hostname,
190
+ port: options.target.port,
191
+ socket: {
192
+ open(upstream) {
193
+ upstream.data = { pair, out: { queue: [] } };
194
+ if (!pairs.has(pair)) {
195
+ upstream.terminate();
196
+ return;
197
+ }
198
+ pair.upstream = upstream;
199
+ for (const bytes of pair.early.splice(0)) send(upstream, bytes);
200
+ },
201
+ data(_upstream, chunk) {
202
+ counts.bytesDown += chunk.length;
203
+ // Copied: the runtime reuses the chunk's buffer once this handler returns.
204
+ route(pair, 'down', Uint8Array.from(chunk));
205
+ },
206
+ drain: flush,
207
+ close() {
208
+ if (pairs.delete(pair)) pair.client.end();
209
+ },
210
+ error() {
211
+ if (pairs.delete(pair)) pair.client.end();
212
+ },
213
+ },
214
+ }).catch(() => {
215
+ counts.upstreamFailed += 1;
216
+ if (pairs.delete(pair)) pair.client.terminate();
217
+ });
218
+ };
219
+
220
+ const listen = (port: number): TCPSocketListener<Leg> =>
221
+ Bun.listen<Leg>({
222
+ hostname: LOOPBACK,
223
+ port,
224
+ socket: {
225
+ open(client) {
226
+ client.data = { pair: undefined, out: { queue: [] } };
227
+ if (mode.kind === 'reset' || mode.kind === 'refuse') {
228
+ counts.reset += 1;
229
+ client.terminate();
230
+ return;
231
+ }
232
+ const pair: Pair = {
233
+ client,
234
+ upstream: undefined,
235
+ early: [],
236
+ held: new Map([
237
+ ['up', []],
238
+ ['down', []],
239
+ ]),
240
+ chain: new Map(),
241
+ };
242
+ client.data.pair = pair;
243
+ pairs.add(pair);
244
+ counts.accepted += 1;
245
+ dial(pair);
246
+ },
247
+ data(client, chunk) {
248
+ counts.bytesUp += chunk.length;
249
+ const pair = client.data.pair;
250
+ if (pair !== undefined) route(pair, 'up', Uint8Array.from(chunk));
251
+ },
252
+ drain: flush,
253
+ close(client) {
254
+ const pair = client.data.pair;
255
+ if (pair !== undefined && pairs.delete(pair)) pair.upstream?.end();
256
+ },
257
+ error(client) {
258
+ const pair = client.data.pair;
259
+ if (pair !== undefined && pairs.delete(pair)) pair.upstream?.terminate();
260
+ },
261
+ },
262
+ });
263
+
264
+ let listener: TCPSocketListener<Leg> | undefined = listen(
265
+ finiteCount('faultProxy', 'port', options.port ?? 0),
266
+ );
267
+ const port = listener.port;
268
+ if (mode.kind === 'refuse') {
269
+ listener.stop(true);
270
+ listener = undefined;
271
+ }
272
+
273
+ return {
274
+ port,
275
+ get mode() {
276
+ return mode;
277
+ },
278
+ async set(next: FaultMode): Promise<void> {
279
+ assert(
280
+ !closed,
281
+ 'faultProxy.set() was called after close(), so there is no proxy to switch',
282
+ 'faultProxy({ target }) again — close() is final',
283
+ );
284
+ mode = screenFaultMode(next);
285
+ if (mode.kind === 'reset' || mode.kind === 'refuse') {
286
+ for (const pair of [...pairs]) cut(pair);
287
+ }
288
+ if (mode.kind === 'refuse') {
289
+ listener?.stop(true);
290
+ listener = undefined;
291
+ return;
292
+ }
293
+ // Back from a refusal: listening again on the SAME port, where every role still dials.
294
+ if (listener === undefined) listener = listen(port);
295
+ if (mode.kind === 'blackhole') return;
296
+ for (const pair of pairs) {
297
+ for (const [direction, chunks] of pair.held) {
298
+ for (const bytes of chunks.splice(0)) route(pair, direction, bytes);
299
+ }
300
+ }
301
+ },
302
+ counts: () => ({ ...counts, open: pairs.size, held: heldBytes() }),
303
+ close(): void {
304
+ if (closed) return;
305
+ closed = true;
306
+ for (const pair of [...pairs]) cut(pair);
307
+ listener?.stop(true);
308
+ listener = undefined;
309
+ },
310
+ };
311
+ }
312
+
313
+ /**
314
+ * `url` re-pointed at a proxy on loopback: scheme, credentials, database and query kept, since the
315
+ * proxy forwards bytes and the service still authenticates them. One rewriter for every service.
316
+ */
317
+ export function throughFaultProxy(url: string, port: number): string {
318
+ const parsed = new URL(url);
319
+ parsed.hostname = LOOPBACK;
320
+ parsed.port = String(port);
321
+ return parsed.toString();
322
+ }
323
+
324
+ /** Where a service URL points: what `faultProxy({ target })` dials. */
325
+ export function faultTarget(url: string): { readonly hostname: string; readonly port: number } {
326
+ const parsed = new URL(url);
327
+ const fallback = DEFAULT_PORTS.get(parsed.protocol);
328
+ const port = parsed.port === '' ? fallback : Number.parseInt(parsed.port, 10);
329
+ assert(
330
+ port !== undefined,
331
+ `${parsed.protocol}// names no port and has no default this proxy knows`,
332
+ 'TEST_DATABASE_URL=postgres://ultimate:ultimate@127.0.0.1:5432/postgres # name the port in the URL',
333
+ );
334
+ return { hostname: parsed.hostname, port };
335
+ }
336
+
337
+ const DEFAULT_PORTS: ReadonlyMap<string, number> = new Map([
338
+ ['postgres:', 5432],
339
+ ['postgresql:', 5432],
340
+ ['redis:', 6379],
341
+ ['nats:', 4222],
342
+ ]);
@@ -4,7 +4,7 @@
4
4
  // Since 22.7 `x test` / `x verify` run each worker WITHOUT `--isolate`: one global and one module
5
5
  // registry per worker, reused across every file it runs (2-5x faster, measured in
6
6
  // `@ultimat3/cli`'s `test-workers.ts`). What a file leaves on `globalThis` then reaches every
7
- // later file on that worker. Measured on notificado.co: one island test that never disposed its
7
+ // later file on that worker. Measured on one adopter's app: one island test that never disposed its
8
8
  // mount left the fake `window`/`document` installed, PGlite then took the process for a browser
9
9
  // (`window.location.pathname` of undefined), and 264 unrelated tests failed.
10
10
  //
@@ -130,6 +130,9 @@ export function runBoundaryHooks(): void {
130
130
  }
131
131
  }
132
132
 
133
+ /** Whether a test file has loaded yet: before it, a call is a preload's, which every file inherits. */
134
+ export const inTestFile = (): boolean => baseline !== undefined;
135
+
133
136
  /** Called by the leak guard as each test file loads. The first call only takes the baseline. */
134
137
  export function runFileBoundary(): void {
135
138
  if (baseline === undefined) {
@@ -221,7 +221,9 @@ export async function testJobs(): Promise<RunJobs> {
221
221
  queue: handle.queue,
222
222
  input,
223
223
  idempotencyKey: handle.idempotencyKeyFor(input),
224
- maxAttempts: handle.retry.attempts,
224
+ // The row's count, never the policy's: a `compensates` job carries the unwind margin, so a
225
+ // final attempt whose worker died is re-offered to unwind instead of buried.
226
+ maxAttempts: jobs.enqueuedAttempts(handle.retry, handle.compensates),
225
227
  ...(options.tenantId === undefined ? {} : { tenantId: options.tenantId }),
226
228
  });
227
229
 
@@ -2,6 +2,9 @@
2
2
  // repeats past the threshold throws where it happened, so an N+1 fails the test that caused it
3
3
  // instead of warning in a dev server nobody is watching.
4
4
  //
5
+ // Both drivers report here: Postgres per statement, `memoryRepo` per repository operation, so a
6
+ // loop in a unit test on the in-memory driver is the same verdict under the same `entity.op`.
7
+ //
5
8
  // Strict by construction: there is no `strict: true` to remember, because a fixture nobody
6
9
  // destructured is a fixture nobody built. Opting in is naming `statements` in the test body.
7
10
 
@@ -12,7 +15,7 @@ export interface ObservedStatement {
12
15
  /** What it is counted under: `members.findById` when attributed, else its own collapsed text. */
13
16
  readonly fingerprint: string;
14
17
  readonly kind: 'read' | 'write';
15
- /** The statement as sent, parameters still `$1..$n`. */
18
+ /** The statement as sent, parameters still `$1..$n`. The memory driver sends none: its label. */
16
19
  readonly text: string;
17
20
  /** The entity and operation that compiled it; absent for hand-written SQL and queue traffic. */
18
21
  readonly attribution?: StatementAttribution | undefined;
package/src/free-port.ts CHANGED
@@ -57,3 +57,62 @@ export const freePortApartFrom = (
57
57
  for (const probe of held) probe.stop();
58
58
  }
59
59
  };
60
+
61
+ /**
62
+ * The addresses `localhost` can name. A dual-stack host (GitHub's Ubuntu runner, a Debian container)
63
+ * maps it to both, and Bun binds the FIRST it resolves: a holder on `::1` left `localhost` free on
64
+ * `127.0.0.1`, so a probe on `localhost`, or on Bun's default `::`, answered "free" for a port
65
+ * `x dev` then met held (scaffold-smoke, CI run 41764752262). Each is asked on its own.
66
+ */
67
+ export const LOOPBACK_ADDRESSES: readonly string[] = ['127.0.0.1', '::1'];
68
+
69
+ /** An address this host does not have — IPv6 switched off — holds nothing. */
70
+ const ABSENT = new Set(['EADDRNOTAVAIL', 'EAFNOSUPPORT', 'EINVAL']);
71
+
72
+ /** Whether `port` is free on every loopback address `localhost` may resolve to. */
73
+ export const loopbackFree = (
74
+ port: number,
75
+ addresses: readonly string[] = LOOPBACK_ADDRESSES,
76
+ ): boolean =>
77
+ addresses.every((hostname) => {
78
+ try {
79
+ Bun.listen({ hostname, port, socket: { data: () => undefined } }).stop(true);
80
+ return true;
81
+ } catch (error) {
82
+ const code = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : '';
83
+ return typeof code === 'string' && ABSENT.has(code);
84
+ }
85
+ });
86
+
87
+ /**
88
+ * A free port whose NEIGHBOUR is free too, both ones a browser opens. `x dev` binds the web role on
89
+ * `port` and the sync node on `port + 1`, so a lone free port whose neighbour another process held
90
+ * was an e2e app that listened, failed its sync bind and exited before `/readyz` — the CLI's own
91
+ * `freeDevPort` rule (`dev-supervisor.ts`), which this package cannot import. A candidate is held
92
+ * while the next is asked for, as a refused port is, so the walk ends. The candidate itself is held
93
+ * on Bun's dual-stack `::`, which covers both loopbacks; the neighbour is asked of each.
94
+ */
95
+ export const freePortPair = (
96
+ listen: () => PortProbe = listenOnAnyPort,
97
+ canBind: (port: number) => boolean = loopbackFree,
98
+ ): number => {
99
+ const held: PortProbe[] = [];
100
+ try {
101
+ for (;;) {
102
+ const probe = listen();
103
+ const next = probe.port + 1;
104
+ if (
105
+ BROWSER_REFUSED_PORTS.has(probe.port) ||
106
+ BROWSER_REFUSED_PORTS.has(next) ||
107
+ !canBind(next)
108
+ ) {
109
+ held.push(probe);
110
+ continue;
111
+ }
112
+ probe.stop();
113
+ return probe.port;
114
+ }
115
+ } finally {
116
+ for (const probe of held) probe.stop();
117
+ }
118
+ };