@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.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +274 -0
- package/dist/bin/fetch-jar.d.ts +2 -0
- package/dist/bin/fetch-jar.js +13 -0
- package/dist/bin/pull-profile.d.ts +2 -0
- package/dist/bin/pull-profile.js +40 -0
- package/dist/bin/show-log.d.ts +2 -0
- package/dist/bin/show-log.js +31 -0
- package/dist/classes/.complete +12 -0
- package/dist/classes/AmClassShutter.class +0 -0
- package/dist/classes/AmContextFactory$ObservedContext.class +0 -0
- package/dist/classes/AmContextFactory.class +0 -0
- package/dist/classes/AmScopes.class +0 -0
- package/dist/classes/HostOps.class +0 -0
- package/dist/classes/JsValues.class +0 -0
- package/dist/classes/Json$Parser.class +0 -0
- package/dist/classes/Json.class +0 -0
- package/dist/classes/LanguageVersions.class +0 -0
- package/dist/classes/Runner.class +0 -0
- package/dist/classes/ScriptContextScope.class +0 -0
- package/dist/src/aic/callbacks.d.ts +35 -0
- package/dist/src/aic/callbacks.js +130 -0
- package/dist/src/aic/conform.d.ts +90 -0
- package/dist/src/aic/conform.js +193 -0
- package/dist/src/aic/constants.d.ts +13 -0
- package/dist/src/aic/constants.js +13 -0
- package/dist/src/aic/diff.d.ts +25 -0
- package/dist/src/aic/diff.js +201 -0
- package/dist/src/aic/emit-journey.d.ts +64 -0
- package/dist/src/aic/emit-journey.js +254 -0
- package/dist/src/aic/emit-result.d.ts +10 -0
- package/dist/src/aic/emit-result.js +39 -0
- package/dist/src/aic/emit-session.d.ts +26 -0
- package/dist/src/aic/emit-session.js +132 -0
- package/dist/src/aic/emit-subject.d.ts +40 -0
- package/dist/src/aic/emit-subject.js +210 -0
- package/dist/src/aic/file-lease.d.ts +35 -0
- package/dist/src/aic/file-lease.js +543 -0
- package/dist/src/aic/http.d.ts +21 -0
- package/dist/src/aic/http.js +127 -0
- package/dist/src/aic/idm.d.ts +10 -0
- package/dist/src/aic/idm.js +112 -0
- package/dist/src/aic/index.d.ts +35 -0
- package/dist/src/aic/index.js +21 -0
- package/dist/src/aic/js.d.ts +8 -0
- package/dist/src/aic/js.js +11 -0
- package/dist/src/aic/lease-identity.d.ts +38 -0
- package/dist/src/aic/lease-identity.js +72 -0
- package/dist/src/aic/lease-lock.d.ts +45 -0
- package/dist/src/aic/lease-lock.js +173 -0
- package/dist/src/aic/managed.d.ts +28 -0
- package/dist/src/aic/managed.js +199 -0
- package/dist/src/aic/provider.d.ts +156 -0
- package/dist/src/aic/provider.js +416 -0
- package/dist/src/aic/record.d.ts +28 -0
- package/dist/src/aic/record.js +183 -0
- package/dist/src/aic/resource-snapshot.d.ts +12 -0
- package/dist/src/aic/resource-snapshot.js +116 -0
- package/dist/src/aic/run.d.ts +102 -0
- package/dist/src/aic/run.js +450 -0
- package/dist/src/aic/tenant.d.ts +107 -0
- package/dist/src/aic/tenant.js +307 -0
- package/dist/src/aic/trace.d.ts +25 -0
- package/dist/src/aic/trace.js +56 -0
- package/dist/src/aic/txid.d.ts +11 -0
- package/dist/src/aic/txid.js +25 -0
- package/dist/src/aic/unsupported.d.ts +15 -0
- package/dist/src/aic/unsupported.js +60 -0
- package/dist/src/bindings/allowlist.d.ts +42 -0
- package/dist/src/bindings/allowlist.js +62 -0
- package/dist/src/bindings/harvest.d.ts +7 -0
- package/dist/src/bindings/harvest.js +137 -0
- package/dist/src/bindings/index.d.ts +4 -0
- package/dist/src/bindings/index.js +3 -0
- package/dist/src/bindings/preamble.d.ts +23 -0
- package/dist/src/bindings/preamble.js +36 -0
- package/dist/src/bindings/run.d.ts +33 -0
- package/dist/src/bindings/run.js +55 -0
- package/dist/src/case/equal.d.ts +2 -0
- package/dist/src/case/equal.js +40 -0
- package/dist/src/case/index.d.ts +5 -0
- package/dist/src/case/index.js +4 -0
- package/dist/src/case/portable.d.ts +10 -0
- package/dist/src/case/portable.js +28 -0
- package/dist/src/case/session.d.ts +34 -0
- package/dist/src/case/session.js +66 -0
- package/dist/src/case/state.d.ts +4 -0
- package/dist/src/case/state.js +47 -0
- package/dist/src/case/types.d.ts +266 -0
- package/dist/src/case/types.js +106 -0
- package/dist/src/case/util.d.ts +7 -0
- package/dist/src/case/util.js +115 -0
- package/dist/src/case/validate.d.ts +8 -0
- package/dist/src/case/validate.js +463 -0
- package/dist/src/case/verdict.d.ts +6 -0
- package/dist/src/case/verdict.js +810 -0
- package/dist/src/diagnostics.d.ts +7 -0
- package/dist/src/diagnostics.js +5 -0
- package/dist/src/emit-dts.d.ts +2 -0
- package/dist/src/emit-dts.js +82 -0
- package/dist/src/emit-js.d.ts +3 -0
- package/dist/src/emit-js.js +149 -0
- package/dist/src/harness/failures.d.ts +26 -0
- package/dist/src/harness/failures.js +113 -0
- package/dist/src/harness/idm.d.ts +12 -0
- package/dist/src/harness/idm.js +44 -0
- package/dist/src/harness/index.d.ts +11 -0
- package/dist/src/harness/index.js +6 -0
- package/dist/src/harness/lease.d.ts +136 -0
- package/dist/src/harness/lease.js +286 -0
- package/dist/src/harness/residue.d.ts +23 -0
- package/dist/src/harness/residue.js +73 -0
- package/dist/src/harness/show-log.d.ts +43 -0
- package/dist/src/harness/show-log.js +253 -0
- package/dist/src/harness/spec.d.ts +67 -0
- package/dist/src/harness/spec.js +189 -0
- package/dist/src/harness/step.d.ts +80 -0
- package/dist/src/harness/step.js +97 -0
- package/dist/src/harness/types.d.ts +100 -0
- package/dist/src/harness/types.js +1 -0
- package/dist/src/harness/vitest.d.ts +52 -0
- package/dist/src/harness/vitest.js +155 -0
- package/dist/src/idents.d.ts +4 -0
- package/dist/src/idents.js +42 -0
- package/dist/src/jvm.d.ts +124 -0
- package/dist/src/jvm.js +432 -0
- package/dist/src/load.d.ts +2 -0
- package/dist/src/load.js +104 -0
- package/dist/src/paths.d.ts +36 -0
- package/dist/src/paths.js +50 -0
- package/dist/src/profile/index.d.ts +5 -0
- package/dist/src/profile/index.js +3 -0
- package/dist/src/profile/normalise.d.ts +16 -0
- package/dist/src/profile/normalise.js +106 -0
- package/dist/src/profile/pull.d.ts +22 -0
- package/dist/src/profile/pull.js +36 -0
- package/dist/src/profile/seed.d.ts +11 -0
- package/dist/src/profile/seed.js +28 -0
- package/dist/src/profile/store.d.ts +13 -0
- package/dist/src/profile/store.js +60 -0
- package/dist/src/profile/types.d.ts +45 -0
- package/dist/src/profile/types.js +1 -0
- package/dist/src/profile/validate.d.ts +38 -0
- package/dist/src/profile/validate.js +129 -0
- package/dist/src/project.d.ts +37 -0
- package/dist/src/project.js +93 -0
- package/dist/src/protocol.d.ts +62 -0
- package/dist/src/protocol.js +68 -0
- package/dist/src/provider-module.d.ts +11 -0
- package/dist/src/provider-module.js +46 -0
- package/dist/src/runner.d.ts +39 -0
- package/dist/src/runner.js +337 -0
- package/dist/src/schema.d.ts +48 -0
- package/dist/src/schema.js +93 -0
- package/generated/scripted-decision-mocks.cjs +722 -0
- package/generated/scripted-decision-mocks.d.ts +626 -0
- package/generated/scripted-decision-next.json +3640 -0
- package/java/AmClassShutter.java +98 -0
- package/java/AmContextFactory.java +69 -0
- package/java/AmScopes.java +49 -0
- package/java/HostOps.java +180 -0
- package/java/JsValues.java +39 -0
- package/java/Json.java +441 -0
- package/java/LanguageVersions.java +55 -0
- package/java/Runner.java +289 -0
- package/java/ScriptContextScope.java +147 -0
- package/package.json +88 -0
- package/src/bindings/rhino/runtime.cjs +3021 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AM_OWNED_SESSION_SET, DEFAULT_SESSION_PRINCIPAL, } from "../case/session.js";
|
|
3
|
+
import { RESOURCE_PREFIX, SUCCESS_NODE_ID } from "./constants.js";
|
|
4
|
+
import { jsStringLiteral } from "./js.js";
|
|
5
|
+
/**
|
|
6
|
+
* A journey whose only job is to end, so that AM issues a session.
|
|
7
|
+
*
|
|
8
|
+
* There is no other way: a session exists only once a journey has run to
|
|
9
|
+
* completion (`docs/api/09-journeys.md` → "Minting a session for a test").
|
|
10
|
+
* Measured 2026-09-14 — the tree needs no Identify Existing User node, the
|
|
11
|
+
* principal need not exist as a managed object, and the journey that later
|
|
12
|
+
* *consumes* the session needs no configuration at all.
|
|
13
|
+
*
|
|
14
|
+
* AM-owned properties are dropped rather than sent. `putSessionProperty` does
|
|
15
|
+
* not override them; the whole login fails with a bare `401 Unauthorized`
|
|
16
|
+
* naming nothing, so sending one would turn a harness bug into an unexplained
|
|
17
|
+
* authentication failure. The five AM derives from the principal arrive by
|
|
18
|
+
* authenticating AS that principal instead.
|
|
19
|
+
*/
|
|
20
|
+
export function emitSessionJourney(session, options) {
|
|
21
|
+
const principal = session.UserId ?? DEFAULT_SESSION_PRINCIPAL;
|
|
22
|
+
const custom = Object.entries(session).filter(([key]) => !AM_OWNED_SESSION_SET.has(key));
|
|
23
|
+
const treeName = `${RESOURCE_PREFIX}-${options.runId}-session`;
|
|
24
|
+
const lines = [
|
|
25
|
+
"// Generated by rhino-local AIC lane: mints a session for the subject",
|
|
26
|
+
"// journey, which cannot seed `existingSession` any other way.",
|
|
27
|
+
`nodeState.putShared("username", ${jsStringLiteral(principal)});`,
|
|
28
|
+
'action.goTo("true")',
|
|
29
|
+
];
|
|
30
|
+
for (const [key, value] of custom) {
|
|
31
|
+
lines.push(` .putSessionProperty(${jsStringLiteral(key)}, ${jsStringLiteral(value)})`);
|
|
32
|
+
}
|
|
33
|
+
lines.push(";", "");
|
|
34
|
+
const script = {
|
|
35
|
+
id: randomUUID(),
|
|
36
|
+
name: `${treeName}-mint`,
|
|
37
|
+
source: lines.join("\n"),
|
|
38
|
+
role: "subject",
|
|
39
|
+
};
|
|
40
|
+
const node = {
|
|
41
|
+
id: randomUUID(),
|
|
42
|
+
displayName: "mint session",
|
|
43
|
+
scriptId: script.id,
|
|
44
|
+
outcomes: ["true"],
|
|
45
|
+
connections: { true: SUCCESS_NODE_ID },
|
|
46
|
+
x: 80,
|
|
47
|
+
y: 100,
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
treeName,
|
|
51
|
+
realm: options.realm,
|
|
52
|
+
identityResource: `managed/${options.realm}_user`,
|
|
53
|
+
scripts: [script],
|
|
54
|
+
nodes: [node],
|
|
55
|
+
treeBody: {
|
|
56
|
+
identityResource: `managed/${options.realm}_user`,
|
|
57
|
+
entryNodeId: node.id,
|
|
58
|
+
innerTreeOnly: false,
|
|
59
|
+
description: "rhino-local AIC lane throwaway. Safe to delete.",
|
|
60
|
+
noSession: false,
|
|
61
|
+
mustRun: false,
|
|
62
|
+
enabled: true,
|
|
63
|
+
transactionalOnly: false,
|
|
64
|
+
uiConfig: { categories: '["Test"]' },
|
|
65
|
+
nodes: {
|
|
66
|
+
[node.id]: {
|
|
67
|
+
connections: node.connections,
|
|
68
|
+
displayName: node.displayName,
|
|
69
|
+
nodeType: "ScriptedDecisionNode",
|
|
70
|
+
version: "1.0",
|
|
71
|
+
x: node.x,
|
|
72
|
+
y: node.y,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
nodeBodies: {
|
|
77
|
+
[node.id]: {
|
|
78
|
+
_id: node.id,
|
|
79
|
+
_type: {
|
|
80
|
+
_id: "ScriptedDecisionNode",
|
|
81
|
+
collection: true,
|
|
82
|
+
name: "Scripted Decision",
|
|
83
|
+
},
|
|
84
|
+
_outcomes: [{ id: "true", displayName: "true" }],
|
|
85
|
+
inputs: ["*"],
|
|
86
|
+
outputs: ["*"],
|
|
87
|
+
outcomes: ["true"],
|
|
88
|
+
script: script.id,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
subjectOutcomes: ["true"],
|
|
92
|
+
invoke: { headers: {}, parameters: {}, cookies: {} },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Deterministic session-minter resources owned by a file lease. */
|
|
96
|
+
export function emitLeasedSessionJourney(session, options) {
|
|
97
|
+
const emitted = emitSessionJourney(session, {
|
|
98
|
+
runId: options.identity.idHash,
|
|
99
|
+
realm: options.realm,
|
|
100
|
+
});
|
|
101
|
+
const script = emitted.scripts[0];
|
|
102
|
+
const node = emitted.nodes[0];
|
|
103
|
+
script.id = options.identity.ids.sessionScript;
|
|
104
|
+
script.name = `${options.identity.ids.sessionTree}-mint`;
|
|
105
|
+
script.source = `// ${options.identity.marker}\n${script.source}`;
|
|
106
|
+
node.id = options.identity.ids.sessionNode;
|
|
107
|
+
node.scriptId = script.id;
|
|
108
|
+
emitted.treeName = options.identity.ids.sessionTree;
|
|
109
|
+
emitted.treeBody = {
|
|
110
|
+
...emitted.treeBody,
|
|
111
|
+
entryNodeId: node.id,
|
|
112
|
+
description: options.identity.marker,
|
|
113
|
+
nodes: {
|
|
114
|
+
[node.id]: {
|
|
115
|
+
connections: node.connections,
|
|
116
|
+
displayName: node.displayName,
|
|
117
|
+
nodeType: "ScriptedDecisionNode",
|
|
118
|
+
x: node.x,
|
|
119
|
+
y: node.y,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
emitted.nodeBodies = {
|
|
124
|
+
[node.id]: {
|
|
125
|
+
inputs: ["*"],
|
|
126
|
+
outputs: ["*"],
|
|
127
|
+
outcomes: node.outcomes,
|
|
128
|
+
script: script.id,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
return emitted;
|
|
132
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Given, JsonObject } from "../case/types.ts";
|
|
2
|
+
export interface InstrumentedSubject {
|
|
3
|
+
source: string;
|
|
4
|
+
bindingName: string;
|
|
5
|
+
snapshotKey: string;
|
|
6
|
+
subjectDigest?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SubjectLeaseProof {
|
|
9
|
+
snapshotKey: string;
|
|
10
|
+
invocationNonce: string;
|
|
11
|
+
header?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Seed `nodeState`, then capture it immediately before and after the author
|
|
15
|
+
* source.
|
|
16
|
+
*
|
|
17
|
+
* The author source stays at top level: wrapping it in a function or block
|
|
18
|
+
* changes AM's already-unusual top-level `const` behaviour. One generated
|
|
19
|
+
* binding is therefore visible to global enumeration. Its identifier and the
|
|
20
|
+
* state key written after the author source are chosen not to occur anywhere
|
|
21
|
+
* in the original source; helper names are private inside the binding's IIFE.
|
|
22
|
+
*
|
|
23
|
+
* The seed lives here rather than in a preceding setup node, and rather than
|
|
24
|
+
* in a query parameter. Measured 2026-09-13: state seeded in the subject, in
|
|
25
|
+
* a preceding node, and with the subject as the entry node all produced
|
|
26
|
+
* byte-identical results, so the extra node bought nothing. A query parameter
|
|
27
|
+
* would have been worse than useless — `requestParameters` is a binding the
|
|
28
|
+
* corpus measures, so harness data sitting in it is observable to the very
|
|
29
|
+
* scripts under test.
|
|
30
|
+
*
|
|
31
|
+
* The seed is applied BEFORE the `before` snapshot, matching what the setup
|
|
32
|
+
* node did: seeded values are the script's starting state, not a mutation it
|
|
33
|
+
* made. It is applied only on the node's FIRST pass — see the guard below.
|
|
34
|
+
*/
|
|
35
|
+
export declare function instrumentSubject(authorSource: string, runId: string, given?: Given, proof?: SubjectLeaseProof): InstrumentedSubject;
|
|
36
|
+
/** JSON objects the subject will seed, for tests and invoke classification. */
|
|
37
|
+
export declare function seededState(given: Given): {
|
|
38
|
+
shared: JsonObject;
|
|
39
|
+
transient: JsonObject;
|
|
40
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsonParseCall, jsStringLiteral } from "./js.js";
|
|
2
|
+
import { sha256 } from "./lease-identity.js";
|
|
3
|
+
const BINDING_PREFIX = "__rhinoLocalHarness_";
|
|
4
|
+
const SNAPSHOT_KEY_PREFIX = "__rhino_local_snapshot_";
|
|
5
|
+
const HELPERS = `\
|
|
6
|
+
function keys(ns) {
|
|
7
|
+
var raw = ns.keys();
|
|
8
|
+
var out = [];
|
|
9
|
+
var i;
|
|
10
|
+
var it;
|
|
11
|
+
var k;
|
|
12
|
+
if (raw === null || raw === undefined) {
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
if (typeof raw.length === "number" && typeof raw !== "string") {
|
|
16
|
+
for (i = 0; i < raw.length; i += 1) {
|
|
17
|
+
out.push(String(raw[i]));
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
if (typeof raw.toArray === "function") {
|
|
22
|
+
raw = raw.toArray();
|
|
23
|
+
for (i = 0; i < raw.length; i += 1) {
|
|
24
|
+
out.push(String(raw[i]));
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
if (typeof raw.iterator === "function") {
|
|
29
|
+
it = raw.iterator();
|
|
30
|
+
while (it.hasNext()) {
|
|
31
|
+
out.push(String(it.next()));
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
if (typeof raw.size === "function" && typeof raw.get === "function" && typeof raw.keySet !== "function") {
|
|
36
|
+
for (i = 0; i < raw.size(); i += 1) {
|
|
37
|
+
out.push(String(raw.get(i)));
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
for (k in raw) {
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(raw, k) && typeof raw[k] !== "function") {
|
|
43
|
+
out.push(String(raw[k]));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function valueToJson(value) {
|
|
50
|
+
var t;
|
|
51
|
+
var i;
|
|
52
|
+
var k;
|
|
53
|
+
var out;
|
|
54
|
+
var mapKeys;
|
|
55
|
+
var arr;
|
|
56
|
+
if (value === null || value === undefined) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
t = typeof value;
|
|
60
|
+
if (t === "string" || t === "number" || t === "boolean") {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
if (typeof Array.isArray === "function" && Array.isArray(value)) {
|
|
64
|
+
out = [];
|
|
65
|
+
for (i = 0; i < value.length; i += 1) {
|
|
66
|
+
out.push(valueToJson(value[i]));
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value.keySet === "function" && typeof value.get === "function") {
|
|
71
|
+
out = {};
|
|
72
|
+
mapKeys = value.keySet();
|
|
73
|
+
if (typeof mapKeys.toArray === "function") {
|
|
74
|
+
arr = mapKeys.toArray();
|
|
75
|
+
for (i = 0; i < arr.length; i += 1) {
|
|
76
|
+
out[String(arr[i])] = valueToJson(value.get(arr[i]));
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (typeof value.size === "function" && typeof value.get === "function") {
|
|
82
|
+
out = [];
|
|
83
|
+
for (i = 0; i < value.size(); i += 1) {
|
|
84
|
+
out.push(valueToJson(value.get(i)));
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
if (t === "object") {
|
|
89
|
+
out = {};
|
|
90
|
+
for (k in value) {
|
|
91
|
+
if (Object.prototype.hasOwnProperty.call(value, k) && typeof value[k] !== "function") {
|
|
92
|
+
out[String(k)] = valueToJson(value[k]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function seed(map, put) {
|
|
101
|
+
var names = Object.keys(map);
|
|
102
|
+
var i;
|
|
103
|
+
for (i = 0; i < names.length; i += 1) {
|
|
104
|
+
put(names[i], map[names[i]]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function snapshot(ns) {
|
|
109
|
+
var names = keys(ns);
|
|
110
|
+
var out = {};
|
|
111
|
+
var i;
|
|
112
|
+
var key;
|
|
113
|
+
for (i = 0; i < names.length; i += 1) {
|
|
114
|
+
key = names[i];
|
|
115
|
+
out[key] = valueToJson(ns.get(key));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
/**
|
|
121
|
+
* Seed `nodeState`, then capture it immediately before and after the author
|
|
122
|
+
* source.
|
|
123
|
+
*
|
|
124
|
+
* The author source stays at top level: wrapping it in a function or block
|
|
125
|
+
* changes AM's already-unusual top-level `const` behaviour. One generated
|
|
126
|
+
* binding is therefore visible to global enumeration. Its identifier and the
|
|
127
|
+
* state key written after the author source are chosen not to occur anywhere
|
|
128
|
+
* in the original source; helper names are private inside the binding's IIFE.
|
|
129
|
+
*
|
|
130
|
+
* The seed lives here rather than in a preceding setup node, and rather than
|
|
131
|
+
* in a query parameter. Measured 2026-09-13: state seeded in the subject, in
|
|
132
|
+
* a preceding node, and with the subject as the entry node all produced
|
|
133
|
+
* byte-identical results, so the extra node bought nothing. A query parameter
|
|
134
|
+
* would have been worse than useless — `requestParameters` is a binding the
|
|
135
|
+
* corpus measures, so harness data sitting in it is observable to the very
|
|
136
|
+
* scripts under test.
|
|
137
|
+
*
|
|
138
|
+
* The seed is applied BEFORE the `before` snapshot, matching what the setup
|
|
139
|
+
* node did: seeded values are the script's starting state, not a mutation it
|
|
140
|
+
* made. It is applied only on the node's FIRST pass — see the guard below.
|
|
141
|
+
*/
|
|
142
|
+
export function instrumentSubject(authorSource, runId, given = {}, proof) {
|
|
143
|
+
const token = runId.replace(/-/g, "_");
|
|
144
|
+
const bindingName = unused(`${BINDING_PREFIX}${token}`, authorSource);
|
|
145
|
+
const snapshotKey = proof?.snapshotKey ?? unused(`${SNAPSHOT_KEY_PREFIX}${runId}`, authorSource);
|
|
146
|
+
const keyLit = jsStringLiteral(snapshotKey);
|
|
147
|
+
const body = authorSource.endsWith("\n") ? authorSource : `${authorSource}\n`;
|
|
148
|
+
const digestPlaceholder = `__AIC_SCRIPT_TESTER_DIGEST_${runId}__`;
|
|
149
|
+
const source = [
|
|
150
|
+
...(proof?.header === undefined ? [] : [`// ${proof.header}`]),
|
|
151
|
+
"// Generated by rhino-local AIC lane. Seed and instrumentation precede",
|
|
152
|
+
"// the author source; request headers/parameters/cookies are sent on the",
|
|
153
|
+
"// authenticate call instead, because no script can assign those bindings.",
|
|
154
|
+
`var ${bindingName} = (function () {`,
|
|
155
|
+
HELPERS,
|
|
156
|
+
`return { snapshot: snapshot, seed: seed };`,
|
|
157
|
+
`}());`,
|
|
158
|
+
`// Seed once. A node that sends callbacks re-executes from the top when`,
|
|
159
|
+
`// the client answers, and re-seeding there would overwrite whatever the`,
|
|
160
|
+
`// earlier pass left in state with the request's original values — the`,
|
|
161
|
+
`// chain would silently restart from the top on every step.`,
|
|
162
|
+
`if (callbacks.isEmpty()) {`,
|
|
163
|
+
` ${bindingName}.seed(${jsonParseCall(given.sharedState ?? {})}, function (k, v) {`,
|
|
164
|
+
` nodeState.putShared(k, v);`,
|
|
165
|
+
` });`,
|
|
166
|
+
` ${bindingName}.seed(${jsonParseCall(given.transientState ?? {})}, function (k, v) {`,
|
|
167
|
+
` nodeState.putTransient(k, v);`,
|
|
168
|
+
` });`,
|
|
169
|
+
`}`,
|
|
170
|
+
`${bindingName}.before = ${bindingName}.snapshot(nodeState);`,
|
|
171
|
+
`// rhino-local author source begins`,
|
|
172
|
+
body,
|
|
173
|
+
`// rhino-local author source ends`,
|
|
174
|
+
`${bindingName}.final = ${bindingName}.snapshot(nodeState);`,
|
|
175
|
+
`nodeState.putTransient(${keyLit}, JSON.stringify({`,
|
|
176
|
+
` before: ${bindingName}.before,`,
|
|
177
|
+
` final: ${bindingName}.final${proof === undefined ? "" : ","}`,
|
|
178
|
+
...(proof === undefined
|
|
179
|
+
? []
|
|
180
|
+
: [
|
|
181
|
+
` invocationNonce: ${jsStringLiteral(proof.invocationNonce)},`,
|
|
182
|
+
` subjectDigest: ${jsStringLiteral(digestPlaceholder)}`,
|
|
183
|
+
]),
|
|
184
|
+
`}));`,
|
|
185
|
+
"",
|
|
186
|
+
].join("\n");
|
|
187
|
+
const subjectDigest = proof === undefined ? undefined : sha256(source);
|
|
188
|
+
return {
|
|
189
|
+
bindingName,
|
|
190
|
+
snapshotKey,
|
|
191
|
+
source: subjectDigest === undefined
|
|
192
|
+
? source
|
|
193
|
+
: source.replace(digestPlaceholder, subjectDigest),
|
|
194
|
+
...(subjectDigest === undefined ? {} : { subjectDigest }),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** JSON objects the subject will seed, for tests and invoke classification. */
|
|
198
|
+
export function seededState(given) {
|
|
199
|
+
return {
|
|
200
|
+
shared: given.sharedState ?? {},
|
|
201
|
+
transient: given.transientState ?? {},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function unused(seed, source) {
|
|
205
|
+
let candidate = seed;
|
|
206
|
+
while (source.includes(candidate)) {
|
|
207
|
+
candidate += "_";
|
|
208
|
+
}
|
|
209
|
+
return candidate;
|
|
210
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TenantProvider } from "./provider.ts";
|
|
2
|
+
import type { LeaseLaneHooks } from "../harness/lease.ts";
|
|
3
|
+
import { type ChainConformanceReport, type LocalChainResult } from "./conform.ts";
|
|
4
|
+
import { type LeaseIdentity } from "./lease-identity.ts";
|
|
5
|
+
import { type AicIo } from "./tenant.ts";
|
|
6
|
+
export interface AicFileLeaseOptions {
|
|
7
|
+
id: string;
|
|
8
|
+
suiteName: string;
|
|
9
|
+
source: string;
|
|
10
|
+
outcomes: readonly string[];
|
|
11
|
+
realm?: string;
|
|
12
|
+
tenant?: string;
|
|
13
|
+
project?: string;
|
|
14
|
+
/** Where the tenant and its bearer come from; see `resolveTenantProvider`. */
|
|
15
|
+
provider?: TenantProvider;
|
|
16
|
+
unsupported?: "fail" | "skip";
|
|
17
|
+
io?: AicIo;
|
|
18
|
+
/** Test seam; production state defaults to the machine temp directory. */
|
|
19
|
+
stateDir?: string;
|
|
20
|
+
/** Test seam for proving a live lock fails closed without a long wait. */
|
|
21
|
+
lockTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface AicLeaseRunRequest extends LocalChainResult {
|
|
24
|
+
source: string;
|
|
25
|
+
hooks: LeaseLaneHooks;
|
|
26
|
+
}
|
|
27
|
+
export declare class AicFileLease {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(options: AicFileLeaseOptions);
|
|
30
|
+
get identity(): LeaseIdentity;
|
|
31
|
+
open(): Promise<void>;
|
|
32
|
+
run(request: AicLeaseRunRequest): Promise<ChainConformanceReport>;
|
|
33
|
+
endTest(): Promise<void>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|