@agiledigital/pingone-aic-script-tester 0.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 (169) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +274 -0
  4. package/dist/bin/fetch-jar.d.ts +2 -0
  5. package/dist/bin/fetch-jar.js +13 -0
  6. package/dist/bin/pull-profile.d.ts +2 -0
  7. package/dist/bin/pull-profile.js +40 -0
  8. package/dist/bin/show-log.d.ts +2 -0
  9. package/dist/bin/show-log.js +31 -0
  10. package/dist/classes/.complete +12 -0
  11. package/dist/classes/AmClassShutter.class +0 -0
  12. package/dist/classes/AmContextFactory$ObservedContext.class +0 -0
  13. package/dist/classes/AmContextFactory.class +0 -0
  14. package/dist/classes/AmScopes.class +0 -0
  15. package/dist/classes/HostOps.class +0 -0
  16. package/dist/classes/JsValues.class +0 -0
  17. package/dist/classes/Json$Parser.class +0 -0
  18. package/dist/classes/Json.class +0 -0
  19. package/dist/classes/LanguageVersions.class +0 -0
  20. package/dist/classes/Runner.class +0 -0
  21. package/dist/classes/ScriptContextScope.class +0 -0
  22. package/dist/src/aic/callbacks.d.ts +35 -0
  23. package/dist/src/aic/callbacks.js +130 -0
  24. package/dist/src/aic/conform.d.ts +90 -0
  25. package/dist/src/aic/conform.js +193 -0
  26. package/dist/src/aic/constants.d.ts +13 -0
  27. package/dist/src/aic/constants.js +13 -0
  28. package/dist/src/aic/diff.d.ts +25 -0
  29. package/dist/src/aic/diff.js +201 -0
  30. package/dist/src/aic/emit-journey.d.ts +64 -0
  31. package/dist/src/aic/emit-journey.js +254 -0
  32. package/dist/src/aic/emit-result.d.ts +10 -0
  33. package/dist/src/aic/emit-result.js +39 -0
  34. package/dist/src/aic/emit-session.d.ts +26 -0
  35. package/dist/src/aic/emit-session.js +132 -0
  36. package/dist/src/aic/emit-subject.d.ts +40 -0
  37. package/dist/src/aic/emit-subject.js +210 -0
  38. package/dist/src/aic/file-lease.d.ts +35 -0
  39. package/dist/src/aic/file-lease.js +543 -0
  40. package/dist/src/aic/http.d.ts +21 -0
  41. package/dist/src/aic/http.js +127 -0
  42. package/dist/src/aic/idm.d.ts +10 -0
  43. package/dist/src/aic/idm.js +112 -0
  44. package/dist/src/aic/index.d.ts +35 -0
  45. package/dist/src/aic/index.js +21 -0
  46. package/dist/src/aic/js.d.ts +8 -0
  47. package/dist/src/aic/js.js +11 -0
  48. package/dist/src/aic/lease-identity.d.ts +38 -0
  49. package/dist/src/aic/lease-identity.js +72 -0
  50. package/dist/src/aic/lease-lock.d.ts +45 -0
  51. package/dist/src/aic/lease-lock.js +173 -0
  52. package/dist/src/aic/managed.d.ts +28 -0
  53. package/dist/src/aic/managed.js +199 -0
  54. package/dist/src/aic/provider.d.ts +156 -0
  55. package/dist/src/aic/provider.js +416 -0
  56. package/dist/src/aic/record.d.ts +28 -0
  57. package/dist/src/aic/record.js +183 -0
  58. package/dist/src/aic/resource-snapshot.d.ts +12 -0
  59. package/dist/src/aic/resource-snapshot.js +116 -0
  60. package/dist/src/aic/run.d.ts +102 -0
  61. package/dist/src/aic/run.js +450 -0
  62. package/dist/src/aic/tenant.d.ts +107 -0
  63. package/dist/src/aic/tenant.js +307 -0
  64. package/dist/src/aic/trace.d.ts +25 -0
  65. package/dist/src/aic/trace.js +56 -0
  66. package/dist/src/aic/txid.d.ts +11 -0
  67. package/dist/src/aic/txid.js +25 -0
  68. package/dist/src/aic/unsupported.d.ts +15 -0
  69. package/dist/src/aic/unsupported.js +60 -0
  70. package/dist/src/bindings/allowlist.d.ts +42 -0
  71. package/dist/src/bindings/allowlist.js +62 -0
  72. package/dist/src/bindings/harvest.d.ts +7 -0
  73. package/dist/src/bindings/harvest.js +137 -0
  74. package/dist/src/bindings/index.d.ts +4 -0
  75. package/dist/src/bindings/index.js +3 -0
  76. package/dist/src/bindings/preamble.d.ts +23 -0
  77. package/dist/src/bindings/preamble.js +36 -0
  78. package/dist/src/bindings/run.d.ts +33 -0
  79. package/dist/src/bindings/run.js +55 -0
  80. package/dist/src/case/equal.d.ts +2 -0
  81. package/dist/src/case/equal.js +40 -0
  82. package/dist/src/case/index.d.ts +5 -0
  83. package/dist/src/case/index.js +4 -0
  84. package/dist/src/case/portable.d.ts +10 -0
  85. package/dist/src/case/portable.js +28 -0
  86. package/dist/src/case/session.d.ts +34 -0
  87. package/dist/src/case/session.js +66 -0
  88. package/dist/src/case/state.d.ts +4 -0
  89. package/dist/src/case/state.js +47 -0
  90. package/dist/src/case/types.d.ts +266 -0
  91. package/dist/src/case/types.js +106 -0
  92. package/dist/src/case/util.d.ts +7 -0
  93. package/dist/src/case/util.js +115 -0
  94. package/dist/src/case/validate.d.ts +8 -0
  95. package/dist/src/case/validate.js +463 -0
  96. package/dist/src/case/verdict.d.ts +6 -0
  97. package/dist/src/case/verdict.js +810 -0
  98. package/dist/src/diagnostics.d.ts +7 -0
  99. package/dist/src/diagnostics.js +5 -0
  100. package/dist/src/emit-dts.d.ts +2 -0
  101. package/dist/src/emit-dts.js +82 -0
  102. package/dist/src/emit-js.d.ts +3 -0
  103. package/dist/src/emit-js.js +149 -0
  104. package/dist/src/harness/failures.d.ts +26 -0
  105. package/dist/src/harness/failures.js +113 -0
  106. package/dist/src/harness/idm.d.ts +12 -0
  107. package/dist/src/harness/idm.js +44 -0
  108. package/dist/src/harness/index.d.ts +11 -0
  109. package/dist/src/harness/index.js +6 -0
  110. package/dist/src/harness/lease.d.ts +136 -0
  111. package/dist/src/harness/lease.js +286 -0
  112. package/dist/src/harness/residue.d.ts +23 -0
  113. package/dist/src/harness/residue.js +73 -0
  114. package/dist/src/harness/show-log.d.ts +43 -0
  115. package/dist/src/harness/show-log.js +253 -0
  116. package/dist/src/harness/spec.d.ts +67 -0
  117. package/dist/src/harness/spec.js +189 -0
  118. package/dist/src/harness/step.d.ts +80 -0
  119. package/dist/src/harness/step.js +97 -0
  120. package/dist/src/harness/types.d.ts +100 -0
  121. package/dist/src/harness/types.js +1 -0
  122. package/dist/src/harness/vitest.d.ts +52 -0
  123. package/dist/src/harness/vitest.js +155 -0
  124. package/dist/src/idents.d.ts +4 -0
  125. package/dist/src/idents.js +42 -0
  126. package/dist/src/jvm.d.ts +124 -0
  127. package/dist/src/jvm.js +432 -0
  128. package/dist/src/load.d.ts +2 -0
  129. package/dist/src/load.js +104 -0
  130. package/dist/src/paths.d.ts +36 -0
  131. package/dist/src/paths.js +50 -0
  132. package/dist/src/profile/index.d.ts +5 -0
  133. package/dist/src/profile/index.js +3 -0
  134. package/dist/src/profile/normalise.d.ts +16 -0
  135. package/dist/src/profile/normalise.js +106 -0
  136. package/dist/src/profile/pull.d.ts +22 -0
  137. package/dist/src/profile/pull.js +36 -0
  138. package/dist/src/profile/seed.d.ts +11 -0
  139. package/dist/src/profile/seed.js +28 -0
  140. package/dist/src/profile/store.d.ts +13 -0
  141. package/dist/src/profile/store.js +60 -0
  142. package/dist/src/profile/types.d.ts +45 -0
  143. package/dist/src/profile/types.js +1 -0
  144. package/dist/src/profile/validate.d.ts +38 -0
  145. package/dist/src/profile/validate.js +129 -0
  146. package/dist/src/project.d.ts +37 -0
  147. package/dist/src/project.js +93 -0
  148. package/dist/src/protocol.d.ts +62 -0
  149. package/dist/src/protocol.js +68 -0
  150. package/dist/src/provider-module.d.ts +11 -0
  151. package/dist/src/provider-module.js +46 -0
  152. package/dist/src/runner.d.ts +39 -0
  153. package/dist/src/runner.js +337 -0
  154. package/dist/src/schema.d.ts +48 -0
  155. package/dist/src/schema.js +93 -0
  156. package/generated/scripted-decision-mocks.cjs +722 -0
  157. package/generated/scripted-decision-mocks.d.ts +626 -0
  158. package/generated/scripted-decision-next.json +3640 -0
  159. package/java/AmClassShutter.java +98 -0
  160. package/java/AmContextFactory.java +69 -0
  161. package/java/AmScopes.java +49 -0
  162. package/java/HostOps.java +180 -0
  163. package/java/JsValues.java +39 -0
  164. package/java/Json.java +441 -0
  165. package/java/LanguageVersions.java +55 -0
  166. package/java/Runner.java +289 -0
  167. package/java/ScriptContextScope.java +147 -0
  168. package/package.json +88 -0
  169. package/src/bindings/rhino/runtime.cjs +3021 -0
