@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,35 @@
|
|
|
1
|
+
import type { CallbackEffect, JsonValue } from "../case/types.ts";
|
|
2
|
+
export interface AuthenticateCallbacks {
|
|
3
|
+
/** JSON payload from the result node's HiddenValueCallback, if present. */
|
|
4
|
+
dumpRaw: string | undefined;
|
|
5
|
+
/** Subject-node callbacks; harness dump stripped. */
|
|
6
|
+
callbacks: CallbackEffect[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Read `/authenticate` callbacks. `type` is taken from the HTTP field as-is
|
|
10
|
+
* (the effects-contract spelling: Java simple class name, PascalCase).
|
|
11
|
+
* Other fields are the callback's `output` name/value pairs, so a NameCallback
|
|
12
|
+
* becomes `{ type: "NameCallback", prompt: "User Name" }` — no `input` (that
|
|
13
|
+
* is the client's reply, not the effect).
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseAuthenticateCallbacks(body: unknown): AuthenticateCallbacks;
|
|
16
|
+
/**
|
|
17
|
+
* Build the body that answers a pass's callbacks.
|
|
18
|
+
*
|
|
19
|
+
* AM wants the whole `/authenticate` response back with each callback's
|
|
20
|
+
* `input` slot filled, so the response is cloned and edited rather than
|
|
21
|
+
* rebuilt — `authId` and every field the harness does not understand survive
|
|
22
|
+
* untouched. Replies are matched to callbacks by type, in order, exactly as
|
|
23
|
+
* the local lane's `submittedCallbacks` matches them; a reply the pass never
|
|
24
|
+
* asked for is refused rather than dropped, because a chain that silently
|
|
25
|
+
* ignores a reply is a test asserting something it never sent.
|
|
26
|
+
*
|
|
27
|
+
* Callbacks with no reply keep whatever AM put in their input slot. That is
|
|
28
|
+
* the tenant's own default, which is the one case where a default is not a
|
|
29
|
+
* guess (an unanswered HiddenValueCallback comes back holding its `id`,
|
|
30
|
+
* measured 2026-09-14).
|
|
31
|
+
*/
|
|
32
|
+
export declare function fillCallbackInputs(body: unknown, replies: readonly {
|
|
33
|
+
type: string;
|
|
34
|
+
value: JsonValue;
|
|
35
|
+
}[], label: string): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isPlainObject, parseJsonValue } from "../case/util.js";
|
|
2
|
+
import { HARNESS_CALLBACK_ID } from "./constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read `/authenticate` callbacks. `type` is taken from the HTTP field as-is
|
|
5
|
+
* (the effects-contract spelling: Java simple class name, PascalCase).
|
|
6
|
+
* Other fields are the callback's `output` name/value pairs, so a NameCallback
|
|
7
|
+
* becomes `{ type: "NameCallback", prompt: "User Name" }` — no `input` (that
|
|
8
|
+
* is the client's reply, not the effect).
|
|
9
|
+
*/
|
|
10
|
+
export function parseAuthenticateCallbacks(body) {
|
|
11
|
+
if (!isPlainObject(body)) {
|
|
12
|
+
throw new Error("rhino-local: authenticate body is not an object");
|
|
13
|
+
}
|
|
14
|
+
const rawCallbacks = body.callbacks;
|
|
15
|
+
if (rawCallbacks === undefined) {
|
|
16
|
+
return { dumpRaw: undefined, callbacks: [] };
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(rawCallbacks)) {
|
|
19
|
+
throw new Error("rhino-local: authenticate.callbacks is not an array");
|
|
20
|
+
}
|
|
21
|
+
let dumpRaw;
|
|
22
|
+
const callbacks = [];
|
|
23
|
+
for (let index = 0; index < rawCallbacks.length; index += 1) {
|
|
24
|
+
const parsed = parseOneCallback(rawCallbacks[index], `callbacks[${index}]`);
|
|
25
|
+
if (parsed.harnessValue !== undefined) {
|
|
26
|
+
dumpRaw = parsed.harnessValue;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
callbacks.push(parsed.effect);
|
|
30
|
+
}
|
|
31
|
+
return { dumpRaw, callbacks };
|
|
32
|
+
}
|
|
33
|
+
function parseOneCallback(raw, path) {
|
|
34
|
+
if (!isPlainObject(raw)) {
|
|
35
|
+
throw new Error(`rhino-local: ${path} is not an object`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof raw.type !== "string" || raw.type.trim() === "") {
|
|
38
|
+
throw new Error(`rhino-local: ${path}.type must be a non-empty string`);
|
|
39
|
+
}
|
|
40
|
+
const effect = { type: raw.type };
|
|
41
|
+
const output = raw.output;
|
|
42
|
+
let id;
|
|
43
|
+
let value;
|
|
44
|
+
if (output !== undefined) {
|
|
45
|
+
if (!Array.isArray(output)) {
|
|
46
|
+
throw new Error(`rhino-local: ${path}.output is not an array`);
|
|
47
|
+
}
|
|
48
|
+
for (let index = 0; index < output.length; index += 1) {
|
|
49
|
+
const item = output[index];
|
|
50
|
+
if (!isPlainObject(item) || typeof item.name !== "string") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (item.name === "_id") {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const parsed = parseJsonValue(item.value ?? null, `${path}.output[${index}].value`);
|
|
57
|
+
if (item.name === "id") {
|
|
58
|
+
id = typeof parsed === "string" ? parsed : String(parsed);
|
|
59
|
+
effect.id = id;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (item.name === "value") {
|
|
63
|
+
value = parsed;
|
|
64
|
+
}
|
|
65
|
+
effect[item.name] = parsed;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (raw.type === "HiddenValueCallback" && id === HARNESS_CALLBACK_ID) {
|
|
69
|
+
if (typeof value !== "string") {
|
|
70
|
+
throw new Error(`rhino-local: harness HiddenValueCallback value must be a string, got ${typeof value}`);
|
|
71
|
+
}
|
|
72
|
+
return { effect, harnessValue: value };
|
|
73
|
+
}
|
|
74
|
+
return { effect };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the body that answers a pass's callbacks.
|
|
78
|
+
*
|
|
79
|
+
* AM wants the whole `/authenticate` response back with each callback's
|
|
80
|
+
* `input` slot filled, so the response is cloned and edited rather than
|
|
81
|
+
* rebuilt — `authId` and every field the harness does not understand survive
|
|
82
|
+
* untouched. Replies are matched to callbacks by type, in order, exactly as
|
|
83
|
+
* the local lane's `submittedCallbacks` matches them; a reply the pass never
|
|
84
|
+
* asked for is refused rather than dropped, because a chain that silently
|
|
85
|
+
* ignores a reply is a test asserting something it never sent.
|
|
86
|
+
*
|
|
87
|
+
* Callbacks with no reply keep whatever AM put in their input slot. That is
|
|
88
|
+
* the tenant's own default, which is the one case where a default is not a
|
|
89
|
+
* guess (an unanswered HiddenValueCallback comes back holding its `id`,
|
|
90
|
+
* measured 2026-09-14).
|
|
91
|
+
*/
|
|
92
|
+
export function fillCallbackInputs(body, replies, label) {
|
|
93
|
+
if (!isPlainObject(body)) {
|
|
94
|
+
throw new Error(`rhino-local: ${label}: authenticate body is not an object`);
|
|
95
|
+
}
|
|
96
|
+
const clone = JSON.parse(JSON.stringify(body));
|
|
97
|
+
const rawCallbacks = Array.isArray(clone.callbacks) ? clone.callbacks : [];
|
|
98
|
+
const pending = new Map();
|
|
99
|
+
for (const reply of replies) {
|
|
100
|
+
const queue = pending.get(reply.type);
|
|
101
|
+
if (queue === undefined) {
|
|
102
|
+
pending.set(reply.type, [reply.value]);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
queue.push(reply.value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const raw of rawCallbacks) {
|
|
109
|
+
if (!isPlainObject(raw) || typeof raw.type !== "string") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const queue = pending.get(raw.type);
|
|
113
|
+
if (queue === undefined || queue.length === 0) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const input = raw.input;
|
|
117
|
+
if (!Array.isArray(input) || input.length === 0 || !isPlainObject(input[0])) {
|
|
118
|
+
throw new Error(`rhino-local: ${label}: cannot answer a ${raw.type} — it has no input slot`);
|
|
119
|
+
}
|
|
120
|
+
input[0].value = queue.shift();
|
|
121
|
+
}
|
|
122
|
+
const leftover = [...pending.entries()].filter(([, queue]) => queue.length > 0);
|
|
123
|
+
if (leftover.length > 0) {
|
|
124
|
+
const detail = leftover
|
|
125
|
+
.map(([type, queue]) => `${queue.length} unused ${type}`)
|
|
126
|
+
.join("; ");
|
|
127
|
+
throw new Error(`rhino-local: ${label} replied to callbacks the pass did not send — ${detail}`);
|
|
128
|
+
}
|
|
129
|
+
return JSON.stringify(clone);
|
|
130
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Case, JsonObject, RecordedEffects, Verdict } from "../case/types.ts";
|
|
2
|
+
import type { RunResult } from "../harness/lease.ts";
|
|
3
|
+
import { type EffectsDisagreement, type ObservationGap } from "./diff.ts";
|
|
4
|
+
import { type ManagedFixture } from "./managed.ts";
|
|
5
|
+
import { type AicReply } from "./run.ts";
|
|
6
|
+
export type LaneRunner = (args: {
|
|
7
|
+
kase: Case;
|
|
8
|
+
source: string;
|
|
9
|
+
}) => Promise<RecordedEffects>;
|
|
10
|
+
export interface ConformanceInput {
|
|
11
|
+
kase: Case;
|
|
12
|
+
source: string;
|
|
13
|
+
local?: LaneRunner;
|
|
14
|
+
/** Omit to skip; pass a runner, or `"tenant"` to use `runAicLane`. */
|
|
15
|
+
aic?: LaneRunner | "tenant";
|
|
16
|
+
}
|
|
17
|
+
export interface LaneResult {
|
|
18
|
+
effects?: RecordedEffects;
|
|
19
|
+
verdict?: Verdict;
|
|
20
|
+
skipped?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ConformanceReport {
|
|
24
|
+
name: string;
|
|
25
|
+
portable: boolean;
|
|
26
|
+
local: LaneResult;
|
|
27
|
+
aic: LaneResult;
|
|
28
|
+
disagreements: EffectsDisagreement[];
|
|
29
|
+
observationGaps: ObservationGap[];
|
|
30
|
+
ambientState: Array<{
|
|
31
|
+
lane: "local" | "aic";
|
|
32
|
+
values: JsonObject;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
/** The local lane's completed chain, ready to be checked by the AIC lane. */
|
|
36
|
+
export interface LocalChainResult {
|
|
37
|
+
cases: readonly Case[];
|
|
38
|
+
localEffects: readonly RecordedEffects[];
|
|
39
|
+
replies: readonly (readonly AicReply[])[];
|
|
40
|
+
/** Present only when the local result came through the lease fixture ledger. */
|
|
41
|
+
managedFixtures?: readonly ManagedFixture[];
|
|
42
|
+
}
|
|
43
|
+
export type AicChainRunner = (args: {
|
|
44
|
+
cases: readonly Case[];
|
|
45
|
+
source: string;
|
|
46
|
+
replies: readonly (readonly AicReply[])[];
|
|
47
|
+
managedFixtures?: readonly ManagedFixture[];
|
|
48
|
+
}) => Promise<readonly RecordedEffects[]>;
|
|
49
|
+
export interface ChainConformanceInput extends LocalChainResult {
|
|
50
|
+
source: string;
|
|
51
|
+
/** Omit to skip; pass a runner, or `"tenant"` to use `runAicChain`. */
|
|
52
|
+
aic?: AicChainRunner | "tenant";
|
|
53
|
+
}
|
|
54
|
+
export interface ChainPassReport extends ConformanceReport {
|
|
55
|
+
/** 1-based position in the chain. */
|
|
56
|
+
pass: number;
|
|
57
|
+
final: boolean;
|
|
58
|
+
/** False when the pass suspended before the wrapper could dump its effects. */
|
|
59
|
+
aicObserved: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface ChainConformanceReport {
|
|
62
|
+
name: string;
|
|
63
|
+
portable: boolean;
|
|
64
|
+
passes: ChainPassReport[];
|
|
65
|
+
disagreements: EffectsDisagreement[];
|
|
66
|
+
observationGaps: ObservationGap[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Run one case on both lanes and diff the two `RecordedEffects`.
|
|
70
|
+
* Both lanes are judged by `judge()` from verdict.ts — a second pass/fail
|
|
71
|
+
* implementation here would make the comparison prove nothing.
|
|
72
|
+
*
|
|
73
|
+
* Either lane may be omitted. A missing local runner is the expected state
|
|
74
|
+
* until the bindings slice lands; a missing AIC runner is used by tests.
|
|
75
|
+
*/
|
|
76
|
+
export declare function conform(input: ConformanceInput): Promise<ConformanceReport>;
|
|
77
|
+
/**
|
|
78
|
+
* Check an already-completed local chain against one AIC journey.
|
|
79
|
+
*
|
|
80
|
+
* The caller supplies every carried case and reply. This function deliberately
|
|
81
|
+
* cannot compute either: making the AIC lane repeat the local carry rule would
|
|
82
|
+
* hide a wrong mock when both lanes made the same guess.
|
|
83
|
+
*/
|
|
84
|
+
export declare function conformChain(input: ChainConformanceInput): Promise<ChainConformanceReport>;
|
|
85
|
+
/**
|
|
86
|
+
* Convert `Lease.execute()` output without re-running or reconstructing it.
|
|
87
|
+
* External one-shot callers may use this too; `useLease()` sends it to the
|
|
88
|
+
* file's already-open `AicFileLease`, not to `runAicChain()`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function chainFromRunResult(result: RunResult): LocalChainResult;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { isPortable } from "../case/portable.js";
|
|
2
|
+
import { diffRecordedEffects, } from "./diff.js";
|
|
3
|
+
import { judge } from "../case/verdict.js";
|
|
4
|
+
import { managedSeedMatches } from "./managed.js";
|
|
5
|
+
import { runAicChain, runAicLane } from "./run.js";
|
|
6
|
+
import { aicUnsupportedReason } from "./unsupported.js";
|
|
7
|
+
/**
|
|
8
|
+
* Run one case on both lanes and diff the two `RecordedEffects`.
|
|
9
|
+
* Both lanes are judged by `judge()` from verdict.ts — a second pass/fail
|
|
10
|
+
* implementation here would make the comparison prove nothing.
|
|
11
|
+
*
|
|
12
|
+
* Either lane may be omitted. A missing local runner is the expected state
|
|
13
|
+
* until the bindings slice lands; a missing AIC runner is used by tests.
|
|
14
|
+
*/
|
|
15
|
+
export async function conform(input) {
|
|
16
|
+
const local = await runLane(input.local, input, "no local runner provided (bindings lane is a separate slice)");
|
|
17
|
+
const aicSkip = aicUnsupportedReason(input.kase);
|
|
18
|
+
const aicRunner = input.aic === "tenant" ? tenantRunner : input.aic;
|
|
19
|
+
const aic = aicSkip
|
|
20
|
+
? skipResult(aicSkip)
|
|
21
|
+
: await runLane(aicRunner, input, "no AIC runner provided");
|
|
22
|
+
const comparison = local.effects !== undefined && aic.effects !== undefined
|
|
23
|
+
? diffRecordedEffects(local.effects, aic.effects)
|
|
24
|
+
: { disagreements: [], observationGaps: [] };
|
|
25
|
+
return {
|
|
26
|
+
name: input.kase.name,
|
|
27
|
+
portable: isPortable(input.kase),
|
|
28
|
+
local,
|
|
29
|
+
aic,
|
|
30
|
+
disagreements: comparison.disagreements,
|
|
31
|
+
observationGaps: comparison.observationGaps,
|
|
32
|
+
ambientState: collectAmbient(local, aic),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check an already-completed local chain against one AIC journey.
|
|
37
|
+
*
|
|
38
|
+
* The caller supplies every carried case and reply. This function deliberately
|
|
39
|
+
* cannot compute either: making the AIC lane repeat the local carry rule would
|
|
40
|
+
* hide a wrong mock when both lanes made the same guess.
|
|
41
|
+
*/
|
|
42
|
+
export async function conformChain(input) {
|
|
43
|
+
validateChainInput(input);
|
|
44
|
+
const local = input.cases.map((kase, index) => observedResult(kase, input.localEffects[index]));
|
|
45
|
+
const harnessOwnsManaged = input.managedFixtures !== undefined &&
|
|
46
|
+
managedSeedMatches(input.cases[0].given.managed, input.managedFixtures);
|
|
47
|
+
const unsupported = input.cases.flatMap((kase) => {
|
|
48
|
+
const reason = aicUnsupportedReason(kase, { harnessOwnsManaged });
|
|
49
|
+
return reason === undefined ? [] : [`${kase.name}: ${reason}`];
|
|
50
|
+
});
|
|
51
|
+
const aicRunner = input.aic === "tenant" ? tenantChainRunner : input.aic;
|
|
52
|
+
let aic;
|
|
53
|
+
if (unsupported.length > 0) {
|
|
54
|
+
aic = input.cases.map(() => skipResult(`AIC lane skipped for the whole chain: ${unsupported.join("; ")}`));
|
|
55
|
+
}
|
|
56
|
+
else if (aicRunner === undefined) {
|
|
57
|
+
aic = input.cases.map(() => skipResult("no AIC chain runner provided"));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
aic = await runChainLane(aicRunner, input);
|
|
61
|
+
}
|
|
62
|
+
const finalIndex = input.cases.length - 1;
|
|
63
|
+
const passes = input.cases.map((kase, index) => {
|
|
64
|
+
const isFinal = index === finalIndex;
|
|
65
|
+
return chainPassReport(kase, index, isFinal, local[index], aic[index]);
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
name: input.cases[finalIndex].name,
|
|
69
|
+
portable: input.cases.every(isPortable),
|
|
70
|
+
passes,
|
|
71
|
+
disagreements: passes.flatMap((pass) => pass.disagreements),
|
|
72
|
+
observationGaps: passes.flatMap((pass) => pass.observationGaps),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convert `Lease.execute()` output without re-running or reconstructing it.
|
|
77
|
+
* External one-shot callers may use this too; `useLease()` sends it to the
|
|
78
|
+
* file's already-open `AicFileLease`, not to `runAicChain()`.
|
|
79
|
+
*/
|
|
80
|
+
export function chainFromRunResult(result) {
|
|
81
|
+
return {
|
|
82
|
+
cases: [...result.steps.map((step) => step.kase), result.kase],
|
|
83
|
+
localEffects: [...result.steps.map((step) => step.effects), result.effects],
|
|
84
|
+
replies: result.steps.map((step) => submittedToAicReplies(step.submitted)),
|
|
85
|
+
managedFixtures: result.fixtures,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function submittedToAicReplies(submitted) {
|
|
89
|
+
const replies = [];
|
|
90
|
+
for (const callback of submitted) {
|
|
91
|
+
const value = callback.value;
|
|
92
|
+
if (Object.prototype.hasOwnProperty.call(callback, "value") && value !== undefined) {
|
|
93
|
+
replies.push({ type: callback.type, value });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return replies;
|
|
97
|
+
}
|
|
98
|
+
function validateChainInput(input) {
|
|
99
|
+
if (input.cases.length === 0) {
|
|
100
|
+
throw new Error("conformChain needs at least one case");
|
|
101
|
+
}
|
|
102
|
+
if (input.localEffects.length !== input.cases.length) {
|
|
103
|
+
throw new Error(`conformChain: ${input.cases.length} cases need ${input.cases.length} local effects, got ${input.localEffects.length}`);
|
|
104
|
+
}
|
|
105
|
+
if (input.replies.length !== input.cases.length - 1) {
|
|
106
|
+
throw new Error(`conformChain: ${input.cases.length} passes need ${input.cases.length - 1} reply sets, got ${input.replies.length}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function runChainLane(runner, input) {
|
|
110
|
+
try {
|
|
111
|
+
const effects = await runner({
|
|
112
|
+
cases: input.cases,
|
|
113
|
+
source: input.source,
|
|
114
|
+
replies: input.replies,
|
|
115
|
+
...(input.managedFixtures !== undefined
|
|
116
|
+
? { managedFixtures: input.managedFixtures }
|
|
117
|
+
: {}),
|
|
118
|
+
});
|
|
119
|
+
if (effects.length !== input.cases.length) {
|
|
120
|
+
throw new Error(`AIC chain runner returned ${effects.length} effects for ${input.cases.length} passes`);
|
|
121
|
+
}
|
|
122
|
+
return input.cases.map((kase, index) => observedResult(kase, effects[index]));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
+
return input.cases.map(() => ({ error: message }));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function observedResult(kase, effects) {
|
|
130
|
+
return { effects, verdict: judge(kase, effects) };
|
|
131
|
+
}
|
|
132
|
+
function chainPassReport(kase, index, final, local, aic) {
|
|
133
|
+
const comparison = final && local.effects !== undefined && aic.effects !== undefined
|
|
134
|
+
? diffRecordedEffects(local.effects, aic.effects)
|
|
135
|
+
: { disagreements: [], observationGaps: [] };
|
|
136
|
+
if (!final && aic.effects !== undefined) {
|
|
137
|
+
comparison.observationGaps.push(intermediatePassGap(kase));
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
pass: index + 1,
|
|
141
|
+
final,
|
|
142
|
+
aicObserved: final && aic.effects !== undefined,
|
|
143
|
+
name: kase.name,
|
|
144
|
+
portable: isPortable(kase),
|
|
145
|
+
local,
|
|
146
|
+
aic,
|
|
147
|
+
disagreements: comparison.disagreements,
|
|
148
|
+
observationGaps: comparison.observationGaps,
|
|
149
|
+
ambientState: collectAmbient(local, aic),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function intermediatePassGap(kase) {
|
|
153
|
+
return {
|
|
154
|
+
channel: "nodeState",
|
|
155
|
+
path: "pass",
|
|
156
|
+
local: "observed",
|
|
157
|
+
aic: "unobserved",
|
|
158
|
+
message: `${kase.name}: intermediate AIC effects are unobserved because the wrapper cannot dump state without adding a script-visible callback`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function collectAmbient(local, aic) {
|
|
162
|
+
const ambient = [];
|
|
163
|
+
for (const [lane, result] of [
|
|
164
|
+
["local", local],
|
|
165
|
+
["aic", aic],
|
|
166
|
+
]) {
|
|
167
|
+
const values = result.effects?.evidence?.ambientState;
|
|
168
|
+
if (values !== undefined && Object.keys(values).length > 0) {
|
|
169
|
+
ambient.push({ lane, values });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return ambient;
|
|
173
|
+
}
|
|
174
|
+
async function runLane(runner, input, missing) {
|
|
175
|
+
if (runner === undefined) {
|
|
176
|
+
return skipResult(missing);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const effects = await runner({ kase: input.kase, source: input.source });
|
|
180
|
+
return { effects, verdict: judge(input.kase, effects) };
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function skipResult(reason) {
|
|
187
|
+
return { skipped: reason };
|
|
188
|
+
}
|
|
189
|
+
const tenantRunner = ({ kase, source }) => runAicLane(kase, source);
|
|
190
|
+
const tenantChainRunner = ({ cases, source, replies, managedFixtures, }) => runAicChain(cases, source, {
|
|
191
|
+
replies,
|
|
192
|
+
...(managedFixtures !== undefined ? { managedFixtures } : {}),
|
|
193
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Built-in Success node. Same id on every AM tenant (`docs/api/09-journeys.md`). */
|
|
2
|
+
export declare const SUCCESS_NODE_ID = "70e691a5-1e33-4ac3-a356-e7b6d60d92e0";
|
|
3
|
+
/** Built-in Failure node. Same id on every AM tenant (`docs/api/09-journeys.md`). */
|
|
4
|
+
export declare const FAILURE_NODE_ID = "e301438c-0bd0-429c-ab0c-66126501069a";
|
|
5
|
+
/**
|
|
6
|
+
* HiddenValueCallback `id` the result script writes. The AIC runner reads
|
|
7
|
+
* this callback back from `/authenticate` and strips it from `effects.callbacks`
|
|
8
|
+
* so the harness dump is not mistaken for a subject-node callback.
|
|
9
|
+
*/
|
|
10
|
+
export declare const HARNESS_CALLBACK_ID = "__rhinoLocalEffects";
|
|
11
|
+
/** Prefix for throwaway tree and script names. Distinct from operator config. */
|
|
12
|
+
export declare const RESOURCE_PREFIX = "rl-aic";
|
|
13
|
+
export declare const SETUP_OUTCOME = "true";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Built-in Success node. Same id on every AM tenant (`docs/api/09-journeys.md`). */
|
|
2
|
+
export const SUCCESS_NODE_ID = "70e691a5-1e33-4ac3-a356-e7b6d60d92e0";
|
|
3
|
+
/** Built-in Failure node. Same id on every AM tenant (`docs/api/09-journeys.md`). */
|
|
4
|
+
export const FAILURE_NODE_ID = "e301438c-0bd0-429c-ab0c-66126501069a";
|
|
5
|
+
/**
|
|
6
|
+
* HiddenValueCallback `id` the result script writes. The AIC runner reads
|
|
7
|
+
* this callback back from `/authenticate` and strips it from `effects.callbacks`
|
|
8
|
+
* so the harness dump is not mistaken for a subject-node callback.
|
|
9
|
+
*/
|
|
10
|
+
export const HARNESS_CALLBACK_ID = "__rhinoLocalEffects";
|
|
11
|
+
/** Prefix for throwaway tree and script names. Distinct from operator config. */
|
|
12
|
+
export const RESOURCE_PREFIX = "rl-aic";
|
|
13
|
+
export const SETUP_OUTCOME = "true";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Case, EvidenceChannel, RecordedEffects, Verdict } from "../case/types.ts";
|
|
2
|
+
export interface EffectsDisagreement {
|
|
3
|
+
channel: EvidenceChannel;
|
|
4
|
+
path: string;
|
|
5
|
+
local: string;
|
|
6
|
+
aic: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ObservationGap {
|
|
10
|
+
channel: EvidenceChannel;
|
|
11
|
+
path: string;
|
|
12
|
+
local: string;
|
|
13
|
+
aic: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface EffectsComparison {
|
|
17
|
+
disagreements: EffectsDisagreement[];
|
|
18
|
+
observationGaps: ObservationGap[];
|
|
19
|
+
}
|
|
20
|
+
/** Compare observable effects without turning absent evidence into equality. */
|
|
21
|
+
export declare function diffRecordedEffects(local: RecordedEffects, aic: RecordedEffects): EffectsComparison;
|
|
22
|
+
export declare function judgeBoth(kase: Case, local: RecordedEffects, aic: RecordedEffects): {
|
|
23
|
+
local: Verdict;
|
|
24
|
+
aic: Verdict;
|
|
25
|
+
};
|