@teamscala/service-boot 0.23.536 → 0.23.538

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.
@@ -1,751 +0,0 @@
1
- /**
2
- * @status generated — ts_function_contract_runner template (scala-test-suites
3
- * templates/ts_function_contract_runner.pkl @1.3.1) over @teamscala/service-boot's
4
- * test_vector rows: one `it` per function_call row (args -> value or
5
- * expect_error) and one per fs_flow row (ordered steps in a temp
6
- * workspace). This runner carries NO facts: every case is the registry
7
- * row projected into ./orpc-contract-rows.json. Regenerate from
8
- * inputs/<repo>/function_contract_runner.json; hand edits are overwritten.
9
- */
10
- import { describe, expect, it, mock } from "bun:test";
11
- import { mkdtemp, rm } from "node:fs/promises";
12
- import { tmpdir } from "node:os";
13
- import { existsSync } from "node:fs";
14
- import { dirname, join, resolve } from "node:path";
15
- import { spawnSync } from "node:child_process";
16
- import projected from "./orpc-contract-rows.json";
17
-
18
- const ROWS = (projected.rows ?? projected) as Vector[];
19
-
20
- // This file, and the one row a child process is selecting. Absent in the
21
- // parent run: every row executes there, and a row with fixtures forks.
22
- const SELF = import.meta.path;
23
- const SELECTED = process.env.SCALA_CONTRACT_ROW;
24
-
25
- if (!Array.isArray(ROWS)) {
26
- throw new Error("./orpc-contract-rows.json must project an array of rows — the runner executes registry rows, never a hand-written case list");
27
- }
28
-
29
- const PKG = "@teamscala/service-boot";
30
-
31
- // import.meta.dir is this FILE's directory (bun's own value, extension-
32
- // independent) — row modules are repo-relative, so the package root is the
33
- // nearest enclosing directory carrying a package.json, walked up: suites
34
- // render CO-LOCATED at any depth (1.3.0), and a fixed two-levels-up guess
35
- // resolves src/tier-selector/TierSelector/ suites against src/.
36
- const ROOT = (() => {
37
- let dir = import.meta.dir;
38
- for (let depth = 0; depth < 8; depth++) {
39
- if (existsSync(join(dir, "package.json"))) return dir;
40
- dir = join(dir, "..");
41
- }
42
- throw new Error("no package.json above " + import.meta.dir + " — a contract suite renders inside a package");
43
- })();
44
-
45
- type Vector = Record<string, any> & { suite_type: string; slug: string };
46
-
47
- // The ONE arg encoding: a declared error value is data, never a literal
48
- // in the row's JSON — {"$error": {"class", "module"?, "args"}}.
49
- // A JSON null at an ARG position is ABSENCE, never a value (row-writer
50
- // convention 2026-09-28 19:50 #1): it decodes to `undefined`, which is what
51
- // a Rust None and an omitted optional both mean. A null under `expect` is
52
- // the opposite — a VALUE, asserted with toEqual(null).
53
- async function decodeValue(value: unknown): Promise<unknown> {
54
- if (value === null) return undefined;
55
- if (value !== null && typeof value === "object" && "$error" in (value as object)) {
56
- const spec = (value as { $error: { class: string; module?: string; args?: unknown[] } }).$error;
57
- const ctor = spec.module
58
- ? ((await import(spec.module)) as Record<string, any>)[spec.class]
59
- : (globalThis as Record<string, any>)[spec.class];
60
- if (typeof ctor !== "function") throw new Error("unconstructible error class: " + spec.class);
61
- return new ctor(...(spec.args ?? []));
62
- }
63
- // A FUNCTION-valued arg: a block export is built from thunks, and JSON
64
- // cannot spell one. {"$fn": {"returns": <any>}} is the zero-arg thunk
65
- // returning that value; {"$fn": {"throws": <any>}} is the one raising
66
- // it. Decoded HERE, at build time, so the closure captures a finished
67
- // value rather than an unresolved promise.
68
- if (typeof value === "object" && "$fn" in (value as object)) {
69
- const spec = (value as { $fn: { returns?: unknown; throws?: unknown } }).$fn;
70
- if (spec.returns !== undefined) return () => spec.returns;
71
- if (spec.throws !== undefined) {
72
- const raised = await decodeValue(spec.throws);
73
- return () => { throw raised; };
74
- }
75
- throw new Error("$fn declares neither returns nor throws — a function whose behaviour is undeclared is a case the runner cannot run");
76
- }
77
- if (Array.isArray(value)) return Promise.all(value.map(decodeValue));
78
- // Composite args are walked: a $error or $fn nested at a member position is
79
- // the same encoding one level down, not a literal object that happens to
80
- // contain one.
81
- if (typeof value === "object") {
82
- const walked: Record<string, unknown> = {};
83
- for (const [key, inner] of Object.entries(value as Record<string, unknown>)) {
84
- walked[key] = await decodeValue(inner);
85
- }
86
- return walked;
87
- }
88
- return value;
89
- }
90
-
91
- // Resolve the EXPORT, without calling it: the shape of a class, a frozen
92
- // table or a namespace is a contract a call cannot state, so this is the
93
- // half of the pair callExport below cannot do.
94
- async function loadExport(modulePath: string, exportName: string): Promise<unknown> {
95
- const mod = (await import(join(ROOT, modulePath))) as Record<string, any>;
96
- if (!(exportName in mod)) throw new Error("no export " + exportName + " in " + modulePath);
97
- return mod[exportName];
98
- }
99
-
100
- async function callExport(modulePath: string, exportName: string, decodedArgs: unknown[]): Promise<unknown> {
101
- const fn = await loadExport(modulePath, exportName);
102
- if (typeof fn !== "function") throw new Error("export " + exportName + " in " + modulePath + " is not a function — a row that wants its shape declares expect.export_shape");
103
- // Decoding happened at the CALLER, once. Decoding here too would walk an
104
- // already-decoded $error instance a second time — an Error object has no
105
- // enumerable own keys, so the walk would collapse it to {} and the row
106
- // would assert against a value the subject never received.
107
- return await fn(...decodedArgs);
108
- }
109
-
110
- function applyEnv(env?: Record<string, string>): () => void {
111
- const before: Record<string, string | undefined> = {};
112
- for (const [key, value] of Object.entries(env ?? {})) {
113
- before[key] = process.env[key];
114
- process.env[key] = value;
115
- }
116
- return () => {
117
- for (const [key, value] of Object.entries(before)) {
118
- if (value === undefined) delete process.env[key];
119
- else process.env[key] = value;
120
- }
121
- };
122
- }
123
-
124
- function assertWellFormed(row: Vector): void {
125
- const bad = (msg: string): never => {
126
- throw new Error("row " + row.slug + " is malformed: " + msg);
127
- };
128
- if (PKG !== "*" && row.package !== PKG) bad("package " + JSON.stringify(row.package) + " — expected " + JSON.stringify(PKG));
129
- if (row.suite_type === "function_call") {
130
- if (typeof row.export !== "string" || row.export.length === 0) bad("export must be a non-empty string");
131
- if (row.args === null || row.env === null) bad("args and env must be arrays/objects, never null — null and an absent key are different states");
132
- if (row.args !== undefined && !Array.isArray(row.args)) bad("args must be a positional array (ruling 18:02 #1)");
133
- if (row.expect !== undefined && row.expect_error !== undefined) bad("declares both expect and expect_error — the arms are XOR");
134
- if (row.expect === undefined && row.expect_error === undefined) bad("declares neither expect nor expect_error — a row that asserts nothing asserts nothing");
135
- // null and an absent key are DIFFERENT states everywhere in a row: expect
136
- // null is a value (asserted with toEqual(null)), but a null args/env is a
137
- // malformed row — it is refused, never silently substituted.
138
- if (row.args === null || row.env === null) bad("args and env must be arrays/objects, never null");
139
- assertOneAxis("row " + row.slug, row.expect, row.args as unknown[] | undefined, row.expect_error);
140
- } else if (row.suite_type === "fs_flow") {
141
- if (!Array.isArray(row.steps) || (row.steps as unknown[]).length === 0) bad("fs_flow needs non-empty ordered steps");
142
- for (let i = 0; i < (row.steps as unknown[]).length; i++) {
143
- const step = (row.steps as any[])[i];
144
- if (typeof step.call !== "string" || step.call.length === 0) bad("steps[" + i + "].call must be a non-empty string");
145
- if (step.args === null) bad("steps[" + i + "].args must be a positional array, never null — null and an absent key are different states");
146
- if (step.args !== undefined && !Array.isArray(step.args)) bad("steps[" + i + "].args must be a positional array");
147
- if (step.expect === undefined && step.expect_error === undefined) bad("steps[" + i + "] asserts nothing");
148
- assertOneAxis("steps[" + i + "] of row " + row.slug, step.expect, step.args as unknown[] | undefined, step.expect_error);
149
- }
150
- } else {
151
- bad("unknown suite_type " + JSON.stringify(row.suite_type));
152
- }
153
- }
154
-
155
- async function expectCall(row: Vector, exportName: string, args: unknown[]): Promise<void> {
156
- const restore = applyEnv(row.env);
157
- try {
158
- await withStubs(row, async () => {
159
- if (isExportShape(row.expect)) {
160
- assertStructural(row.slug, await loadExport(row.module, exportName), (row.expect as Record<string, unknown>)[EXPORT_SHAPE]);
161
- } else if (row.expect_error === undefined) {
162
- const decoded = await Promise.all(args.map(decodeValue));
163
- if (isStructural(row.expect) && (row.expect as Record<string, unknown>)[ARG_SHAPE] !== undefined) argTarget(row.slug, row.expect as Record<string, unknown>, decoded);
164
- const value = await callExport(row.module, exportName, decoded);
165
- if (isStructural(row.expect)) {
166
- if ((row.expect as Record<string, unknown>)[ARG_SHAPE] !== undefined) assertArgShape(row.slug, row.expect as Record<string, unknown>, decoded);
167
- else assertStructural(row.slug, value, row.expect);
168
- }
169
- else if (isTextSpec(row.expect)) assertText(row.slug, value, row.expect as Record<string, unknown>);
170
- else expect(value).toEqual(row.expect);
171
- } else {
172
- const raised = callExport(row.module, exportName, await Promise.all(args.map(decodeValue)));
173
- await expect(raised).rejects.toThrow(row.expect_error);
174
- }
175
- });
176
- } finally {
177
- restore();
178
- }
179
- }
180
-
181
- // ------------------------------------------------------ structural expect
182
- // A step names its own target: the live fs_flow rows spell call as
183
- // "path/to/module.ts:exportName" and the arm admits no row module, so a
184
- // step that declares no row module is split at its LAST colon. A call that
185
- // can neither be split nor resolved is refused BY NAME.
186
- function stepTarget(row: Vector, step: any): [string, string] {
187
- if (row.module !== undefined && row.module !== null && row.module !== "") return [String(row.module), String(step.call)];
188
- const spelled = String(step.call);
189
- const colon = spelled.lastIndexOf(":");
190
- if (colon <= 0 || colon === spelled.length - 1) {
191
- throw new Error("row " + row.slug + " is malformed: a step with no row module spells call as <module>:<export>, got " + JSON.stringify(spelled));
192
- }
193
- return [spelled.slice(0, colon), spelled.slice(colon + 1)];
194
- }
195
-
196
- const STRUCTURAL_VERBS = ["has_members", "member_is", "member_equals", "reads_resolve"] as const;
197
- const EXPORT_SHAPE = "export_shape";
198
- const ARG_SHAPE = "arg_shape";
199
- const TEXT_VERBS = ["contains_text", "absent", "in_order", "repeated"] as const;
200
-
201
- function isTextSpec(spec: unknown): boolean {
202
- return (
203
- spec !== null &&
204
- typeof spec === "object" &&
205
- !Array.isArray(spec) &&
206
- Object.keys(spec as object).some((key) => (TEXT_VERBS as readonly string[]).includes(key))
207
- );
208
- }
209
-
210
- function assertText(slug: string, value: unknown, spec: Record<string, unknown>): void {
211
- const bad = (msg: string): never => {
212
- throw new Error("row " + slug + " text expect did not hold: " + msg);
213
- };
214
- if (typeof value !== "string") {
215
- throw new Error("row " + slug + " is malformed: a text expect asserts a returned string, got " + (value === null ? "null" : typeof value));
216
- }
217
- const text = value as string;
218
- for (const verb of Object.keys(spec)) {
219
- if (!(TEXT_VERBS as readonly string[]).includes(verb)) {
220
- throw new Error("row " + slug + " is malformed: text expect " + verb + " is not a declared verb " + JSON.stringify(TEXT_VERBS) + " — a predicate this runner does not understand is a case it stopped asserting");
221
- }
222
- const needles = spec[verb];
223
- if (verb === "contains_text") {
224
- for (const needle of needles as string[]) if (!text.includes(needle)) bad("the text has no " + JSON.stringify(needle));
225
- } else if (verb === "absent") {
226
- for (const needle of needles as string[]) if (text.includes(needle)) bad("the text carries " + JSON.stringify(needle));
227
- } else if (verb === "in_order") {
228
- let after = -1;
229
- let previous = "the start";
230
- for (const marker of needles as string[]) {
231
- const at = text.indexOf(marker);
232
- if (at === -1) bad(JSON.stringify(marker) + " is not in the text at all");
233
- if (at <= after) bad(JSON.stringify(marker) + " is not after " + previous);
234
- after = at;
235
- previous = JSON.stringify(marker);
236
- }
237
- } else {
238
- const rep = needles as { pattern?: unknown; count?: unknown; group?: unknown };
239
- if (typeof rep.pattern !== "string" || typeof rep.count !== "number") {
240
- throw new Error("row " + slug + " is malformed: repeated needs {pattern, count} and an optional group — how many times the pattern matches, and which capture must not drift between the matches");
241
- }
242
- let matches: RegExpMatchArray[];
243
- try {
244
- matches = [...text.matchAll(new RegExp(rep.pattern, "g"))];
245
- } catch (err) {
246
- throw new Error("row " + slug + " is malformed: repeated.pattern is not a usable regex — " + String(err));
247
- }
248
- if (matches.length !== rep.count) bad("the pattern matched " + matches.length + " times, not the declared " + rep.count);
249
- if (rep.group !== undefined) {
250
- const group = rep.group as number;
251
- const first = matches[0]?.[group];
252
- if (first === undefined) bad("capture group " + group + " took no part in the first match");
253
- for (const match of matches) {
254
- if (match[group] !== first) bad("capture group " + group + " differs between matches: " + JSON.stringify(first) + " vs " + JSON.stringify(match[group]) + " — the two copies of this text must be the same set");
255
- }
256
- }
257
- }
258
- }
259
- }
260
-
261
-
262
- function isExportShape(spec: unknown): boolean {
263
- return (
264
- spec !== null &&
265
- typeof spec === "object" &&
266
- !Array.isArray(spec) &&
267
- (spec as Record<string, unknown>)[EXPORT_SHAPE] !== undefined
268
- );
269
- }
270
-
271
- function assertOneAxis(where: string, spec: unknown, args: unknown[] | undefined, expectError: unknown): void {
272
- const bad = (msg: string): never => {
273
- throw new Error(where + " is malformed: " + msg);
274
- };
275
- if (isExportShape(spec)) {
276
- const declared = (spec as Record<string, unknown>)[EXPORT_SHAPE];
277
- if (declared === null || typeof declared !== "object" || Array.isArray(declared)) bad(EXPORT_SHAPE + " must be a structural spec object");
278
- if (expectError !== undefined) bad(EXPORT_SHAPE + " with expect_error — no call is made, so nothing is raised to match");
279
- if (args !== undefined && args.length > 0) bad(EXPORT_SHAPE + " declares args — the export is not called, so nothing receives them");
280
- for (const verb of Object.keys(spec as object)) {
281
- if (verb !== EXPORT_SHAPE) bad("expect declares " + verb + " alongside " + EXPORT_SHAPE + " — one row states one axis; a row that wants both is two rows");
282
- }
283
- return;
284
- }
285
- if (isRenderRow(spec, args)) {
286
- if (args !== undefined && args.length > 1) bad("a render row takes at most one arg — the props object, not a positional list");
287
- if (spec === null || spec === undefined || typeof spec !== "object" || Array.isArray(spec)) return;
288
- for (const verb of Object.keys(spec as object)) {
289
- if (verb !== "render") bad("expect declares " + verb + " alongside render — one row states one axis; a row that wants both is two rows");
290
- }
291
- const renderSpec = (spec as Record<string, unknown>).render;
292
- if (renderSpec === null || typeof renderSpec !== "object" || Array.isArray(renderSpec)) bad("render must be a spec object");
293
- for (const verb of Object.keys(renderSpec as object)) {
294
- if (!(RENDER_VERBS as readonly string[]).includes(verb)) bad("render " + verb + " is not a declared verb " + JSON.stringify(RENDER_VERBS) + " — a predicate this runner does not understand is a case it stopped asserting");
295
- }
296
- const clickSelectors = (renderSpec as Record<string, unknown>).click;
297
- if (clickSelectors !== undefined) {
298
- if ((spec as Record<string, unknown>).expect_error !== undefined) bad("click beside expect_error — a row that expects the render to throw has nothing to click");
299
- const selectors = (Array.isArray(clickSelectors) ? clickSelectors : [clickSelectors]) as unknown[];
300
- if (selectors.length === 0) bad("click needs at least one selector");
301
- for (const selector of selectors) {
302
- if (typeof selector !== "string" || selector.length === 0) bad("click selectors must be non-empty strings, got " + JSON.stringify(selector));
303
- }
304
- }
305
- return;
306
- }
307
- if (spec === null || spec === undefined || typeof spec !== "object" || Array.isArray(spec)) return;
308
- if ((spec as Record<string, unknown>)[ARG_SHAPE] === undefined) return;
309
- const argSpec = (spec as Record<string, unknown>)[ARG_SHAPE];
310
- if (argSpec === null || typeof argSpec !== "object" || Array.isArray(argSpec)) bad(ARG_SHAPE + " must be a spec object");
311
- if (typeof (argSpec as { arg?: unknown }).arg !== "number") bad(ARG_SHAPE + ".arg must be declared — the positional index of the argument the subject mutates");
312
- for (const verb of Object.keys(spec as object)) {
313
- if (verb !== ARG_SHAPE) bad("expect declares " + verb + " alongside " + ARG_SHAPE + " — one row states one axis; a row that wants both is two rows");
314
- }
315
- }
316
-
317
- function isStructural(spec: unknown): boolean {
318
- return (
319
- isExportShape(spec) ||
320
- (spec !== null &&
321
- typeof spec === "object" &&
322
- !Array.isArray(spec) &&
323
- Object.keys(spec as object).some((key) =>
324
- (STRUCTURAL_VERBS as readonly string[]).includes(key) || key === ARG_SHAPE
325
- )
326
- )
327
- );
328
- }
329
-
330
- function argTarget(slug: string, spec: Record<string, unknown>, decoded: unknown[]): number {
331
- const target = (spec[ARG_SHAPE] as { arg?: unknown }).arg;
332
- if (typeof target !== "number" || !Number.isInteger(target) || target < 0 || target >= decoded.length) {
333
- throw new Error("row " + slug + " is malformed: " + ARG_SHAPE + ".arg must be an argument position this row declares (0.." + (decoded.length - 1) + "), got " + JSON.stringify(target));
334
- }
335
- return target;
336
- }
337
-
338
- function assertArgShape(slug: string, spec: Record<string, unknown>, decoded: unknown[]): void {
339
- assertStructural(slug, decoded[argTarget(slug, spec, decoded)], spec[ARG_SHAPE], "arg");
340
- }
341
-
342
- function memberAt(value: unknown, path: string): unknown {
343
- let at: any = value;
344
- for (const key of path.split(".")) {
345
- if (at === null || at === undefined) return undefined;
346
- at = at[key];
347
- }
348
- return at;
349
- }
350
-
351
- function assertStructural(slug: string, value: unknown, spec: any, declaration?: string): void {
352
- for (const verb of Object.keys(spec)) {
353
- if (verb === declaration) continue;
354
- if (!(STRUCTURAL_VERBS as readonly string[]).includes(verb)) {
355
- throw new Error(
356
- "row " + slug + " is malformed: structural expect " + verb +
357
- " is not a declared verb " + JSON.stringify(STRUCTURAL_VERBS) +
358
- " — a predicate this runner does not understand is a case it stopped asserting");
359
- }
360
- }
361
- const bad = (verb: string, detail: string): never => {
362
- throw new Error("row " + slug + " outcome " + verb + " did not hold: " + detail);
363
- };
364
- if (spec.has_members !== undefined) {
365
- for (const path of spec.has_members as string[]) {
366
- if (memberAt(value, path) === undefined) bad("has_members", path + " is not declared");
367
- }
368
- }
369
- if (spec.member_is !== undefined) {
370
- for (const [path, kind] of Object.entries(spec.member_is as Record<string, string>)) {
371
- const got = memberAt(value, path);
372
- if (got === undefined) bad("member_is", path + " is not declared");
373
- if (typeof got !== kind) bad("member_is", path + " is " + typeof got + ", the row declares " + kind);
374
- }
375
- }
376
- if (spec.member_equals !== undefined) {
377
- for (const [path, want] of Object.entries(spec.member_equals as Record<string, unknown>)) {
378
- expect(memberAt(value, path)).toEqual(want);
379
- }
380
- }
381
- if (spec.reads_resolve !== undefined) {
382
- for (const path of spec.reads_resolve as string[]) {
383
- try {
384
- memberAt(value, path);
385
- } catch (err) {
386
- bad("reads_resolve", "reading " + path + " threw " + String(err));
387
- }
388
- }
389
- }
390
- }
391
-
392
- const RENDER_VERBS = ["contains", "selector_present", "selector_absent", "click"] as const;
393
- const ELEMENT_MARKER = "$throw_on_render";
394
-
395
- function isRender(spec: unknown): boolean {
396
- return (
397
- spec !== null &&
398
- typeof spec === "object" &&
399
- !Array.isArray(spec) &&
400
- (spec as Record<string, unknown>).render !== undefined
401
- );
402
- }
403
-
404
- function hasMarker(value: unknown): boolean {
405
- if (Array.isArray(value)) return value.some(hasMarker);
406
- if (value !== null && typeof value === "object") {
407
- if (Object.keys(value as object).includes(ELEMENT_MARKER)) return true;
408
- for (const key of Object.keys(value as object)) {
409
- if (hasMarker((value as Record<string, unknown>)[key])) return true;
410
- }
411
- }
412
- return false;
413
- }
414
-
415
- function isRenderRow(spec: unknown, args: unknown[] | undefined): boolean {
416
- return isRender(spec) || (args !== undefined && args.some(hasMarker));
417
- }
418
-
419
- // The ONE element encoding: {"$throw_on_render": "<message>"} anywhere in
420
- // the props tree becomes a component whose render raises that message — the
421
- // throwing child a recovery contract is ABOUT. Anything else starting with $
422
- // inside a render tree is refused: a marker this runner does not know would
423
- // otherwise render as inert data and a case would silently stop asserting.
424
- function buildElements(React: any, value: unknown, slug: string): unknown {
425
- if (Array.isArray(value)) return value.map((item, index) => {
426
- const builtItem = buildElements(React, item, slug);
427
- if (builtItem !== null && typeof builtItem === "object" && (builtItem as Record<string, unknown>).$$typeof !== undefined) return React.cloneElement(builtItem, { key: String(index) });
428
- return builtItem;
429
- });
430
- if (value !== null && typeof value === "object") {
431
- const record = value as Record<string, unknown>;
432
- if (Object.keys(record).includes(ELEMENT_MARKER)) {
433
- if (typeof record[ELEMENT_MARKER] !== "string") throw new Error("row " + slug + " is malformed: " + ELEMENT_MARKER + " takes the error message string, got " + JSON.stringify(record[ELEMENT_MARKER]));
434
- const message = record[ELEMENT_MARKER] as string;
435
- return React.createElement(function Thrower(): never {
436
- throw new Error(message);
437
- });
438
- }
439
- const built: Record<string, unknown> = {};
440
- for (const key of Object.keys(record)) {
441
- if (key.startsWith("$")) throw new Error("row " + slug + " is malformed: element marker " + key + " is not declared — a marker this runner does not know is a child that would render as inert data");
442
- built[key] = buildElements(React, record[key], slug);
443
- }
444
- return built;
445
- }
446
- return value;
447
- }
448
-
449
- function assertRenderSpec(slug: string, container: HTMLElement, spec: any): void {
450
- const text = container.textContent ?? "";
451
- const bad = (verb: string, detail: string): never => {
452
- throw new Error("row " + slug + " outcome " + verb + " did not hold: " + detail);
453
- };
454
- for (const needle of (spec.contains ?? []) as string[]) {
455
- if (!text.includes(needle)) bad("contains", "the rendered text has no " + JSON.stringify(needle));
456
- }
457
- for (const selector of (spec.selector_present ?? []) as string[]) {
458
- if (container.querySelector(selector) === null) bad("selector_present", "no element matches " + selector);
459
- }
460
- for (const selector of (spec.selector_absent ?? []) as string[]) {
461
- if (container.querySelector(selector) !== null) bad("selector_absent", selector + " is present");
462
- }
463
- }
464
-
465
- async function renderRow(row: Vector): Promise<void> {
466
- const restore = applyEnv(row.env);
467
- try {
468
- await withStubs(row, async () => {
469
- if (!globalThis.document) {
470
- const { GlobalRegistrator } = await import("@happy-dom/global-registrator");
471
- await GlobalRegistrator.register({ url: "http://localhost" });
472
- }
473
- (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
474
- const React = await import("react");
475
- const { createRoot } = await import("react-dom/client");
476
- const act = (React as unknown as { act: (run: () => Promise<void>) => Promise<void> }).act;
477
- const Component = await loadExport(row.module, row.export);
478
- if (typeof Component !== "function") throw new Error("export " + row.export + " in " + row.module + " is not a component — a render row needs a callable component export");
479
- const decoded = await Promise.all(((row.args ?? []) as unknown[]).map(decodeValue));
480
- const props = decoded.length === 0 ? {} : buildElements(React, decoded[0], row.slug);
481
- const host = document.createElement("div");
482
- document.body.appendChild(host);
483
- const root = createRoot(host);
484
- let caught: unknown;
485
- try {
486
- await act(async () => {
487
- root.render(React.createElement(Component, props as Record<string, unknown>));
488
- });
489
- } catch (err) {
490
- caught = err;
491
- }
492
- if (row.expect_error !== undefined) {
493
- if (caught === undefined) throw new Error("row " + row.slug + " expected the render to throw " + JSON.stringify(row.expect_error) + ", and it returned a tree");
494
- const message = caught instanceof Error ? caught.message : String(caught);
495
- if (!message.includes(String(row.expect_error))) throw new Error("row " + row.slug + " outcome expect_error did not hold: the render threw " + JSON.stringify(message));
496
- await act(async () => { root.unmount(); });
497
- host.remove();
498
- return;
499
- }
500
- if (caught !== undefined) throw caught instanceof Error ? caught : new Error(String(caught));
501
- const clickSelectors = ((row.expect as Record<string, unknown>).render as Record<string, unknown>).click;
502
- if (clickSelectors !== undefined) {
503
- for (const selector of (Array.isArray(clickSelectors) ? clickSelectors : [clickSelectors]) as string[]) {
504
- const target = host.querySelector(selector);
505
- if (target === null) throw new Error("row " + row.slug + " outcome click did not hold: no element matches " + selector);
506
- await act(async () => {
507
- target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
508
- });
509
- }
510
- }
511
- assertRenderSpec(row.slug, host, (row.expect as Record<string, unknown>).render);
512
- await act(async () => { root.unmount(); });
513
- host.remove();
514
- });
515
- } finally {
516
- restore();
517
- }
518
- }
519
- function runFsFlow(row: Vector): Promise<void> {
520
- return (async () => {
521
- const workspace = await mkdtemp(join(tmpdir(), "contract-"));
522
- const before = process.env.SCALA_WORKSPACE_ROOT;
523
- process.env.SCALA_WORKSPACE_ROOT = workspace;
524
- try {
525
- await withStubs(row, async () => {
526
- for (const step of row.steps as Vector[]) {
527
- const [modPath, stepExport] = stepTarget(row, step);
528
- const args = await Promise.all((step.args ?? []).map(decodeValue));
529
- if (isExportShape(step.expect)) {
530
- assertStructural(row.slug, await loadExport(modPath, stepExport), (step.expect as Record<string, unknown>)[EXPORT_SHAPE]);
531
- } else if (step.expect_error !== undefined) {
532
- await expect(callExport(modPath, stepExport, args)).rejects.toThrow(step.expect_error);
533
- } else {
534
- if (isStructural(step.expect) && (step.expect as Record<string, unknown>)[ARG_SHAPE] !== undefined) argTarget(row.slug, step.expect as Record<string, unknown>, args);
535
- const value = await callExport(modPath, stepExport, args);
536
- if (isStructural(step.expect)) {
537
- if ((step.expect as Record<string, unknown>)[ARG_SHAPE] !== undefined) assertArgShape(row.slug, step.expect as Record<string, unknown>, args);
538
- else assertStructural(row.slug, value, step.expect);
539
- }
540
- else if (isTextSpec(step.expect)) assertText(row.slug, value, step.expect as Record<string, unknown>);
541
- else expect(value).toEqual(step.expect);
542
- }
543
- }
544
- });
545
- } finally {
546
- if (before === undefined) delete process.env.SCALA_WORKSPACE_ROOT;
547
- else process.env.SCALA_WORKSPACE_ROOT = before;
548
- await rm(workspace, { recursive: true, force: true });
549
- }
550
- })();
551
- }
552
-
553
-
554
- // ------------------------------------------------------------- fixtures
555
- // Stubs, recorded outcomes and seeded state are three different facts. This
556
- // runner carries the FIRST two; seeded state is the db_flow arm's business.
557
- //
558
- // The CLOSED outcome vocabulary. A key outside it is refused by name: a
559
- // predicate the runner does not understand is a row whose case silently
560
- // stopped being asserted, which is the one failure this wave cannot have.
561
- const OUTCOME_VERBS = ["count", "equals", "contains", "absent", "sql_contains", "sql_absent", "binds"] as const;
562
-
563
- type Stub = Vector & { module: string; export: string; record?: string; returns?: unknown; error?: unknown; when_sql_contains?: string };
564
- type Call = { args: unknown[] };
565
-
566
- // A stub's `module` is the SUBJECT'S OWN IMPORT SPECIFIER — what the
567
- // subject wrote in its import line — so it resolves against the subject
568
- // file's directory, not the package root.
569
- function stubPath(row: Vector, stub: Stub): string {
570
- return resolve(dirname(resolve(ROOT, String(row.module))), String(stub.module));
571
- }
572
-
573
- // A stub answers with the DATA the row declared, decoded SYNCHRONOUSLY: a
574
- // subject may call its dependency without awaiting it, and an async stub
575
- // hands that subject a Promise whose members are undefined — the row then
576
- // asserts against a value no row declared. Awaiting a plain value is the
577
- // value, so an async subject is unaffected. A stub return is a VALUE
578
- // position: null is null here, the opposite of an ARG position.
579
- function decodeStubReturn(value: unknown): unknown {
580
- if (Array.isArray(value)) return value.map(decodeStubReturn);
581
- if (typeof value === "object" && value !== null) {
582
- if ("$fn" in value) {
583
- // A stubbed export may RETURN a contract whose members are FUNCTIONS —
584
- // an injected page client (getBrandColors: () => Record) is the measured
585
- // shape. {"$fn": {"returns": X}} decodes to the same SYNC zero-arg thunk
586
- // the arg side builds; a function member declared as plain data would be
587
- // called by the subject and fail as "not a function" against a stub that
588
- // answered with an inert object.
589
- const spec = (value as { $fn: { returns?: unknown; throws?: unknown } }).$fn;
590
- if (spec.returns !== undefined) return () => decodeStubReturn(spec.returns);
591
- if (spec.throws !== undefined) {
592
- const raised = decodeStubReturn(spec.throws);
593
- return () => {
594
- throw raised;
595
- };
596
- }
597
- throw new Error("a fixture stub return declares $fn with neither returns nor throws — a function whose behaviour is undeclared is a case the runner cannot run");
598
- }
599
- if ("$error" in value) {
600
- throw new Error("a fixture stub return declares $error — a stub's failure is the stub-level error key; a return is what the call answers with");
601
- }
602
- const walked: Record<string, unknown> = {};
603
- for (const [key, inner] of Object.entries(value as Record<string, unknown>)) {
604
- walked[key] = decodeStubReturn(inner);
605
- }
606
- return walked;
607
- }
608
- return value;
609
- }
610
-
611
- // Every stub sharing an export forms ONE group: a Db stub answers per SQL
612
- // shape, and the first shape whose needle the call's SQL satisfies wins. A
613
- // call matching no declared shape REFUSES — guessing an answer no row
614
- // declared is how a test asserts something that was never true.
615
- function stubReturn(slug: string, group: Stub[], call: Call): unknown {
616
- const sql = typeof call.args[0] === "string" ? (call.args[0] as string) : "";
617
- for (const stub of group) {
618
- if (stub.when_sql_contains !== undefined && !sql.includes(String(stub.when_sql_contains))) continue;
619
- if (stub.error !== undefined) throw stub.error;
620
- return stub.returns;
621
- }
622
- if (group.every((stub) => stub.when_sql_contains !== undefined)) {
623
- throw new Error(
624
- `row ${slug} is malformed: no fixture stub matches the SQL shape ${JSON.stringify(sql.slice(0, 120))} — ` +
625
- "declared shapes: " + group.map((stub) => JSON.stringify(stub.when_sql_contains)).join(", "));
626
- }
627
- return group[0].returns;
628
- }
629
-
630
- // Applies the row's stubs, runs the body, ASSERTS the recording on the
631
- // success path, then RESTORES every mocked module with the exports it had —
632
- // a stub that outlived its row would answer the rows after it with a value
633
- // they never declared. Outcomes are NOT asserted when the body itself
634
- // threw: the row already failed for a more specific reason than any
635
- // predicate over a call that may never have completed.
636
- async function withStubs<T>(row: Vector, fn: () => Promise<T>): Promise<T> {
637
- const stubs = (row.fixtures?.stubs ?? []) as Stub[];
638
- if (stubs.length === 0) return fn();
639
- const slug = row.slug;
640
- const recorders: Record<string, Call[]> = {};
641
- for (const stub of stubs) {
642
- if (stub.record !== undefined) (recorders[stub.record] ??= []);
643
- }
644
- const paths = [...new Set(stubs.map((stub) => stubPath(row, stub)))];
645
- for (const path of paths) {
646
- const group = stubs.filter((stub) => stubPath(row, stub) === path);
647
- const exported: Record<string, unknown> = {};
648
- for (const name of new Set(group.map((stub) => String(stub.export)))) {
649
- const arms = group.filter((stub) => String(stub.export) === name);
650
- exported[name] = (...args: unknown[]) => {
651
- const call: Call = { args };
652
- for (const arm of arms) {
653
- if (arm.record !== undefined) (recorders[arm.record] ??= []).push(call);
654
- }
655
- return decodeStubReturn(stubReturn(slug, arms, call));
656
- };
657
- }
658
- mock.module(path, () => exported);
659
- }
660
- const result = await fn();
661
- returnOutcome(slug, recorders, row.outcomes as Record<string, Vector[]> | undefined);
662
- return result;
663
- }
664
-
665
- // A STUB LIVES FOR THE WHOLE PROCESS — measured on bun 1.4.2, not assumed:
666
- // mock.module(path, () => original) leaves the mock installed,
667
- // mock.restore() does not restore module mocks, and the live namespace is
668
- // readonly, so there is no in-process teardown that undoes a module mock.
669
- // The honest consequence: a row with fixtures runs in its OWN PROCESS (
670
- // runIsolated below), so no stub can answer a row that never declared it.
671
- function hasFixtures(row: Vector): boolean {
672
- return Array.isArray(row.fixtures?.stubs) && (row.fixtures!.stubs as Stub[]).length > 0;
673
- }
674
-
675
- // Re-runs THIS FILE under `bun test` selecting only this row, in a fresh
676
- // process. The child's exit code is the verdict and its output is the
677
- // evidence, so a refusal inside the child (an undeclared SQL shape, an
678
- // outcome verb the runner does not know) surfaces verbatim in the parent.
679
- function runIsolated(row: Vector): void {
680
- const child = spawnSync("bun", ["test", SELF, "--test-name-pattern", row.slug], {
681
- cwd: ROOT,
682
- env: { ...process.env, SCALA_CONTRACT_ROW: row.slug },
683
- encoding: "utf8",
684
- });
685
- if (child.status !== 0) {
686
- const evidence = (child.stderr || child.stdout || "").trimEnd().split("\n").slice(-14).join("\n");
687
- throw new Error(`row ${row.slug} failed in its isolated run:\n${evidence}`);
688
- }
689
- }
690
-
691
- // ONE predicate vocabulary over what the stubs recorded: count is the number
692
- // of calls; equals is the whole recording deep-equal to the declared value;
693
- // contains/absent are substrings of it; sql_contains/sql_absent are over the
694
- // recorded SQL; binds is the binds argument of the selected call (the one
695
- // sql_contains names, or the first call when no SQL predicate is declared),
696
- // deep-equal to the declared array. Every declared verb is checked — one the
697
- // runner does not know is a refusal, never a skip.
698
- function returnOutcome(slug: string, recorders: Record<string, Call[]>, outcomes?: Record<string, Vector[]>): void {
699
- if (outcomes === undefined) return;
700
- for (const [recorder, predicates] of Object.entries(outcomes)) {
701
- const calls = recorders[recorder] ?? [];
702
- for (const predicate of predicates) {
703
- for (const verb of Object.keys(predicate)) {
704
- if (!(OUTCOME_VERBS as readonly string[]).includes(verb)) {
705
- throw new Error(
706
- `row ${slug} is malformed: outcome ${recorder}.${verb} is not a declared verb ` +
707
- JSON.stringify(OUTCOME_VERBS) + " — a predicate this runner does not understand is a case it stopped asserting");
708
- }
709
- }
710
- const say = (verb: string): never => {
711
- throw new Error(`row ${slug} outcome ${recorder}.${verb} did not hold`);
712
- };
713
- if (predicate.count !== undefined && calls.length !== Number(predicate.count)) say("count");
714
- if (predicate.sql_absent !== undefined) {
715
- for (const call of calls) {
716
- if (typeof call.args[0] === "string" && (call.args[0] as string).includes(String(predicate.sql_absent))) say("sql_absent");
717
- }
718
- }
719
- const text = JSON.stringify(calls);
720
- if (predicate.contains !== undefined && !text.includes(String(predicate.contains))) say("contains");
721
- if (predicate.absent !== undefined && text.includes(String(predicate.absent))) say("absent");
722
- if (predicate.equals !== undefined && text !== JSON.stringify(predicate.equals)) say("equals");
723
- if (predicate.sql_contains !== undefined && !calls.some((call) => typeof call.args[0] === "string" && (call.args[0] as string).includes(String(predicate.sql_contains)))) say("sql_contains");
724
- if (predicate.binds !== undefined) {
725
- const selected = predicate.sql_contains === undefined
726
- ? calls[0]
727
- : calls.find((call) => typeof call.args[0] === "string" && (call.args[0] as string).includes(String(predicate.sql_contains)));
728
- if (selected === undefined) say("binds");
729
- const binds = selected.args.find((arg) => Array.isArray(arg));
730
- if (JSON.stringify(binds) !== JSON.stringify(predicate.binds)) say("binds");
731
- }
732
- }
733
- }
734
- }
735
- function runRow(row: Vector): Promise<void> | void {
736
- // Refused BY NAME before any call: the failing `it` is the row's slug.
737
- assertWellFormed(row);
738
- if (SELECTED === undefined && hasFixtures(row)) return runIsolated(row);
739
- if (isRenderRow(row.expect, row.args as unknown[] | undefined)) return renderRow(row);
740
- if (row.suite_type === "fs_flow") return runFsFlow(row);
741
- return expectCall(row, row.export, row.args ?? []);
742
- }
743
-
744
- describe("@teamscala/service-boot function_call contracts", () => {
745
- for (const row of ROWS) {
746
- if (SELECTED !== undefined && row.slug !== SELECTED) continue;
747
- it(row.slug, async () => {
748
- await runRow(row);
749
- });
750
- }
751
- });