@@ -0,0 +1,7 @@
1
+ /** Failure records and log retrieval: what `aic-script-tester-show-log` is made of. */
2
+ export { readFailures, sortNewestFirst } from "./harness/failures.ts";
3
+ export type { FailureRecord } from "./harness/failures.ts";
4
+ export { createDefaultIo, runShowLog } from "./harness/show-log.ts";
5
+ export type { ShowLogIo, ShowLogOptions } from "./harness/show-log.ts";
6
+ export { loadProviderModule } from "./provider-module.ts";
7
+ export { projectRoot, stateDir, PROJECT_ENV, STATE_DIR_ENV } from "./project.ts";
@@ -0,0 +1,5 @@
1
+ /** Failure records and log retrieval: what `aic-script-tester-show-log` is made of. */
2
+ export { readFailures, sortNewestFirst } from "./harness/failures.js";
3
+ export { createDefaultIo, runShowLog } from "./harness/show-log.js";
4
+ export { loadProviderModule } from "./provider-module.js";
5
+ export { projectRoot, stateDir, PROJECT_ENV, STATE_DIR_ENV } from "./project.js";
@@ -0,0 +1,2 @@
1
+ import type { ContextsDocument } from "./schema.ts";
2
+ export declare function emitDts(doc: ContextsDocument): string;
@@ -0,0 +1,82 @@
1
+ import { assertIdent, pascal, tsParamName, tsType } from "./idents.js";
2
+ import { classifyBinding, membersOf } from "./schema.js";
3
+ const HEADER = `\
4
+ // GENERATED from docs/api/bindings/scripted-decision-next.json — do not edit.
5
+ // Re-run: npm -w packages/rhino-local run generate
6
+ //
7
+ // Case-authoring types for the scripted-decision mock surface. javaScriptType
8
+ // values are mapped as-is (object → object, array → unknown[]); they do not
9
+ // include the Rhino Java interop layer in src/scripts/templates/am/types/.
10
+ // Every generated method throws at runtime until a later slice implements it.
11
+ `;
12
+ export function emitDts(doc) {
13
+ const interfaces = [];
14
+ const aggregate = ["export interface ScriptedDecisionMocks {"];
15
+ for (const binding of doc.bindings) {
16
+ const name = assertIdent(binding.name, "binding name");
17
+ const kind = classifyBinding(binding);
18
+ if (kind === "scalar") {
19
+ aggregate.push(` ${name}: ${tsType(binding.javaScriptType)};`);
20
+ continue;
21
+ }
22
+ if (kind === "opaque") {
23
+ aggregate.push(` ${name}: OpaqueBinding;`);
24
+ continue;
25
+ }
26
+ const iface = pascal(name);
27
+ interfaces.push(...emitInterface(iface, binding.elements, iface));
28
+ aggregate.push(` ${name}: ${iface};`);
29
+ }
30
+ aggregate.push("}");
31
+ const opaque = [
32
+ "/**",
33
+ " * Binding whose contexts metadata lists an empty `elements` array.",
34
+ " * A test case seeds this object directly; the generator does not invent",
35
+ " * methods the JSON does not name.",
36
+ " */",
37
+ "export type OpaqueBinding = {",
38
+ " [key: string]: unknown;",
39
+ "};",
40
+ "",
41
+ ];
42
+ return [HEADER, ...opaque, ...interfaces, ...aggregate, ""].join("\n");
43
+ }
44
+ function emitInterface(ifaceName, elements, typePrefix) {
45
+ const members = membersOf(elements);
46
+ const nested = [];
47
+ const body = [`export interface ${ifaceName} {`];
48
+ for (const member of members) {
49
+ const ident = assertIdent(member.name, `member of ${ifaceName}`);
50
+ if (member.kind === "field") {
51
+ const nestedName = `${typePrefix}${pascal(member.name)}`;
52
+ if (member.elements.length === 0) {
53
+ body.push(` ${ident}: OpaqueBinding;`);
54
+ }
55
+ else {
56
+ nested.push(...emitInterface(nestedName, member.elements, nestedName));
57
+ body.push(` ${ident}: ${nestedName};`);
58
+ }
59
+ continue;
60
+ }
61
+ for (const overload of member.overloads) {
62
+ body.push(methodSignature(ident, overload));
63
+ }
64
+ }
65
+ body.push("}", "");
66
+ return [...nested, ...body];
67
+ }
68
+ function methodSignature(name, overload) {
69
+ const params = overload.parameters.map((parameter) => `${tsParamName(parameter.name)}: ${tsType(parameter.javaScriptType)}`);
70
+ const ret = tsType(overload.returnType);
71
+ const oneLine = ` ${name}(${params.join(", ")}): ${ret};`;
72
+ if (oneLine.length <= 80) {
73
+ return oneLine;
74
+ }
75
+ const lines = [` ${name}(`];
76
+ params.forEach((param, index) => {
77
+ const comma = index < params.length - 1 ? "," : "";
78
+ lines.push(` ${param}${comma}`);
79
+ });
80
+ lines.push(` ): ${ret};`);
81
+ return lines.join("\n");
82
+ }
@@ -0,0 +1,3 @@
1
+ import type { ContextsDocument } from "./schema.ts";
2
+ export declare const UNSEEDED_STRING = "__rhino-local-unseeded__";
3
+ export declare function emitJs(doc: ContextsDocument): string;
@@ -0,0 +1,149 @@
1
+ import { assertIdent } from "./idents.js";
2
+ import { classifyBinding, membersOf, overloadLabel } from "./schema.js";
3
+ export const UNSEEDED_STRING = "__rhino-local-unseeded__";
4
+ const HEADER = `\
5
+ // GENERATED from docs/api/bindings/scripted-decision-next.json — do not edit.
6
+ // Re-run: npm -w packages/rhino-local run generate
7
+ //
8
+ // Evaluated by AM's Rhino 1.7.14 as part of the script-under-test's scope.
9
+ // AM-safe JavaScript only: no let, no top-level const, no for...of, no object
10
+ // shorthand, no destructuring, no default parameters, and none of Map / Set /
11
+ // Symbol / Promise / WeakMap / WeakSet. See docs/api/12-script-bindings-matrix.md.
12
+ `;
13
+ const HELPERS = `\
14
+ function __rhinoLocalArgKind(value) {
15
+ if (Array.isArray(value)) {
16
+ return "array";
17
+ }
18
+ var t = typeof value;
19
+ if (t === "string" || t === "number" || t === "boolean") {
20
+ return t;
21
+ }
22
+ return "object";
23
+ }
24
+
25
+ function __rhinoLocalNotMocked(binding, method, args, signatures) {
26
+ var arity = args.length;
27
+ var matched = [];
28
+ var i;
29
+ var j;
30
+ var sig;
31
+ var typesOk;
32
+ for (i = 0; i < signatures.length; i += 1) {
33
+ sig = signatures[i];
34
+ if (sig.arity !== arity) {
35
+ continue;
36
+ }
37
+ typesOk = true;
38
+ for (j = 0; j < arity; j += 1) {
39
+ if (__rhinoLocalArgKind(args[j]) !== sig.types[j]) {
40
+ typesOk = false;
41
+ break;
42
+ }
43
+ }
44
+ if (typesOk) {
45
+ matched.push(sig.label);
46
+ }
47
+ }
48
+ if (matched.length === 0) {
49
+ for (i = 0; i < signatures.length; i += 1) {
50
+ if (signatures[i].arity === arity) {
51
+ matched.push(signatures[i].label);
52
+ }
53
+ }
54
+ }
55
+ if (matched.length === 0) {
56
+ matched.push("no matching signature");
57
+ }
58
+ throw new Error(
59
+ "rhino-local: not mocked: " +
60
+ binding +
61
+ "." +
62
+ method +
63
+ " arity=" +
64
+ arity +
65
+ " overload=[" +
66
+ matched.join(" | ") +
67
+ "]"
68
+ );
69
+ }
70
+ `;
71
+ export function emitJs(doc) {
72
+ const lines = [HEADER, HELPERS];
73
+ for (const binding of doc.bindings) {
74
+ lines.push(emitBinding(binding));
75
+ }
76
+ return lines.join("\n").replace(/\n+$/, "") + "\n";
77
+ }
78
+ function emitBinding(binding) {
79
+ const name = assertIdent(binding.name, "binding name");
80
+ const kind = classifyBinding(binding);
81
+ if (kind === "scalar") {
82
+ return `var ${name} = ${scalarLiteral(binding)};\n`;
83
+ }
84
+ if (kind === "opaque") {
85
+ return (`// Opaque container: the contexts metadata lists no elements. A case\n` +
86
+ `// seeds this object directly; a later slice may replace the assignment.\n` +
87
+ `var ${name} = {};\n`);
88
+ }
89
+ return `var ${name} = ${emitObject(binding.elements, name, 0)};\n`;
90
+ }
91
+ function scalarLiteral(binding) {
92
+ switch (binding.javaScriptType) {
93
+ case "string":
94
+ return JSON.stringify(UNSEEDED_STRING);
95
+ case "boolean":
96
+ return "false";
97
+ case "number":
98
+ return "NaN";
99
+ default:
100
+ throw new Error(`rhino-local: not a scalar javaScriptType: ${binding.javaScriptType}`);
101
+ }
102
+ }
103
+ function emitObject(elements, path, indent) {
104
+ const members = membersOf(elements);
105
+ if (members.length === 0) {
106
+ return "{}";
107
+ }
108
+ const pad = " ".repeat(indent);
109
+ const inner = " ".repeat(indent + 1);
110
+ const parts = ["{"];
111
+ for (const member of members) {
112
+ const ident = assertIdent(member.name, `member of ${path}`);
113
+ if (member.kind === "field") {
114
+ const nestedPath = `${path}.${member.name}`;
115
+ const value = member.elements.length === 0
116
+ ? "{}"
117
+ : emitObject(member.elements, nestedPath, indent + 1);
118
+ parts.push(`${inner}${ident}: ${value},`);
119
+ continue;
120
+ }
121
+ parts.push(`${inner}${ident}: ${emitMethod(path, member.overloads, indent + 1)},`);
122
+ }
123
+ parts.push(`${pad}}`);
124
+ return parts.join("\n");
125
+ }
126
+ function emitMethod(bindingPath, overloads, indent) {
127
+ const first = overloads[0];
128
+ if (!first) {
129
+ throw new Error(`rhino-local: method at ${bindingPath} has no overloads`);
130
+ }
131
+ const bodyPad = " ".repeat(indent + 1);
132
+ const closePad = " ".repeat(indent);
133
+ const sigPad = " ".repeat(indent + 2);
134
+ const sigs = overloads.map((overload) => {
135
+ const arity = overload.parameters.length;
136
+ const types = JSON.stringify(overload.parameters.map((parameter) => parameter.javaScriptType));
137
+ const label = JSON.stringify(overloadLabel(overload));
138
+ return `{ arity: ${arity}, types: ${types}, label: ${label} }`;
139
+ });
140
+ const firstSig = sigs[0];
141
+ const sigBlock = sigs.length === 1 && firstSig
142
+ ? `[${firstSig}]`
143
+ : `[\n${sigs.map((sig) => `${sigPad}${sig},`).join("\n")}\n${bodyPad}]`;
144
+ return [
145
+ "function () {",
146
+ `${bodyPad}__rhinoLocalNotMocked(${JSON.stringify(bindingPath)}, ${JSON.stringify(first.name)}, arguments, ${sigBlock});`,
147
+ `${closePad}}`,
148
+ ].join("\n");
149
+ }
@@ -0,0 +1,26 @@
1
+ import type { AicTrace } from "../aic/trace.ts";
2
+ export interface FailureRecord {
3
+ testName: string;
4
+ stem: string;
5
+ passIds: string[];
6
+ file: string;
7
+ suite: string;
8
+ timestamp: string;
9
+ tenant: string;
10
+ }
11
+ /**
12
+ * A local-only failure has no transaction id, and must not produce a record
13
+ * that would send `show-log` off to fetch nothing.
14
+ */
15
+ export declare function failureRecordFor(input: {
16
+ trace: AicTrace | undefined;
17
+ testName: string;
18
+ file: string;
19
+ suite: string;
20
+ timestamp: string;
21
+ }): FailureRecord | undefined;
22
+ export declare function appendFailure(record: FailureRecord, path?: string): Promise<void>;
23
+ export declare function recordFailureIfAny(input: Parameters<typeof failureRecordFor>[0], path?: string): Promise<FailureRecord | undefined>;
24
+ export declare function readFailures(path?: string): Promise<FailureRecord[]>;
25
+ export declare function parseFailures(text: string): FailureRecord[];
26
+ export declare function sortNewestFirst(records: readonly FailureRecord[]): FailureRecord[];
@@ -0,0 +1,113 @@
1
+ import { appendFile, chmod, mkdir, readFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { isPlainObject } from "../case/util.js";
4
+ import { failuresPath } from "../project.js";
5
+ /**
6
+ * A local-only failure has no transaction id, and must not produce a record
7
+ * that would send `show-log` off to fetch nothing.
8
+ */
9
+ export function failureRecordFor(input) {
10
+ const trace = input.trace;
11
+ if (trace === undefined || trace.stem.length === 0 || trace.passIds.length === 0) {
12
+ return undefined;
13
+ }
14
+ return {
15
+ testName: input.testName,
16
+ stem: trace.stem,
17
+ passIds: [...trace.passIds],
18
+ file: input.file,
19
+ suite: input.suite,
20
+ timestamp: input.timestamp,
21
+ tenant: trace.tenantName,
22
+ };
23
+ }
24
+ export async function appendFailure(record, path = failuresPath({ create: true })) {
25
+ await mkdir(dirname(path), { recursive: true });
26
+ await appendFile(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
27
+ // `mode` applies only when the append creates the file.
28
+ await chmod(path, 0o600);
29
+ }
30
+ export async function recordFailureIfAny(input, path) {
31
+ const record = failureRecordFor(input);
32
+ if (record === undefined) {
33
+ return undefined;
34
+ }
35
+ await appendFailure(record, path ?? failuresPath({ create: true }));
36
+ return record;
37
+ }
38
+ export async function readFailures(path = failuresPath()) {
39
+ let text;
40
+ try {
41
+ text = await readFile(path, "utf8");
42
+ }
43
+ catch (error) {
44
+ if (isEnoent(error)) {
45
+ return [];
46
+ }
47
+ throw error;
48
+ }
49
+ return parseFailures(text);
50
+ }
51
+ export function parseFailures(text) {
52
+ const records = [];
53
+ for (const line of text.split(/\r?\n/)) {
54
+ if (line.trim() === "") {
55
+ continue;
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(line);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ const record = asFailureRecord(parsed);
65
+ if (record !== undefined) {
66
+ records.push(record);
67
+ }
68
+ }
69
+ return records;
70
+ }
71
+ export function sortNewestFirst(records) {
72
+ return records
73
+ .map((record, index) => ({ record, index }))
74
+ .sort((a, b) => {
75
+ const time = b.record.timestamp.localeCompare(a.record.timestamp);
76
+ return time !== 0 ? time : b.index - a.index;
77
+ })
78
+ .map((row) => row.record);
79
+ }
80
+ function asFailureRecord(value) {
81
+ if (!isPlainObject(value)) {
82
+ return undefined;
83
+ }
84
+ if (typeof value.testName !== "string" ||
85
+ typeof value.stem !== "string" ||
86
+ value.stem.length === 0 ||
87
+ !isStringArray(value.passIds) ||
88
+ value.passIds.length === 0 ||
89
+ typeof value.file !== "string" ||
90
+ typeof value.suite !== "string" ||
91
+ typeof value.timestamp !== "string" ||
92
+ typeof value.tenant !== "string") {
93
+ return undefined;
94
+ }
95
+ return {
96
+ testName: value.testName,
97
+ stem: value.stem,
98
+ passIds: value.passIds,
99
+ file: value.file,
100
+ suite: value.suite,
101
+ timestamp: value.timestamp,
102
+ tenant: value.tenant,
103
+ };
104
+ }
105
+ function isStringArray(value) {
106
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
107
+ }
108
+ function isEnoent(error) {
109
+ return (typeof error === "object" &&
110
+ error !== null &&
111
+ "code" in error &&
112
+ error.code === "ENOENT");
113
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Local `check()`/`cleanup()` handle and shared managed-path split. The tenant
3
+ * counterpart in `aic/idm.ts` imports only that path rule across the seam.
4
+ */
5
+ import type { JsonObject } from "../case/types.ts";
6
+ import type { FixtureSpec, IdmHandle } from "./types.ts";
7
+ /** Local handle over one run's harvested managed store. */
8
+ export declare function localIdmHandle(store: Record<string, JsonObject[]>, onDelete: (resource: string) => void): IdmHandle;
9
+ /** `managed/alpha_user/alice` → [`managed/alpha_user`, `alice`]. */
10
+ export declare function splitResource(resource: string): [string, string];
11
+ /** Collapse a ledger into the `given.managed` shape the local lane seeds. */
12
+ export declare function ledgerToManaged(ledger: readonly FixtureSpec[]): Record<string, JsonObject[]>;
@@ -0,0 +1,44 @@
1
+ /** Local handle over one run's harvested managed store. */
2
+ export function localIdmHandle(store, onDelete) {
3
+ return {
4
+ read(resource) {
5
+ const [collection, id] = splitResource(resource);
6
+ const row = (store[collection] ?? []).find((entry) => entry._id === id);
7
+ return Promise.resolve(row ?? null);
8
+ },
9
+ query(type, filter) {
10
+ const rows = (store[type] ?? []).filter((row) => Object.entries(filter).every((entry) => JSON.stringify(row[entry[0]]) === JSON.stringify(entry[1])));
11
+ return Promise.resolve(rows);
12
+ },
13
+ delete(resource) {
14
+ const [collection, id] = splitResource(resource);
15
+ const rows = store[collection];
16
+ if (rows !== undefined) {
17
+ const index = rows.findIndex((entry) => entry._id === id);
18
+ if (index >= 0) {
19
+ rows.splice(index, 1);
20
+ }
21
+ }
22
+ onDelete(resource);
23
+ return Promise.resolve();
24
+ },
25
+ };
26
+ }
27
+ /** `managed/alpha_user/alice` → [`managed/alpha_user`, `alice`]. */
28
+ export function splitResource(resource) {
29
+ const cut = resource.lastIndexOf("/");
30
+ if (cut <= 0 || cut === resource.length - 1) {
31
+ throw new Error(`rhino-local: ${JSON.stringify(resource)} is not a managed record path (expected managed/<type>/<id>)`);
32
+ }
33
+ return [resource.slice(0, cut), resource.slice(cut + 1)];
34
+ }
35
+ /** Collapse a ledger into the `given.managed` shape the local lane seeds. */
36
+ export function ledgerToManaged(ledger) {
37
+ const managed = {};
38
+ for (const fixture of ledger) {
39
+ const rows = managed[fixture.type] ?? [];
40
+ rows.push(fixture.record);
41
+ managed[fixture.type] = rows;
42
+ }
43
+ return managed;
44
+ }
@@ -0,0 +1,11 @@
1
+ export { defineSuite, Lease, managed, RunBuilder } from "./lease.ts";
2
+ export type { LeaseOptions, LeaseLane, LeaseLaneRunRequest, RunResult, StepResult, Suite, CheckContext, Check, LeaseLaneCheck, LeaseLaneHooks, } from "./lease.ts";
3
+ export { carryGiven, submittedCallbacks } from "./step.ts";
4
+ export type { CallbackReply, StepContext, StepExpect, StepSpec } from "./step.ts";
5
+ export { aicWhenEnabled, AIC_LANE_ENV, useLease } from "./vitest.ts";
6
+ export type { UseLeaseAicOptions, UseLeaseOptions } from "./vitest.ts";
7
+ export { localIdmHandle, ledgerToManaged, splitResource } from "./idm.ts";
8
+ export { describeResidue, findResidue } from "./residue.ts";
9
+ export type { ResidueEntry } from "./residue.ts";
10
+ export { applyInputsAndEsv, caseWithGiven, ESV_STATE_PREFIX, mergeChannels, normaliseWire, parseInputs, toCase, toGiven, } from "./spec.ts";
11
+ export type * from "./types.ts";
@@ -0,0 +1,6 @@
1
+ export { defineSuite, Lease, managed, RunBuilder } from "./lease.js";
2
+ export { carryGiven, submittedCallbacks } from "./step.js";
3
+ export { aicWhenEnabled, AIC_LANE_ENV, useLease } from "./vitest.js";
4
+ export { localIdmHandle, ledgerToManaged, splitResource } from "./idm.js";
5
+ export { describeResidue, findResidue } from "./residue.js";
6
+ export { applyInputsAndEsv, caseWithGiven, ESV_STATE_PREFIX, mergeChannels, normaliseWire, parseInputs, toCase, toGiven, } from "./spec.js";
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Framework-free local lease and the narrow lane port used by AIC. Tenant
3
+ * lifecycle and I/O stay in `aic/file-lease.ts`; this module hands the
4
+ * recorded local run and its bound check/cleanup hooks across that boundary.
5
+ */
6
+ import type { z } from "zod";
7
+ import type { ChainConformanceReport } from "../aic/conform.ts";
8
+ import type { Case, CallbackEffect, Expect, JsonObject, RecordedEffects, Verdict } from "../case/types.ts";
9
+ import type { RhinoRunner } from "../runner.ts";
10
+ import { normaliseWire } from "./spec.ts";
11
+ import type { StepSpec } from "./step.ts";
12
+ import type { Channels, FixtureSpec, IdmHandle, RequestDraft, SuiteSpec, WireMap } from "./types.ts";
13
+ export interface RunResult {
14
+ kase: Case;
15
+ effects: RecordedEffects;
16
+ verdict: Verdict;
17
+ /** Fixture provenance for the AIC lane; author-declared managed data has none. */
18
+ fixtures: readonly FixtureSpec[];
19
+ /**
20
+ * One entry per suspended pass, in order. Empty for a single-pass run.
21
+ * `aic/chainFromRunResult` consumes these recorded cases and submissions;
22
+ * the AIC lane must not reconstruct them from the final case.
23
+ */
24
+ steps: StepResult[];
25
+ /** Present when this lease automatically checked the completed run on AIC. */
26
+ conformance?: ChainConformanceReport;
27
+ }
28
+ /** One pass of a step chain: what it was asked, what it did, what was sent back. */
29
+ export interface StepResult {
30
+ kase: Case;
31
+ effects: RecordedEffects;
32
+ verdict: Verdict;
33
+ /** Exactly what the next pass was handed as `given.callbacks`. */
34
+ submitted: CallbackEffect[];
35
+ }
36
+ export interface CheckContext<TInput> {
37
+ input: TInput;
38
+ effects: RecordedEffects;
39
+ }
40
+ export interface LeaseOptions {
41
+ runner: RhinoRunner;
42
+ timeoutMs?: number;
43
+ /** A test that deliberately leaves a record behind and asserts on it. */
44
+ allowResidue?: boolean;
45
+ /**
46
+ * How to name the run in failure messages. Supplied by the vitest adapter
47
+ * so this module stays framework-free — the residue check and the merge
48
+ * logic are worth unit-testing without a test runner in the way.
49
+ */
50
+ testName?: () => string;
51
+ /** Fixed tenant realm; the Vitest AIC adapter supplies it to both lanes. */
52
+ realm?: string;
53
+ /** Cross-feature port implemented by the tenant-aware AIC vertical. */
54
+ lane?: LeaseLane;
55
+ }
56
+ export interface LeaseLane {
57
+ run(request: LeaseLaneRunRequest): Promise<ChainConformanceReport>;
58
+ endTest(): Promise<void>;
59
+ }
60
+ export interface LeaseLaneRunRequest {
61
+ result: RunResult;
62
+ source: string;
63
+ hooks: LeaseLaneHooks;
64
+ }
65
+ export type Check<TInput> = (idm: IdmHandle, ctx: CheckContext<TInput>) => void | Promise<void>;
66
+ export type LeaseLaneCheck = (idm: IdmHandle, effects: RecordedEffects) => void | Promise<void>;
67
+ /** Hooks bound to parsed input but replayed with each lane's own effects/IDM. */
68
+ export interface LeaseLaneHooks {
69
+ stepChecks: readonly (LeaseLaneCheck | undefined)[];
70
+ finalChecks: readonly LeaseLaneCheck[];
71
+ cleanup?: (idm: IdmHandle) => void | Promise<void>;
72
+ }
73
+ /**
74
+ * One test's run, assembled lazily.
75
+ *
76
+ * Deliberately NOT a thenable. A builder that is also a promise invites an
77
+ * `await` halfway through the chain, after which `.check()` is appending to a
78
+ * run that has already settled — a bug that reads as working code. Making
79
+ * `.expect()` the only terminal turns that whole class into a type error.
80
+ */
81
+ export declare class RunBuilder<TInput> {
82
+ #private;
83
+ constructor(lease: Lease<z.ZodType>, testName: string, rawInput: unknown);
84
+ state(state: Channels["state"]): this;
85
+ esv(esv: Readonly<Record<string, string>>): this;
86
+ headers(headers: WireMap): this;
87
+ params(params: WireMap): this;
88
+ session(session: JsonObject): this;
89
+ /**
90
+ * Declare one suspended pass: what the script must send, what the client
91
+ * sends back, and what must be true of the world in between.
92
+ *
93
+ * Passes run in declaration order and the terminal `.expect()` judges the
94
+ * one after the last step, so a two-callback journey is two `.step()` calls
95
+ * and one `.expect()`. A step whose expectations fail aborts the chain
96
+ * rather than replying anyway: every later pass is seeded from this one, so
97
+ * continuing would report a cascade of failures that all trace back here.
98
+ */
99
+ step(spec: StepSpec<TInput>): this;
100
+ /** The same, for a chain built from data. */
101
+ steps(specs: readonly StepSpec<TInput>[]): this;
102
+ /** Assert against each lane's IDM store and response. Fail by throwing. */
103
+ check(fn: Check<TInput>): this;
104
+ /** The only terminal. */
105
+ expect(expected: Expect): Promise<RunResult>;
106
+ }
107
+ export declare class Lease<TSchema extends z.ZodType> {
108
+ #private;
109
+ constructor(spec: SuiteSpec<TSchema>, options: LeaseOptions);
110
+ /** Suite-scoped fixtures, created once before the first test. */
111
+ open(): void;
112
+ readonly fixtures: {
113
+ create: (type: string, record: JsonObject | JsonObject[]) => Promise<void>;
114
+ };
115
+ run(input?: z.input<TSchema>): RunBuilder<z.output<TSchema>>;
116
+ /**
117
+ * Drained after every test. The per-test ledger goes; the suite's stays
118
+ * until close(). Clearing only at teardown would let test 3 see test 1's
119
+ * records, which is the cross-test interference this design exists to
120
+ * remove.
121
+ */
122
+ endTest(): Promise<void>;
123
+ /** Suite teardown: drop what the suite created, alongside the journey. */
124
+ close(): Promise<void>;
125
+ ledger(): readonly FixtureSpec[];
126
+ execute(testName: string, rawInput: unknown, override: Channels, expected: Expect, checks: readonly Check<unknown>[], steps?: readonly StepSpec<unknown>[]): Promise<RunResult>;
127
+ }
128
+ export interface Suite<TSchema extends z.ZodType> {
129
+ spec: SuiteSpec<TSchema>;
130
+ lease(options: LeaseOptions): Lease<TSchema>;
131
+ }
132
+ export declare function defineSuite<TSchema extends z.ZodType>(spec: SuiteSpec<TSchema>): Suite<TSchema>;
133
+ /** A managed record the suite creates once, for the whole file. */
134
+ export declare function managed(type: string, record: JsonObject): FixtureSpec;
135
+ export { normaliseWire };
136
+ export type { RequestDraft, SuiteSpec };