@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,201 @@
|
|
|
1
|
+
import { deepEqual } from "../case/equal.js";
|
|
2
|
+
import { diffState, sameMutationValue } from "../case/state.js";
|
|
3
|
+
import { STATE_CHANNELS } from "../case/types.js";
|
|
4
|
+
import { formatValue } from "../case/util.js";
|
|
5
|
+
import { judge } from "../case/verdict.js";
|
|
6
|
+
/** Compare observable effects without turning absent evidence into equality. */
|
|
7
|
+
export function diffRecordedEffects(local, aic) {
|
|
8
|
+
const disagreements = [];
|
|
9
|
+
const observationGaps = [];
|
|
10
|
+
const localUnobserved = new Set(local.evidence?.unobservedChannels ?? []);
|
|
11
|
+
const aicUnobserved = new Set(aic.evidence?.unobservedChannels ?? []);
|
|
12
|
+
const localUnified = local.evidence?.stateBuckets === "unified";
|
|
13
|
+
const aicUnified = aic.evidence?.stateBuckets === "unified";
|
|
14
|
+
if (localUnified || aicUnified) {
|
|
15
|
+
observationGaps.push({
|
|
16
|
+
channel: "nodeState",
|
|
17
|
+
path: "buckets",
|
|
18
|
+
local: localUnified ? "unified" : "exact",
|
|
19
|
+
aic: aicUnified ? "unified" : "exact",
|
|
20
|
+
message: "nodeState: per-bucket absence and hidden lower-precedence writes cannot be compared through a unified view",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
compareScalar("outcome", local.outcome, aic.outcome, localUnobserved, aicUnobserved, disagreements, observationGaps);
|
|
24
|
+
compareState(local, aic, localUnobserved, aicUnobserved, disagreements, observationGaps);
|
|
25
|
+
for (const channel of ["callbacks", "openidm", "http", "logs"]) {
|
|
26
|
+
compareArray(channel, local[channel], aic[channel], localUnobserved, aicUnobserved, disagreements, observationGaps);
|
|
27
|
+
}
|
|
28
|
+
return { disagreements, observationGaps };
|
|
29
|
+
}
|
|
30
|
+
function compareScalar(channel, local, aic, localUnobserved, aicUnobserved, disagreements, gaps) {
|
|
31
|
+
if (recordGapIfUnobserved(channel, localUnobserved, aicUnobserved, gaps)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!deepEqual(local, aic)) {
|
|
35
|
+
disagreements.push(disagree(channel, channel, formatValue(local), formatValue(aic), `${channel}: local ${formatValue(local)}, AIC ${formatValue(aic)}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function compareArray(channel, local, aic, localUnobserved, aicUnobserved, disagreements, gaps) {
|
|
39
|
+
if (recordGapIfUnobserved(channel, localUnobserved, aicUnobserved, gaps)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const length = Math.max(local.length, aic.length);
|
|
43
|
+
for (let index = 0; index < length; index += 1) {
|
|
44
|
+
if (deepEqual(local[index], aic[index])) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
disagreements.push(disagree(channel, `[${index}]`, local[index] === undefined ? "(none)" : formatValue(local[index]), aic[index] === undefined ? "(none)" : formatValue(aic[index]), `${channel}: [${index}] differed`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function compareState(local, aic, localUnobserved, aicUnobserved, disagreements, gaps) {
|
|
51
|
+
const localMutations = collectState(local, localUnobserved, "local", gaps);
|
|
52
|
+
const aicMutations = collectState(aic, aicUnobserved, "AIC", gaps);
|
|
53
|
+
const remaining = aicMutations.slice();
|
|
54
|
+
const pairedAicKeys = new Set();
|
|
55
|
+
for (const localMutation of localMutations) {
|
|
56
|
+
const sameKey = remaining
|
|
57
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
58
|
+
.filter(({ candidate }) => candidate.mutation.key === localMutation.mutation.key);
|
|
59
|
+
const equal = sameKey.find(({ candidate }) => sameMutationValue(localMutation.mutation, candidate.mutation));
|
|
60
|
+
const index = equal?.index ?? sameKey[0]?.index ?? -1;
|
|
61
|
+
if (index < 0) {
|
|
62
|
+
if (aic.evidence?.stateBuckets === "unified" &&
|
|
63
|
+
(unifiedBeforeHasKey(aic, localMutation.mutation.key) ||
|
|
64
|
+
pairedAicKeys.has(localMutation.mutation.key))) {
|
|
65
|
+
gaps.push(hiddenMutationGap("AIC", localMutation));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
disagreements.push(stateDisagreement(localMutation, undefined));
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const aicMutation = remaining[index];
|
|
73
|
+
remaining.splice(index, 1);
|
|
74
|
+
if (aicMutation === undefined) {
|
|
75
|
+
disagreements.push(stateDisagreement(localMutation, undefined));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
pairedAicKeys.add(aicMutation.mutation.key);
|
|
79
|
+
if (!sameMutationValue(localMutation.mutation, aicMutation.mutation)) {
|
|
80
|
+
disagreements.push(stateDisagreement(localMutation, aicMutation));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (localMutation.kind === "bucketed" && aicMutation.kind === "bucketed") {
|
|
84
|
+
if (localMutation.bucket !== aicMutation.bucket) {
|
|
85
|
+
disagreements.push(stateDisagreement(localMutation, aicMutation));
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!locationsCompatible(localMutation, aicMutation)) {
|
|
90
|
+
disagreements.push(stateDisagreement(localMutation, aicMutation));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
gaps.push({
|
|
94
|
+
channel: "nodeState",
|
|
95
|
+
path: localMutation.mutation.key,
|
|
96
|
+
local: formatLocated(localMutation),
|
|
97
|
+
aic: formatLocated(aicMutation),
|
|
98
|
+
message: `nodeState: ${JSON.stringify(localMutation.mutation.key)} value and operation agree, but its bucket is unobservable`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const aicMutation of remaining) {
|
|
102
|
+
if (local.evidence?.stateBuckets === "unified" &&
|
|
103
|
+
unifiedBeforeHasKey(local, aicMutation.mutation.key)) {
|
|
104
|
+
gaps.push(hiddenMutationGap("local", aicMutation));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
disagreements.push(stateDisagreement(undefined, aicMutation));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function unifiedBeforeHasKey(effects, key) {
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(effects.evidence?.ambientState ?? {}, key)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return STATE_CHANNELS.some((bucket) => Object.prototype.hasOwnProperty.call(effects[bucket].initial, key));
|
|
116
|
+
}
|
|
117
|
+
function hiddenMutationGap(unobservingLane, observed) {
|
|
118
|
+
return {
|
|
119
|
+
channel: "nodeState",
|
|
120
|
+
path: observed.mutation.key,
|
|
121
|
+
local: unobservingLane === "local" ? "hidden by unified view" : formatLocated(observed),
|
|
122
|
+
aic: unobservingLane === "AIC" ? "hidden by unified view" : formatLocated(observed),
|
|
123
|
+
message: `nodeState: ${JSON.stringify(observed.mutation.key)} may be a lower-precedence or same-value write hidden from the ${unobservingLane} unified view`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function collectState(effects, unobserved, lane, gaps) {
|
|
127
|
+
const mutations = [];
|
|
128
|
+
for (const bucket of STATE_CHANNELS) {
|
|
129
|
+
if (unobserved.has(bucket)) {
|
|
130
|
+
gaps.push({
|
|
131
|
+
channel: bucket,
|
|
132
|
+
path: bucket,
|
|
133
|
+
local: lane === "local" ? "unobserved" : "not compared",
|
|
134
|
+
aic: lane === "AIC" ? "unobserved" : "not compared",
|
|
135
|
+
message: `${bucket}: ${lane} lane cannot observe this channel`,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const mutation of diffState(effects[bucket].initial, effects[bucket].final)) {
|
|
140
|
+
mutations.push({ kind: "bucketed", bucket, mutation });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const mutation of effects.evidence?.unbucketedState ?? []) {
|
|
144
|
+
mutations.push({
|
|
145
|
+
kind: "unbucketed",
|
|
146
|
+
possibleBuckets: mutation.possibleBuckets,
|
|
147
|
+
mutation,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return mutations;
|
|
151
|
+
}
|
|
152
|
+
function locationsCompatible(a, b) {
|
|
153
|
+
if (a.kind === "bucketed" && b.kind === "unbucketed") {
|
|
154
|
+
return b.possibleBuckets.includes(a.bucket);
|
|
155
|
+
}
|
|
156
|
+
if (a.kind === "unbucketed" && b.kind === "bucketed") {
|
|
157
|
+
return a.possibleBuckets.includes(b.bucket);
|
|
158
|
+
}
|
|
159
|
+
if (a.kind === "unbucketed" && b.kind === "unbucketed") {
|
|
160
|
+
return a.possibleBuckets.some((bucket) => b.possibleBuckets.includes(bucket));
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function recordGapIfUnobserved(channel, local, aic, gaps) {
|
|
165
|
+
if (!local.has(channel) && !aic.has(channel)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
gaps.push({
|
|
169
|
+
channel,
|
|
170
|
+
path: channel,
|
|
171
|
+
local: local.has(channel) ? "unobserved" : "observed",
|
|
172
|
+
aic: aic.has(channel) ? "unobserved" : "observed",
|
|
173
|
+
message: `${channel}: lanes cannot be compared because ${local.has(channel) ? "local" : "AIC"} did not observe this channel`,
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
function stateDisagreement(local, aic) {
|
|
178
|
+
const key = local?.mutation.key ?? aic?.mutation.key ?? "state";
|
|
179
|
+
return disagree("nodeState", key, local === undefined ? "(none)" : formatLocated(local), aic === undefined ? "(none)" : formatLocated(aic), `nodeState: ${JSON.stringify(key)} differed`);
|
|
180
|
+
}
|
|
181
|
+
function formatLocated(value) {
|
|
182
|
+
const location = value.kind === "bucketed"
|
|
183
|
+
? value.bucket
|
|
184
|
+
: `one of ${value.possibleBuckets.join(", ")}`;
|
|
185
|
+
return `${formatMutation(value.mutation)} in ${location}`;
|
|
186
|
+
}
|
|
187
|
+
function formatMutation(mutation) {
|
|
188
|
+
if (mutation.operation === "removed") {
|
|
189
|
+
return `removed (was ${formatValue(mutation.before)})`;
|
|
190
|
+
}
|
|
191
|
+
if (mutation.operation === "added") {
|
|
192
|
+
return `added ${formatValue(mutation.after)}`;
|
|
193
|
+
}
|
|
194
|
+
return `changed ${formatValue(mutation.before)} → ${formatValue(mutation.after)}`;
|
|
195
|
+
}
|
|
196
|
+
function disagree(channel, path, local, aic, message) {
|
|
197
|
+
return { channel, path, local, aic, message };
|
|
198
|
+
}
|
|
199
|
+
export function judgeBoth(kase, local, aic) {
|
|
200
|
+
return { local: judge(kase, local), aic: judge(kase, aic) };
|
|
201
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Case } from "../case/types.ts";
|
|
2
|
+
import type { LeaseIdentity } from "./lease-identity.ts";
|
|
3
|
+
export interface EmitJourneyOptions {
|
|
4
|
+
/** Short token used in tree/script names. Must match `[A-Za-z0-9-]{1,32}`. */
|
|
5
|
+
runId?: string;
|
|
6
|
+
realm?: string;
|
|
7
|
+
idFactory?: () => string;
|
|
8
|
+
}
|
|
9
|
+
export interface WrapperInvoke {
|
|
10
|
+
headers: Record<string, string[]>;
|
|
11
|
+
parameters: Record<string, string[]>;
|
|
12
|
+
cookies: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface ScriptResource {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
source: string;
|
|
18
|
+
role: "subject" | "result";
|
|
19
|
+
/** Baked-in outcome, only for `role: "result"`. */
|
|
20
|
+
outcome?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface NodeResource {
|
|
23
|
+
id: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
scriptId: string;
|
|
26
|
+
outcomes: string[];
|
|
27
|
+
connections: Record<string, string>;
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
}
|
|
31
|
+
export interface WrapperJourney {
|
|
32
|
+
treeName: string;
|
|
33
|
+
realm: string;
|
|
34
|
+
identityResource: string;
|
|
35
|
+
scripts: ScriptResource[];
|
|
36
|
+
nodes: NodeResource[];
|
|
37
|
+
/** Tree PUT body. No `_id` / `_rev`. */
|
|
38
|
+
treeBody: Record<string, unknown>;
|
|
39
|
+
/** Node PUT bodies keyed by node id. */
|
|
40
|
+
nodeBodies: Record<string, Record<string, unknown>>;
|
|
41
|
+
subjectOutcomes: string[];
|
|
42
|
+
invoke: WrapperInvoke;
|
|
43
|
+
}
|
|
44
|
+
export interface LeasedJourneyOptions {
|
|
45
|
+
identity: LeaseIdentity;
|
|
46
|
+
realm: string;
|
|
47
|
+
suiteName: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Outcomes declared on the subject node. Always includes `true` and `false`
|
|
51
|
+
* plus `expect.outcome` and everything `case.outcomes` declares, so a script
|
|
52
|
+
* that takes the other branch still reaches a result node that can dump state.
|
|
53
|
+
* A pass expected to suspend has no outcome to add, which is why the declared
|
|
54
|
+
* vocabulary is folded in as well — otherwise a step chain would wire up only
|
|
55
|
+
* the outcomes its first pass mentions.
|
|
56
|
+
*/
|
|
57
|
+
export declare function subjectOutcomes(kase: Case): string[];
|
|
58
|
+
export declare function emitWrapperJourney(kase: Case, source: string, options?: EmitJourneyOptions): WrapperJourney;
|
|
59
|
+
/** Emit the fixed graph owned by one file lease. */
|
|
60
|
+
export declare function emitLeasedJourney(options: LeasedJourneyOptions): WrapperJourney;
|
|
61
|
+
export declare const STATIC_NODE_IDS: {
|
|
62
|
+
readonly success: "70e691a5-1e33-4ac3-a356-e7b6d60d92e0";
|
|
63
|
+
readonly failure: "e301438c-0bd0-429c-ab0c-66126501069a";
|
|
64
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { FAILURE_NODE_ID, RESOURCE_PREFIX, SETUP_OUTCOME, SUCCESS_NODE_ID, } from "./constants.js";
|
|
3
|
+
import { emitResultScript } from "./emit-result.js";
|
|
4
|
+
import { instrumentSubject } from "./emit-subject.js";
|
|
5
|
+
const RUN_ID_PATTERN = /^[A-Za-z0-9-]{1,32}$/;
|
|
6
|
+
/**
|
|
7
|
+
* Outcomes declared on the subject node. Always includes `true` and `false`
|
|
8
|
+
* plus `expect.outcome` and everything `case.outcomes` declares, so a script
|
|
9
|
+
* that takes the other branch still reaches a result node that can dump state.
|
|
10
|
+
* A pass expected to suspend has no outcome to add, which is why the declared
|
|
11
|
+
* vocabulary is folded in as well — otherwise a step chain would wire up only
|
|
12
|
+
* the outcomes its first pass mentions.
|
|
13
|
+
*/
|
|
14
|
+
export function subjectOutcomes(kase) {
|
|
15
|
+
const set = new Set(["true", "false"]);
|
|
16
|
+
if (kase.expect.outcome !== null) {
|
|
17
|
+
set.add(kase.expect.outcome);
|
|
18
|
+
}
|
|
19
|
+
for (const outcome of kase.outcomes ?? []) {
|
|
20
|
+
set.add(outcome);
|
|
21
|
+
}
|
|
22
|
+
return [...set];
|
|
23
|
+
}
|
|
24
|
+
export function emitWrapperJourney(kase, source, options = {}) {
|
|
25
|
+
if (kase.given.engine === "legacy") {
|
|
26
|
+
throw new Error("rhino-local: AIC wrapper emit is next-gen only; legacy needs JavaImporter + Action.send");
|
|
27
|
+
}
|
|
28
|
+
const runId = options.runId ?? randomUUID().replace(/-/g, "").slice(0, 12);
|
|
29
|
+
if (!RUN_ID_PATTERN.test(runId)) {
|
|
30
|
+
throw new Error(`rhino-local: runId ${JSON.stringify(runId)} must match ${RUN_ID_PATTERN}`);
|
|
31
|
+
}
|
|
32
|
+
const realm = options.realm ?? kase.given.realm ?? "alpha";
|
|
33
|
+
const nextId = options.idFactory ?? randomUUID;
|
|
34
|
+
const outcomes = subjectOutcomes(kase);
|
|
35
|
+
const treeName = `${RESOURCE_PREFIX}-${runId}`;
|
|
36
|
+
const instrumented = instrumentSubject(source, runId, kase.given);
|
|
37
|
+
const subjectScript = {
|
|
38
|
+
id: nextId(),
|
|
39
|
+
name: `${treeName}-subject`,
|
|
40
|
+
source: instrumented.source,
|
|
41
|
+
role: "subject",
|
|
42
|
+
};
|
|
43
|
+
const resultScripts = [];
|
|
44
|
+
const resultNodes = [];
|
|
45
|
+
const resultByOutcome = new Map();
|
|
46
|
+
for (let index = 0; index < outcomes.length; index += 1) {
|
|
47
|
+
const outcome = outcomes[index];
|
|
48
|
+
if (outcome === undefined) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const script = {
|
|
52
|
+
id: nextId(),
|
|
53
|
+
name: `${treeName}-result-${slug(outcome)}`,
|
|
54
|
+
source: emitResultScript(outcome, instrumented.snapshotKey),
|
|
55
|
+
role: "result",
|
|
56
|
+
outcome,
|
|
57
|
+
};
|
|
58
|
+
resultScripts.push(script);
|
|
59
|
+
const node = {
|
|
60
|
+
id: nextId(),
|
|
61
|
+
displayName: `result ${outcome}`,
|
|
62
|
+
scriptId: script.id,
|
|
63
|
+
outcomes: [SETUP_OUTCOME],
|
|
64
|
+
connections: { [SETUP_OUTCOME]: SUCCESS_NODE_ID },
|
|
65
|
+
x: 560,
|
|
66
|
+
y: 80 + index * 140,
|
|
67
|
+
};
|
|
68
|
+
resultNodes.push(node);
|
|
69
|
+
resultByOutcome.set(outcome, node);
|
|
70
|
+
}
|
|
71
|
+
const subjectConnections = {};
|
|
72
|
+
for (const outcome of outcomes) {
|
|
73
|
+
const resultNode = resultByOutcome.get(outcome);
|
|
74
|
+
if (resultNode === undefined) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
subjectConnections[outcome] = resultNode.id;
|
|
78
|
+
}
|
|
79
|
+
const subjectNode = {
|
|
80
|
+
id: nextId(),
|
|
81
|
+
displayName: kase.name,
|
|
82
|
+
scriptId: subjectScript.id,
|
|
83
|
+
outcomes,
|
|
84
|
+
connections: subjectConnections,
|
|
85
|
+
x: 80,
|
|
86
|
+
y: 200,
|
|
87
|
+
};
|
|
88
|
+
const nodes = [subjectNode, ...resultNodes];
|
|
89
|
+
const scripts = [subjectScript, ...resultScripts];
|
|
90
|
+
const treeNodes = {};
|
|
91
|
+
for (const node of nodes) {
|
|
92
|
+
treeNodes[node.id] = {
|
|
93
|
+
connections: node.connections,
|
|
94
|
+
displayName: node.displayName,
|
|
95
|
+
nodeType: "ScriptedDecisionNode",
|
|
96
|
+
version: "1.0",
|
|
97
|
+
x: node.x,
|
|
98
|
+
y: node.y,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const treeBody = {
|
|
102
|
+
identityResource: `managed/${realm}_user`,
|
|
103
|
+
entryNodeId: subjectNode.id,
|
|
104
|
+
innerTreeOnly: false,
|
|
105
|
+
description: "rhino-local AIC lane throwaway. Safe to delete.",
|
|
106
|
+
noSession: false,
|
|
107
|
+
mustRun: false,
|
|
108
|
+
enabled: true,
|
|
109
|
+
transactionalOnly: false,
|
|
110
|
+
uiConfig: { categories: '["Test"]' },
|
|
111
|
+
nodes: treeNodes,
|
|
112
|
+
};
|
|
113
|
+
const nodeBodies = {};
|
|
114
|
+
for (const node of nodes) {
|
|
115
|
+
nodeBodies[node.id] = scriptedDecisionBody(node);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
treeName,
|
|
119
|
+
realm,
|
|
120
|
+
identityResource: `managed/${realm}_user`,
|
|
121
|
+
scripts,
|
|
122
|
+
nodes,
|
|
123
|
+
treeBody,
|
|
124
|
+
nodeBodies,
|
|
125
|
+
subjectOutcomes: outcomes,
|
|
126
|
+
invoke: {
|
|
127
|
+
headers: copyStringArrayMap(kase.given.requestHeaders),
|
|
128
|
+
parameters: copyStringArrayMap(kase.given.requestParameters),
|
|
129
|
+
cookies: copyStringMap(kase.given.requestCookies),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Emit the fixed graph owned by one file lease. */
|
|
134
|
+
export function emitLeasedJourney(options) {
|
|
135
|
+
const { identity } = options;
|
|
136
|
+
const inertSource = [
|
|
137
|
+
`// ${identity.marker}`,
|
|
138
|
+
"// Inert until the owning file lease arms this subject slot.",
|
|
139
|
+
'throw new Error("rhino-local AIC subject invoked before it was armed");',
|
|
140
|
+
"",
|
|
141
|
+
].join("\n");
|
|
142
|
+
const subjectScript = {
|
|
143
|
+
id: identity.ids.subjectScript,
|
|
144
|
+
name: `${identity.treeName}-subject`,
|
|
145
|
+
source: inertSource,
|
|
146
|
+
role: "subject",
|
|
147
|
+
};
|
|
148
|
+
const resultScripts = identity.outcomes.map((outcome) => ({
|
|
149
|
+
id: identity.ids.resultScripts[outcome],
|
|
150
|
+
name: `${identity.treeName}-result-${slug(outcome)}`,
|
|
151
|
+
source: `${`// ${identity.marker}\n`}${emitResultScript(outcome, identity.snapshotKey, identity.structuralDigest)}`,
|
|
152
|
+
role: "result",
|
|
153
|
+
outcome,
|
|
154
|
+
}));
|
|
155
|
+
const resultNodes = identity.outcomes.map((outcome, index) => ({
|
|
156
|
+
id: identity.ids.resultNodes[outcome],
|
|
157
|
+
displayName: `result ${outcome}`,
|
|
158
|
+
scriptId: identity.ids.resultScripts[outcome],
|
|
159
|
+
outcomes: [SETUP_OUTCOME],
|
|
160
|
+
connections: { [SETUP_OUTCOME]: SUCCESS_NODE_ID },
|
|
161
|
+
x: 560,
|
|
162
|
+
y: 80 + index * 140,
|
|
163
|
+
}));
|
|
164
|
+
const subjectConnections = Object.fromEntries(identity.outcomes.map((outcome) => [
|
|
165
|
+
outcome,
|
|
166
|
+
identity.ids.resultNodes[outcome],
|
|
167
|
+
]));
|
|
168
|
+
const subjectNode = {
|
|
169
|
+
id: identity.ids.subjectNode,
|
|
170
|
+
displayName: options.suiteName,
|
|
171
|
+
scriptId: subjectScript.id,
|
|
172
|
+
outcomes: [...identity.outcomes],
|
|
173
|
+
connections: subjectConnections,
|
|
174
|
+
x: 80,
|
|
175
|
+
y: 200,
|
|
176
|
+
};
|
|
177
|
+
const nodes = [subjectNode, ...resultNodes];
|
|
178
|
+
const treeNodes = Object.fromEntries(nodes.map((node) => [
|
|
179
|
+
node.id,
|
|
180
|
+
{
|
|
181
|
+
connections: node.connections,
|
|
182
|
+
displayName: node.displayName,
|
|
183
|
+
nodeType: "ScriptedDecisionNode",
|
|
184
|
+
x: node.x,
|
|
185
|
+
y: node.y,
|
|
186
|
+
},
|
|
187
|
+
]));
|
|
188
|
+
const nodeBodies = Object.fromEntries(nodes.map((node) => [node.id, leasedScriptedDecisionBody(node)]));
|
|
189
|
+
return {
|
|
190
|
+
treeName: identity.treeName,
|
|
191
|
+
realm: options.realm,
|
|
192
|
+
identityResource: `managed/${options.realm}_user`,
|
|
193
|
+
scripts: [subjectScript, ...resultScripts],
|
|
194
|
+
nodes,
|
|
195
|
+
treeBody: {
|
|
196
|
+
identityResource: `managed/${options.realm}_user`,
|
|
197
|
+
entryNodeId: subjectNode.id,
|
|
198
|
+
description: identity.marker,
|
|
199
|
+
enabled: true,
|
|
200
|
+
uiConfig: { categories: '["Test"]' },
|
|
201
|
+
nodes: treeNodes,
|
|
202
|
+
},
|
|
203
|
+
nodeBodies,
|
|
204
|
+
subjectOutcomes: [...identity.outcomes],
|
|
205
|
+
invoke: { headers: {}, parameters: {}, cookies: {} },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function scriptedDecisionBody(node) {
|
|
209
|
+
return {
|
|
210
|
+
_id: node.id,
|
|
211
|
+
_type: {
|
|
212
|
+
_id: "ScriptedDecisionNode",
|
|
213
|
+
collection: true,
|
|
214
|
+
name: "Scripted Decision",
|
|
215
|
+
},
|
|
216
|
+
_outcomes: node.outcomes.map((id) => ({ id, displayName: id })),
|
|
217
|
+
inputs: ["*"],
|
|
218
|
+
outputs: ["*"],
|
|
219
|
+
outcomes: node.outcomes,
|
|
220
|
+
script: node.scriptId,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function leasedScriptedDecisionBody(node) {
|
|
224
|
+
return {
|
|
225
|
+
inputs: ["*"],
|
|
226
|
+
outputs: ["*"],
|
|
227
|
+
outcomes: node.outcomes,
|
|
228
|
+
script: node.scriptId,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function slug(outcome) {
|
|
232
|
+
const cleaned = outcome.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
233
|
+
return cleaned.length > 0 ? cleaned : "outcome";
|
|
234
|
+
}
|
|
235
|
+
function copyStringArrayMap(value) {
|
|
236
|
+
if (value === undefined) {
|
|
237
|
+
return {};
|
|
238
|
+
}
|
|
239
|
+
const copy = {};
|
|
240
|
+
for (const [key, items] of Object.entries(value)) {
|
|
241
|
+
copy[key] = items.slice();
|
|
242
|
+
}
|
|
243
|
+
return copy;
|
|
244
|
+
}
|
|
245
|
+
function copyStringMap(value) {
|
|
246
|
+
if (value === undefined) {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
return { ...value };
|
|
250
|
+
}
|
|
251
|
+
export const STATIC_NODE_IDS = {
|
|
252
|
+
success: SUCCESS_NODE_ID,
|
|
253
|
+
failure: FAILURE_NODE_ID,
|
|
254
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next-gen scripted-decision source that dumps final `nodeState` into a
|
|
3
|
+
* HiddenValueCallback. `outcome` is baked in because the tree routes each
|
|
4
|
+
* subject-node outcome to a dedicated result node — the result script cannot
|
|
5
|
+
* see which connection brought it here.
|
|
6
|
+
*
|
|
7
|
+
* Does not compare against `expect`. The runner builds `RecordedEffects`
|
|
8
|
+
* from the dump; `judge()` in verdict.ts is the only pass/fail implementation.
|
|
9
|
+
*/
|
|
10
|
+
export declare function emitResultScript(outcome: string, snapshotKey: string, leaseDigest?: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HARNESS_CALLBACK_ID } from "./constants.js";
|
|
2
|
+
import { jsStringLiteral } from "./js.js";
|
|
3
|
+
const HEADER = `\
|
|
4
|
+
// Generated by rhino-local AIC lane. Returns the subject node's snapshots.
|
|
5
|
+
// Comparison against the case happens in verdict.ts, not here.
|
|
6
|
+
`;
|
|
7
|
+
/**
|
|
8
|
+
* Next-gen scripted-decision source that dumps final `nodeState` into a
|
|
9
|
+
* HiddenValueCallback. `outcome` is baked in because the tree routes each
|
|
10
|
+
* subject-node outcome to a dedicated result node — the result script cannot
|
|
11
|
+
* see which connection brought it here.
|
|
12
|
+
*
|
|
13
|
+
* Does not compare against `expect`. The runner builds `RecordedEffects`
|
|
14
|
+
* from the dump; `judge()` in verdict.ts is the only pass/fail implementation.
|
|
15
|
+
*/
|
|
16
|
+
export function emitResultScript(outcome, snapshotKey, leaseDigest) {
|
|
17
|
+
const idLit = jsStringLiteral(HARNESS_CALLBACK_ID);
|
|
18
|
+
const outcomeLit = jsStringLiteral(outcome);
|
|
19
|
+
const keyLit = jsStringLiteral(snapshotKey);
|
|
20
|
+
return [
|
|
21
|
+
HEADER,
|
|
22
|
+
`var __rhinoLocalOutcome = ${outcomeLit};`,
|
|
23
|
+
`var __rhinoLocalSnapshots = JSON.parse(String(nodeState.get(${keyLit})));`,
|
|
24
|
+
`var __rhinoLocalPayload = JSON.stringify({`,
|
|
25
|
+
` outcome: __rhinoLocalOutcome,`,
|
|
26
|
+
` before: __rhinoLocalSnapshots.before,`,
|
|
27
|
+
` final: __rhinoLocalSnapshots.final,`,
|
|
28
|
+
` invocationNonce: __rhinoLocalSnapshots.invocationNonce,`,
|
|
29
|
+
` subjectDigest: __rhinoLocalSnapshots.subjectDigest${leaseDigest === undefined ? "" : ","}`,
|
|
30
|
+
...(leaseDigest === undefined
|
|
31
|
+
? []
|
|
32
|
+
: [` leaseDigest: ${jsStringLiteral(leaseDigest)}`]),
|
|
33
|
+
`});`,
|
|
34
|
+
`if (callbacks.isEmpty()) {`,
|
|
35
|
+
` callbacksBuilder.hiddenValueCallback(${idLit}, __rhinoLocalPayload);`,
|
|
36
|
+
`}`,
|
|
37
|
+
``,
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { WrapperJourney } from "./emit-journey.ts";
|
|
2
|
+
import type { LeaseIdentity } from "./lease-identity.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A journey whose only job is to end, so that AM issues a session.
|
|
5
|
+
*
|
|
6
|
+
* There is no other way: a session exists only once a journey has run to
|
|
7
|
+
* completion (`docs/api/09-journeys.md` → "Minting a session for a test").
|
|
8
|
+
* Measured 2026-09-14 — the tree needs no Identify Existing User node, the
|
|
9
|
+
* principal need not exist as a managed object, and the journey that later
|
|
10
|
+
* *consumes* the session needs no configuration at all.
|
|
11
|
+
*
|
|
12
|
+
* AM-owned properties are dropped rather than sent. `putSessionProperty` does
|
|
13
|
+
* not override them; the whole login fails with a bare `401 Unauthorized`
|
|
14
|
+
* naming nothing, so sending one would turn a harness bug into an unexplained
|
|
15
|
+
* authentication failure. The five AM derives from the principal arrive by
|
|
16
|
+
* authenticating AS that principal instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function emitSessionJourney(session: Record<string, string>, options: {
|
|
19
|
+
runId: string;
|
|
20
|
+
realm: string;
|
|
21
|
+
}): WrapperJourney;
|
|
22
|
+
/** Deterministic session-minter resources owned by a file lease. */
|
|
23
|
+
export declare function emitLeasedSessionJourney(session: Record<string, string>, options: {
|
|
24
|
+
identity: LeaseIdentity;
|
|
25
|
+
realm: string;
|
|
26
|
+
}): WrapperJourney;
|