@nodetool-ai/reliability-harness 0.8.1
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/README.md +34 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/cli.d.ts +73 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +201 -0
- package/dist/cli.js.map +1 -0
- package/dist/compare.d.ts +98 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +267 -0
- package/dist/compare.js.map +1 -0
- package/dist/core/diff.d.ts +39 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +114 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/golden.d.ts +50 -0
- package/dist/core/golden.d.ts.map +1 -0
- package/dist/core/golden.js +150 -0
- package/dist/core/golden.js.map +1 -0
- package/dist/core/invariants/index.d.ts +32 -0
- package/dist/core/invariants/index.d.ts.map +1 -0
- package/dist/core/invariants/index.js +44 -0
- package/dist/core/invariants/index.js.map +1 -0
- package/dist/core/invariants/leaks.d.ts +22 -0
- package/dist/core/invariants/leaks.d.ts.map +1 -0
- package/dist/core/invariants/leaks.js +55 -0
- package/dist/core/invariants/leaks.js.map +1 -0
- package/dist/core/invariants/lifecycle.d.ts +19 -0
- package/dist/core/invariants/lifecycle.d.ts.map +1 -0
- package/dist/core/invariants/lifecycle.js +95 -0
- package/dist/core/invariants/lifecycle.js.map +1 -0
- package/dist/core/invariants/protocol-validity.d.ts +4 -0
- package/dist/core/invariants/protocol-validity.d.ts.map +1 -0
- package/dist/core/invariants/protocol-validity.js +131 -0
- package/dist/core/invariants/protocol-validity.js.map +1 -0
- package/dist/core/invariants/state-machine.d.ts +29 -0
- package/dist/core/invariants/state-machine.d.ts.map +1 -0
- package/dist/core/invariants/state-machine.js +79 -0
- package/dist/core/invariants/state-machine.js.map +1 -0
- package/dist/core/invariants/status-sets.d.ts +27 -0
- package/dist/core/invariants/status-sets.d.ts.map +1 -0
- package/dist/core/invariants/status-sets.js +51 -0
- package/dist/core/invariants/status-sets.js.map +1 -0
- package/dist/core/invariants/terminal-uniqueness.d.ts +23 -0
- package/dist/core/invariants/terminal-uniqueness.d.ts.map +1 -0
- package/dist/core/invariants/terminal-uniqueness.js +99 -0
- package/dist/core/invariants/terminal-uniqueness.js.map +1 -0
- package/dist/core/invariants/types.d.ts +25 -0
- package/dist/core/invariants/types.d.ts.map +1 -0
- package/dist/core/invariants/types.js +2 -0
- package/dist/core/invariants/types.js.map +1 -0
- package/dist/core/journey.d.ts +121 -0
- package/dist/core/journey.d.ts.map +1 -0
- package/dist/core/journey.js +166 -0
- package/dist/core/journey.js.map +1 -0
- package/dist/core/normalize.d.ts +86 -0
- package/dist/core/normalize.d.ts.map +1 -0
- package/dist/core/normalize.js +182 -0
- package/dist/core/normalize.js.map +1 -0
- package/dist/core/record.d.ts +160 -0
- package/dist/core/record.d.ts.map +1 -0
- package/dist/core/record.js +74 -0
- package/dist/core/record.js.map +1 -0
- package/dist/core/stable-json.d.ts +13 -0
- package/dist/core/stable-json.d.ts.map +1 -0
- package/dist/core/stable-json.js +28 -0
- package/dist/core/stable-json.js.map +1 -0
- package/dist/drivers/anchors.d.ts +29 -0
- package/dist/drivers/anchors.d.ts.map +1 -0
- package/dist/drivers/anchors.js +48 -0
- package/dist/drivers/anchors.js.map +1 -0
- package/dist/drivers/app-headless.d.ts +39 -0
- package/dist/drivers/app-headless.d.ts.map +1 -0
- package/dist/drivers/app-headless.js +123 -0
- package/dist/drivers/app-headless.js.map +1 -0
- package/dist/drivers/browser.d.ts +34 -0
- package/dist/drivers/browser.d.ts.map +1 -0
- package/dist/drivers/browser.js +176 -0
- package/dist/drivers/browser.js.map +1 -0
- package/dist/drivers/cli.d.ts +13 -0
- package/dist/drivers/cli.d.ts.map +1 -0
- package/dist/drivers/cli.js +82 -0
- package/dist/drivers/cli.js.map +1 -0
- package/dist/drivers/fixture-nodes.d.ts +124 -0
- package/dist/drivers/fixture-nodes.d.ts.map +1 -0
- package/dist/drivers/fixture-nodes.js +189 -0
- package/dist/drivers/fixture-nodes.js.map +1 -0
- package/dist/drivers/index.d.ts +18 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.js +12 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/kernel.d.ts +8 -0
- package/dist/drivers/kernel.d.ts.map +1 -0
- package/dist/drivers/kernel.js +247 -0
- package/dist/drivers/kernel.js.map +1 -0
- package/dist/drivers/packaged.d.ts +28 -0
- package/dist/drivers/packaged.d.ts.map +1 -0
- package/dist/drivers/packaged.js +303 -0
- package/dist/drivers/packaged.js.map +1 -0
- package/dist/drivers/python-bridge.d.ts +22 -0
- package/dist/drivers/python-bridge.d.ts.map +1 -0
- package/dist/drivers/python-bridge.js +48 -0
- package/dist/drivers/python-bridge.js.map +1 -0
- package/dist/drivers/registry.d.ts +11 -0
- package/dist/drivers/registry.d.ts.map +1 -0
- package/dist/drivers/registry.js +18 -0
- package/dist/drivers/registry.js.map +1 -0
- package/dist/drivers/relay-fields.d.ts +70 -0
- package/dist/drivers/relay-fields.d.ts.map +1 -0
- package/dist/drivers/relay-fields.js +104 -0
- package/dist/drivers/relay-fields.js.map +1 -0
- package/dist/drivers/repo-root.d.ts +9 -0
- package/dist/drivers/repo-root.d.ts.map +1 -0
- package/dist/drivers/repo-root.js +39 -0
- package/dist/drivers/repo-root.js.map +1 -0
- package/dist/drivers/spawn-cli.d.ts +8 -0
- package/dist/drivers/spawn-cli.d.ts.map +1 -0
- package/dist/drivers/spawn-cli.js +41 -0
- package/dist/drivers/spawn-cli.js.map +1 -0
- package/dist/drivers/types.d.ts +18 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +2 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/drivers/ws-server.d.ts +8 -0
- package/dist/drivers/ws-server.d.ts.map +1 -0
- package/dist/drivers/ws-server.js +382 -0
- package/dist/drivers/ws-server.js.map +1 -0
- package/dist/faults/bridge-faults.d.ts +7 -0
- package/dist/faults/bridge-faults.d.ts.map +1 -0
- package/dist/faults/bridge-faults.js +90 -0
- package/dist/faults/bridge-faults.js.map +1 -0
- package/dist/faults/host-faults.d.ts +40 -0
- package/dist/faults/host-faults.d.ts.map +1 -0
- package/dist/faults/host-faults.js +70 -0
- package/dist/faults/host-faults.js.map +1 -0
- package/dist/faults/index.d.ts +21 -0
- package/dist/faults/index.d.ts.map +1 -0
- package/dist/faults/index.js +29 -0
- package/dist/faults/index.js.map +1 -0
- package/dist/faults/net-proxy.d.ts +75 -0
- package/dist/faults/net-proxy.d.ts.map +1 -0
- package/dist/faults/net-proxy.js +305 -0
- package/dist/faults/net-proxy.js.map +1 -0
- package/dist/faults/provider-faults.d.ts +12 -0
- package/dist/faults/provider-faults.d.ts.map +1 -0
- package/dist/faults/provider-faults.js +110 -0
- package/dist/faults/provider-faults.js.map +1 -0
- package/dist/faults/registry.d.ts +17 -0
- package/dist/faults/registry.d.ts.map +1 -0
- package/dist/faults/registry.js +15 -0
- package/dist/faults/registry.js.map +1 -0
- package/dist/faults/types.d.ts +42 -0
- package/dist/faults/types.d.ts.map +1 -0
- package/dist/faults/types.js +2 -0
- package/dist/faults/types.js.map +1 -0
- package/dist/faults/ws-faults.d.ts +6 -0
- package/dist/faults/ws-faults.d.ts.map +1 -0
- package/dist/faults/ws-faults.js +48 -0
- package/dist/faults/ws-faults.js.map +1 -0
- package/dist/faults/ws-proxy.d.ts +36 -0
- package/dist/faults/ws-proxy.d.ts.map +1 -0
- package/dist/faults/ws-proxy.js +40 -0
- package/dist/faults/ws-proxy.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/compare.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { diffNormalizedRecords, formatStreamDiff, streamDiffIsEmpty } from "./core/diff.js";
|
|
2
|
+
import { checkGoldens } from "./core/golden.js";
|
|
3
|
+
import { INVARIANT_CHECKS } from "./core/invariants/index.js";
|
|
4
|
+
import { normalizeRunRecord } from "./core/normalize.js";
|
|
5
|
+
import { getFaultModule } from "./faults/registry.js";
|
|
6
|
+
// Side-effecting import: registers every built-in fault module (task D1's
|
|
7
|
+
// "faults/index.ts registers on import" contract) so `getFaultModule` below
|
|
8
|
+
// resolves journey-declared fault names without every caller of
|
|
9
|
+
// `compareJourney` remembering to import the fault barrel itself.
|
|
10
|
+
import "./faults/index.js";
|
|
11
|
+
function onlyServerToClient(record) {
|
|
12
|
+
return {
|
|
13
|
+
...record,
|
|
14
|
+
frames: record.frames.filter((f) => f.direction === "server_to_client")
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Applies every fault in `faults` whose optional `surface` restriction either
|
|
19
|
+
* matches `surfaceName` or is unset (applies everywhere), in order, and
|
|
20
|
+
* returns one teardown that undoes them all in reverse — so a single
|
|
21
|
+
* `finally` block at each call site fully unwinds the fault's process-global
|
|
22
|
+
* side effects (today: the provider registry) regardless of how the driver
|
|
23
|
+
* run in between went. A fault name with no registered `FaultModule` is
|
|
24
|
+
* skipped and reported back via `unknownFaults` (its own return value), never
|
|
25
|
+
* silently ignored or thrown.
|
|
26
|
+
*/
|
|
27
|
+
async function applyFaults(journey, surfaceName, faults) {
|
|
28
|
+
const applicable = faults.filter((f) => f.surface === undefined || f.surface === surfaceName);
|
|
29
|
+
const applied = [];
|
|
30
|
+
const unknown = [];
|
|
31
|
+
const teardowns = [];
|
|
32
|
+
for (const fault of applicable) {
|
|
33
|
+
const module = getFaultModule(fault.type);
|
|
34
|
+
if (!module) {
|
|
35
|
+
unknown.push(fault.type);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const teardown = await module.configure({ journey, surface: surfaceName, fault });
|
|
39
|
+
teardowns.push(teardown);
|
|
40
|
+
applied.push(fault.type);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
applied,
|
|
44
|
+
unknown,
|
|
45
|
+
teardown: async () => {
|
|
46
|
+
for (const teardown of teardowns.reverse()) {
|
|
47
|
+
await teardown();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Runs `journey` on every driver in `drivers`, diffing each non-oracle
|
|
54
|
+
* surface's normalized `server_to_client` stream against the oracle's (the
|
|
55
|
+
* one driver named `options.oracleName`, default `"kernel"` — §12's "the
|
|
56
|
+
* kernel surface is the oracle"). `drivers` must include the oracle driver
|
|
57
|
+
* itself; it both anchors the diff and is reported as its own (trivially
|
|
58
|
+
* clean) surface.
|
|
59
|
+
*
|
|
60
|
+
* `options.faults` (task D1) selects the fault matrix for this run — default
|
|
61
|
+
* is the journey's own declared `faults` block (`journey.json`), so callers
|
|
62
|
+
* that don't care about fault injection get the journey's authored behavior
|
|
63
|
+
* unchanged. Each applicable fault (its `surface` restriction, if any,
|
|
64
|
+
* matched against the driver about to run) is configured immediately before
|
|
65
|
+
* that driver's `run()` and torn down immediately after, so faults never
|
|
66
|
+
* leak across drivers or across `compareJourney` calls.
|
|
67
|
+
*/
|
|
68
|
+
export async function compareJourney(journey, drivers, options = {}) {
|
|
69
|
+
const oracleName = options.oracleName ?? "kernel";
|
|
70
|
+
const oracleDriver = drivers.find((d) => d.name === oracleName);
|
|
71
|
+
if (!oracleDriver) {
|
|
72
|
+
throw new Error(`compareJourney: no driver named "${oracleName}" among [${drivers
|
|
73
|
+
.map((d) => d.name)
|
|
74
|
+
.join(", ")}] — the oracle driver must be included`);
|
|
75
|
+
}
|
|
76
|
+
const faults = options.faults ?? journey.manifest.faults;
|
|
77
|
+
const invariantNames = journey.manifest.assertions.invariants;
|
|
78
|
+
const checks = invariantNames
|
|
79
|
+
.map((name) => ({ name, check: INVARIANT_CHECKS[name] }))
|
|
80
|
+
.filter((c) => !!c.check);
|
|
81
|
+
const unknownInvariants = invariantNames.filter((name) => !INVARIANT_CHECKS[name]);
|
|
82
|
+
const issues = [];
|
|
83
|
+
if (unknownInvariants.length > 0) {
|
|
84
|
+
issues.push(`journey declares unknown invariant(s), skipped: ${unknownInvariants.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
const unknownFaultsSeen = new Set();
|
|
87
|
+
/** Runs one driver with its applicable faults configured/torn down around
|
|
88
|
+
* the call — shared by the oracle (run once, up front) and every other
|
|
89
|
+
* surface's loop iteration below. */
|
|
90
|
+
const runWithFaults = async (driver) => {
|
|
91
|
+
const { applied, unknown, teardown } = await applyFaults(journey, driver.name, faults);
|
|
92
|
+
for (const name of unknown)
|
|
93
|
+
unknownFaultsSeen.add(name);
|
|
94
|
+
try {
|
|
95
|
+
const record = await driver.run(journey);
|
|
96
|
+
return { record, faultsApplied: applied };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await teardown();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Run the oracle first — every other surface's diff needs its normalized
|
|
103
|
+
// record, and running it once (not per-surface) keeps an N-driver compare
|
|
104
|
+
// to N+1 runs, not 2N.
|
|
105
|
+
const { record: oracleRecord, faultsApplied: oracleFaultsApplied } = await runWithFaults(oracleDriver);
|
|
106
|
+
const oracleNormalized = onlyServerToClient(normalizeRunRecord(oracleRecord));
|
|
107
|
+
const surfaces = [];
|
|
108
|
+
for (const driver of drivers) {
|
|
109
|
+
if (driver.supports && !driver.supports(journey)) {
|
|
110
|
+
surfaces.push({
|
|
111
|
+
surface: driver.name,
|
|
112
|
+
ran: false,
|
|
113
|
+
skipReason: "not applicable to this journey",
|
|
114
|
+
violations: [],
|
|
115
|
+
goldens: [],
|
|
116
|
+
diffVsOracle: null,
|
|
117
|
+
diffVsOracleEmpty: true,
|
|
118
|
+
notApplicable: true,
|
|
119
|
+
faultsApplied: [],
|
|
120
|
+
ok: true
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let record;
|
|
125
|
+
let faultsApplied;
|
|
126
|
+
if (driver.name === oracleDriver.name) {
|
|
127
|
+
record = oracleRecord;
|
|
128
|
+
faultsApplied = oracleFaultsApplied;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
try {
|
|
132
|
+
const result = await runWithFaults(driver);
|
|
133
|
+
record = result.record;
|
|
134
|
+
faultsApplied = result.faultsApplied;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const skipReason = `run failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
138
|
+
surfaces.push({
|
|
139
|
+
surface: driver.name,
|
|
140
|
+
ran: false,
|
|
141
|
+
skipReason,
|
|
142
|
+
violations: [],
|
|
143
|
+
goldens: [],
|
|
144
|
+
diffVsOracle: null,
|
|
145
|
+
diffVsOracleEmpty: false,
|
|
146
|
+
notApplicable: false,
|
|
147
|
+
faultsApplied: [],
|
|
148
|
+
ok: false
|
|
149
|
+
});
|
|
150
|
+
issues.push(`${driver.name}: ${skipReason}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const violations = checks.flatMap((c) => c.check(record));
|
|
155
|
+
// §4's golden fixtures: the only assertion on what the run actually
|
|
156
|
+
// produced. Every surface is held to them, not just the oracle — a
|
|
157
|
+
// surface can agree with the oracle frame-for-frame and still be wrong.
|
|
158
|
+
const goldens = checkGoldens(journey, record, { faultsApplied });
|
|
159
|
+
const goldenFailures = goldens.flatMap((g) => g.failures);
|
|
160
|
+
let diff = null;
|
|
161
|
+
let diffEmpty = true;
|
|
162
|
+
if (driver.name !== oracleDriver.name) {
|
|
163
|
+
const normalized = onlyServerToClient(normalizeRunRecord(record));
|
|
164
|
+
diff = diffNormalizedRecords(oracleNormalized, normalized);
|
|
165
|
+
diffEmpty = streamDiffIsEmpty(diff);
|
|
166
|
+
}
|
|
167
|
+
const ok = violations.length === 0 && diffEmpty && goldenFailures.length === 0;
|
|
168
|
+
if (violations.length > 0) {
|
|
169
|
+
issues.push(`${driver.name}: ${violations.length} invariant violation(s)`);
|
|
170
|
+
}
|
|
171
|
+
for (const golden of goldens) {
|
|
172
|
+
if (golden.failures.length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
issues.push(`${driver.name}: ${golden.kind} golden mismatch (${golden.failures.length})`);
|
|
175
|
+
}
|
|
176
|
+
if (!diffEmpty) {
|
|
177
|
+
issues.push(`${driver.name}: diverges from ${oracleDriver.name}`);
|
|
178
|
+
}
|
|
179
|
+
surfaces.push({
|
|
180
|
+
surface: driver.name,
|
|
181
|
+
ran: true,
|
|
182
|
+
status: record.status,
|
|
183
|
+
error: record.error,
|
|
184
|
+
violations,
|
|
185
|
+
goldens,
|
|
186
|
+
diffVsOracle: diff,
|
|
187
|
+
diffVsOracleEmpty: diffEmpty,
|
|
188
|
+
notApplicable: false,
|
|
189
|
+
faultsApplied,
|
|
190
|
+
ok
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const unknownFaults = Array.from(unknownFaultsSeen);
|
|
194
|
+
if (unknownFaults.length > 0) {
|
|
195
|
+
issues.push(`fault(s) requested with no registered module: ${unknownFaults.join(", ")}`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
journeyId: journey.manifest.name,
|
|
199
|
+
oracle: oracleDriver.name,
|
|
200
|
+
unknownInvariants,
|
|
201
|
+
unknownFaults,
|
|
202
|
+
surfaces,
|
|
203
|
+
verdict: { ok: surfaces.every((s) => s.ok) && unknownFaults.length === 0, issues }
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Human-readable rendering of a `CompareReport` for `nodetool reliability run`.
|
|
207
|
+
* `showDiff` (default `true`) controls whether a diverging surface's full
|
|
208
|
+
* per-channel stream diff is inlined under its summary line, or just noted —
|
|
209
|
+
* `nodetool reliability run`'s `--diff` flag maps straight to this. */
|
|
210
|
+
export function formatCompareReport(report, options = {}) {
|
|
211
|
+
const showDiff = options.showDiff ?? true;
|
|
212
|
+
const lines = [];
|
|
213
|
+
lines.push(`journey: ${report.journeyId} (oracle: ${report.oracle})`);
|
|
214
|
+
for (const s of report.surfaces) {
|
|
215
|
+
if (!s.ran) {
|
|
216
|
+
const mark = s.notApplicable ? "·" : "✗";
|
|
217
|
+
lines.push(` ${mark} ${s.surface}: ${s.skipReason}`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const mark = s.ok ? "✓" : "✗";
|
|
221
|
+
const bits = [String(s.status)];
|
|
222
|
+
if (s.error)
|
|
223
|
+
bits.push(`error: ${s.error}`);
|
|
224
|
+
if (s.faultsApplied.length > 0)
|
|
225
|
+
bits.push(`faults: ${s.faultsApplied.join(", ")}`);
|
|
226
|
+
if (s.violations.length > 0)
|
|
227
|
+
bits.push(`${s.violations.length} violation(s)`);
|
|
228
|
+
for (const golden of s.goldens) {
|
|
229
|
+
if (!golden.checked)
|
|
230
|
+
bits.push(`${golden.kind} golden skipped`);
|
|
231
|
+
else if (golden.failures.length > 0) {
|
|
232
|
+
bits.push(`${golden.kind} golden: ${golden.failures.length} mismatch(es)`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!s.diffVsOracleEmpty) {
|
|
236
|
+
bits.push(showDiff ? `diverges from ${report.oracle}` : `diverges from ${report.oracle} (--diff for detail)`);
|
|
237
|
+
}
|
|
238
|
+
lines.push(` ${mark} ${s.surface}: ${bits.join(" — ")}`);
|
|
239
|
+
for (const v of s.violations) {
|
|
240
|
+
lines.push(` [${v.invariant}] ${v.message}`);
|
|
241
|
+
}
|
|
242
|
+
for (const golden of s.goldens) {
|
|
243
|
+
if (!golden.checked && golden.skipReason) {
|
|
244
|
+
lines.push(` [${golden.kind}] ${golden.skipReason}`);
|
|
245
|
+
}
|
|
246
|
+
for (const failure of golden.failures) {
|
|
247
|
+
lines.push(` [${golden.kind}] ${failure}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (showDiff && !s.diffVsOracleEmpty && s.diffVsOracle) {
|
|
251
|
+
for (const line of formatStreamDiff(s.diffVsOracle).split("\n")) {
|
|
252
|
+
lines.push(` ${line}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (report.unknownInvariants.length > 0) {
|
|
257
|
+
lines.push(` ! unknown invariant(s) declared: ${report.unknownInvariants.join(", ")}`);
|
|
258
|
+
}
|
|
259
|
+
if (report.unknownFaults.length > 0) {
|
|
260
|
+
lines.push(` ! unknown fault(s) requested: ${report.unknownFaults.join(", ")}`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(report.verdict.ok
|
|
263
|
+
? "verdict: OK"
|
|
264
|
+
: `verdict: FAIL — ${report.verdict.issues.join("; ")}`);
|
|
265
|
+
return lines.join("\n");
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../src/compare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-node-channel stream diff (§4, §12).
|
|
3
|
+
*
|
|
4
|
+
* Grown from `packages/cli/src/debug/diff.ts`, which diffs two
|
|
5
|
+
* `DebugReport`s at the verdict/summary level (issues, token/cost deltas)
|
|
6
|
+
* for the debug harness's `--watch` mode. This module diffs two
|
|
7
|
+
* `NormalizedRunRecord`s at the *message* level, one node-channel at a time
|
|
8
|
+
* — because the actor model legitimately interleaves frames across nodes,
|
|
9
|
+
* a global stream diff would flag reorderings that are not bugs.
|
|
10
|
+
* `diffIsEmpty`/`formatDiff` there and `streamDiffIsEmpty`/`formatStreamDiff`
|
|
11
|
+
* here are deliberately parallel APIs; the debug-harness copy is untouched
|
|
12
|
+
* so its own tests and callers stay green.
|
|
13
|
+
*/
|
|
14
|
+
import type { NormalizedRunRecord } from "./normalize.js";
|
|
15
|
+
interface ChannelDiffEntry {
|
|
16
|
+
kind: "added" | "removed" | "changed" | "unchanged";
|
|
17
|
+
/** Index within the side the entry belongs to (baseline for removed/changed, candidate for added). */
|
|
18
|
+
baselineMessage?: Record<string, unknown>;
|
|
19
|
+
candidateMessage?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
interface ChannelDiff {
|
|
22
|
+
channel: string;
|
|
23
|
+
entries: ChannelDiffEntry[];
|
|
24
|
+
}
|
|
25
|
+
export interface StreamDiff {
|
|
26
|
+
channels: ChannelDiff[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Diffs two normalized records channel by channel. `baseline` is
|
|
30
|
+
* conventionally the kernel (oracle) surface (§12: "the kernel surface is
|
|
31
|
+
* the oracle"), but the function is symmetric in shape — either side can be
|
|
32
|
+
* either record.
|
|
33
|
+
*/
|
|
34
|
+
export declare function diffNormalizedRecords(baseline: NormalizedRunRecord, candidate: NormalizedRunRecord): StreamDiff;
|
|
35
|
+
/** True when no channel diverged — the C1 done-when: a record diffed against itself is empty. */
|
|
36
|
+
export declare function streamDiffIsEmpty(diff: StreamDiff): boolean;
|
|
37
|
+
export declare function formatStreamDiff(diff: StreamDiff): string;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/core/diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAsB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,UAAU,gBAAgB;IACxB,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;IACpD,sGAAsG;IACtG,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AA6ED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,mBAAmB,EAC7B,SAAS,EAAE,mBAAmB,GAC7B,UAAU,CAwBZ;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAE3D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAczD"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { stableStringify } from "./stable-json.js";
|
|
2
|
+
function canonicalKey(message) {
|
|
3
|
+
return stableStringify(message);
|
|
4
|
+
}
|
|
5
|
+
function groupByChannel(frames) {
|
|
6
|
+
const groups = new Map();
|
|
7
|
+
for (const frame of frames) {
|
|
8
|
+
const group = groups.get(frame.channel);
|
|
9
|
+
if (group)
|
|
10
|
+
group.push(frame);
|
|
11
|
+
else
|
|
12
|
+
groups.set(frame.channel, [frame]);
|
|
13
|
+
}
|
|
14
|
+
return groups;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Longest-common-subsequence diff over two message sequences (by canonical
|
|
18
|
+
* JSON equality), yielding `equal`/`remove`/`add` runs in original order.
|
|
19
|
+
* O(n·m); channels are short (one node's messages in one run), so this is
|
|
20
|
+
* cheap in practice.
|
|
21
|
+
*/
|
|
22
|
+
function lcsDiff(baseline, candidate) {
|
|
23
|
+
const bKeys = baseline.map(canonicalKey);
|
|
24
|
+
const cKeys = candidate.map(canonicalKey);
|
|
25
|
+
const n = baseline.length;
|
|
26
|
+
const m = candidate.length;
|
|
27
|
+
// dp[i][j] = length of LCS of bKeys[i:] and cKeys[j:]
|
|
28
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
29
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
30
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
31
|
+
dp[i][j] =
|
|
32
|
+
bKeys[i] === cKeys[j]
|
|
33
|
+
? dp[i + 1][j + 1] + 1
|
|
34
|
+
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const entries = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
let j = 0;
|
|
40
|
+
while (i < n && j < m) {
|
|
41
|
+
if (bKeys[i] === cKeys[j]) {
|
|
42
|
+
entries.push({
|
|
43
|
+
kind: "unchanged",
|
|
44
|
+
baselineMessage: baseline[i],
|
|
45
|
+
candidateMessage: candidate[j]
|
|
46
|
+
});
|
|
47
|
+
i++;
|
|
48
|
+
j++;
|
|
49
|
+
}
|
|
50
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
51
|
+
entries.push({ kind: "removed", baselineMessage: baseline[i] });
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
entries.push({ kind: "added", candidateMessage: candidate[j] });
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
while (i < n) {
|
|
60
|
+
entries.push({ kind: "removed", baselineMessage: baseline[i] });
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
while (j < m) {
|
|
64
|
+
entries.push({ kind: "added", candidateMessage: candidate[j] });
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Diffs two normalized records channel by channel. `baseline` is
|
|
71
|
+
* conventionally the kernel (oracle) surface (§12: "the kernel surface is
|
|
72
|
+
* the oracle"), but the function is symmetric in shape — either side can be
|
|
73
|
+
* either record.
|
|
74
|
+
*/
|
|
75
|
+
export function diffNormalizedRecords(baseline, candidate) {
|
|
76
|
+
const baselineGroups = groupByChannel(baseline.frames);
|
|
77
|
+
const candidateGroups = groupByChannel(candidate.frames);
|
|
78
|
+
const channels = new Set([
|
|
79
|
+
...baselineGroups.keys(),
|
|
80
|
+
...candidateGroups.keys()
|
|
81
|
+
]);
|
|
82
|
+
const result = [];
|
|
83
|
+
for (const channel of Array.from(channels).sort()) {
|
|
84
|
+
const baselineMessages = (baselineGroups.get(channel) ?? []).map((f) => f.message);
|
|
85
|
+
const candidateMessages = (candidateGroups.get(channel) ?? []).map((f) => f.message);
|
|
86
|
+
const entries = lcsDiff(baselineMessages, candidateMessages).filter((e) => e.kind !== "unchanged");
|
|
87
|
+
if (entries.length > 0) {
|
|
88
|
+
result.push({ channel, entries });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { channels: result };
|
|
92
|
+
}
|
|
93
|
+
/** True when no channel diverged — the C1 done-when: a record diffed against itself is empty. */
|
|
94
|
+
export function streamDiffIsEmpty(diff) {
|
|
95
|
+
return diff.channels.length === 0;
|
|
96
|
+
}
|
|
97
|
+
export function formatStreamDiff(diff) {
|
|
98
|
+
if (streamDiffIsEmpty(diff))
|
|
99
|
+
return "No divergence.";
|
|
100
|
+
const lines = [];
|
|
101
|
+
for (const channel of diff.channels) {
|
|
102
|
+
lines.push(`channel ${channel.channel}:`);
|
|
103
|
+
for (const entry of channel.entries) {
|
|
104
|
+
if (entry.kind === "removed") {
|
|
105
|
+
lines.push(` - ${canonicalKey(entry.baselineMessage)}`);
|
|
106
|
+
}
|
|
107
|
+
else if (entry.kind === "added") {
|
|
108
|
+
lines.push(` + ${canonicalKey(entry.candidateMessage)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/core/diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden-fixture assertions (§4): the `expected/outputs.json` and
|
|
3
|
+
* `expected/stream.shape.json` a journey opts into via
|
|
4
|
+
* `assertions.outputs` / `assertions.streamShape`.
|
|
5
|
+
*
|
|
6
|
+
* These are the only checks that pin down *what a run produced*. Invariants
|
|
7
|
+
* describe stream shape and lifecycle; the cross-surface diff only says the
|
|
8
|
+
* surfaces agree — both pass happily when every surface returns the same
|
|
9
|
+
* wrong answer. A journey that declares a golden must have it compared, or
|
|
10
|
+
* declaring it means nothing.
|
|
11
|
+
*/
|
|
12
|
+
import type { Journey } from "./journey.js";
|
|
13
|
+
import type { RunRecord } from "./record.js";
|
|
14
|
+
import { type NormalizedRunRecord } from "./normalize.js";
|
|
15
|
+
/** One golden entry, matching `expected/stream.shape.json`'s element shape. */
|
|
16
|
+
interface StreamShapeEntry {
|
|
17
|
+
channel: string;
|
|
18
|
+
message: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface GoldenCheckResult {
|
|
21
|
+
kind: "outputs" | "streamShape";
|
|
22
|
+
/** False when the journey didn't declare it, or the run couldn't be compared. */
|
|
23
|
+
checked: boolean;
|
|
24
|
+
/** Why it wasn't checked. Only set when `!checked`. */
|
|
25
|
+
skipReason?: string;
|
|
26
|
+
/** Human-readable mismatches; empty when the golden matched. */
|
|
27
|
+
failures: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A run's terminal outputs, keyed the way `expected/outputs.json` is: by an
|
|
31
|
+
* Output node's public `output_name`, last value wins (a node that streams
|
|
32
|
+
* several values ends on the one a client would display).
|
|
33
|
+
*/
|
|
34
|
+
export declare function terminalOutputsOf(record: RunRecord): Record<string, unknown>;
|
|
35
|
+
/** A run's normalized `server_to_client` stream in golden-fixture shape. */
|
|
36
|
+
export declare function streamShapeOf(record: RunRecord, normalized?: NormalizedRunRecord): StreamShapeEntry[];
|
|
37
|
+
/**
|
|
38
|
+
* Runs the golden comparisons a journey declared against one surface's record.
|
|
39
|
+
*
|
|
40
|
+
* Faulted runs are skipped rather than compared: a golden describes what the
|
|
41
|
+
* journey produces when nothing is broken, and every fault journey that opts
|
|
42
|
+
* into outputs (`host-disk-full-write`, `python-node-workflow`) declares its
|
|
43
|
+
* faults precisely because the run is *supposed* to fail under them. The skip
|
|
44
|
+
* is reported, never silent.
|
|
45
|
+
*/
|
|
46
|
+
export declare function checkGoldens(journey: Journey, record: RunRecord, options?: {
|
|
47
|
+
faultsApplied?: readonly string[];
|
|
48
|
+
}): GoldenCheckResult[];
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=golden.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"golden.d.ts","sourceRoot":"","sources":["../../src/core/golden.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAsB,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,+EAA+E;AAC/E,UAAU,gBAAgB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,GAAG,aAAa,CAAC;IAChC,iFAAiF;IACjF,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAKD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAW5E;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,EACjB,UAAU,GAAE,mBAAgD,GAC3D,gBAAgB,EAAE,CAIpB;AAyFD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GAClD,iBAAiB,EAAE,CA0CrB"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { normalizeRunRecord } from "./normalize.js";
|
|
2
|
+
import { stableStringify } from "./stable-json.js";
|
|
3
|
+
/** Max mismatching stream-shape entries reported before truncating. */
|
|
4
|
+
const MAX_REPORTED_ENTRIES = 10;
|
|
5
|
+
/**
|
|
6
|
+
* A run's terminal outputs, keyed the way `expected/outputs.json` is: by an
|
|
7
|
+
* Output node's public `output_name`, last value wins (a node that streams
|
|
8
|
+
* several values ends on the one a client would display).
|
|
9
|
+
*/
|
|
10
|
+
export function terminalOutputsOf(record) {
|
|
11
|
+
const outputs = {};
|
|
12
|
+
for (const frame of record.frames) {
|
|
13
|
+
if (frame.direction !== "server_to_client")
|
|
14
|
+
continue;
|
|
15
|
+
const message = frame.message;
|
|
16
|
+
if (message["type"] !== "output_update")
|
|
17
|
+
continue;
|
|
18
|
+
const name = message["output_name"];
|
|
19
|
+
if (typeof name !== "string")
|
|
20
|
+
continue;
|
|
21
|
+
outputs[name] = message["value"];
|
|
22
|
+
}
|
|
23
|
+
return outputs;
|
|
24
|
+
}
|
|
25
|
+
/** A run's normalized `server_to_client` stream in golden-fixture shape. */
|
|
26
|
+
export function streamShapeOf(record, normalized = normalizeRunRecord(record)) {
|
|
27
|
+
return normalized.frames
|
|
28
|
+
.filter((frame) => frame.direction === "server_to_client")
|
|
29
|
+
.map((frame) => ({ channel: frame.channel, message: frame.message }));
|
|
30
|
+
}
|
|
31
|
+
function compareOutputs(expected, record) {
|
|
32
|
+
const actual = terminalOutputsOf(record);
|
|
33
|
+
const expectedRecord = (expected ?? {});
|
|
34
|
+
const failures = [];
|
|
35
|
+
const keys = new Set([
|
|
36
|
+
...Object.keys(expectedRecord),
|
|
37
|
+
...Object.keys(actual)
|
|
38
|
+
]);
|
|
39
|
+
for (const key of Array.from(keys).sort()) {
|
|
40
|
+
const want = stableStringify(expectedRecord[key]);
|
|
41
|
+
const got = stableStringify(actual[key]);
|
|
42
|
+
if (want === got)
|
|
43
|
+
continue;
|
|
44
|
+
if (!(key in expectedRecord)) {
|
|
45
|
+
failures.push(`unexpected output "${key}": ${got}`);
|
|
46
|
+
}
|
|
47
|
+
else if (!(key in actual)) {
|
|
48
|
+
failures.push(`missing output "${key}" (expected ${want})`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
failures.push(`output "${key}": expected ${want}, got ${got}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return failures;
|
|
55
|
+
}
|
|
56
|
+
function byChannel(entries) {
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const serialized = stableStringify(entry.message);
|
|
60
|
+
const group = groups.get(entry.channel);
|
|
61
|
+
if (group)
|
|
62
|
+
group.push(serialized);
|
|
63
|
+
else
|
|
64
|
+
groups.set(entry.channel, [serialized]);
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Compared per node-channel, in channel order — the same rule the
|
|
70
|
+
* cross-surface diff uses (§4: "per-node-channel, not globally"). The actor
|
|
71
|
+
* model legitimately interleaves frames across nodes run to run, so a global
|
|
72
|
+
* index-wise comparison would flag reorderings that are not regressions;
|
|
73
|
+
* within one channel the order is the node's own and is asserted exactly.
|
|
74
|
+
*
|
|
75
|
+
* One limit is inherited from normalization: id placeholders are numbered by
|
|
76
|
+
* order of first sight (`IdMapper`), so a run where a *different* node emits
|
|
77
|
+
* first renumbers them and mismatches. The cross-surface diff has the same
|
|
78
|
+
* property; journeys whose node start order isn't structurally fixed should
|
|
79
|
+
* not declare `streamShape`.
|
|
80
|
+
*/
|
|
81
|
+
function compareStreamShape(expected, record, normalized) {
|
|
82
|
+
if (!Array.isArray(expected)) {
|
|
83
|
+
return ["expected/stream.shape.json is not an array of {channel, message}"];
|
|
84
|
+
}
|
|
85
|
+
const golden = byChannel(expected);
|
|
86
|
+
const actual = byChannel(streamShapeOf(record, normalized));
|
|
87
|
+
const failures = [];
|
|
88
|
+
const channels = Array.from(new Set([...golden.keys(), ...actual.keys()])).sort();
|
|
89
|
+
for (const channel of channels) {
|
|
90
|
+
const want = golden.get(channel) ?? [];
|
|
91
|
+
const got = actual.get(channel) ?? [];
|
|
92
|
+
for (let i = 0; i < Math.max(want.length, got.length); i++) {
|
|
93
|
+
if (failures.length >= MAX_REPORTED_ENTRIES) {
|
|
94
|
+
failures.push("… further mismatches not reported");
|
|
95
|
+
return failures;
|
|
96
|
+
}
|
|
97
|
+
if (want[i] === got[i])
|
|
98
|
+
continue;
|
|
99
|
+
if (want[i] === undefined) {
|
|
100
|
+
failures.push(`${channel}[${i}]: unexpected ${got[i]}`);
|
|
101
|
+
}
|
|
102
|
+
else if (got[i] === undefined) {
|
|
103
|
+
failures.push(`${channel}[${i}]: missing ${want[i]}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
failures.push(`${channel}[${i}]: expected ${want[i]}, got ${got[i]}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return failures;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Runs the golden comparisons a journey declared against one surface's record.
|
|
114
|
+
*
|
|
115
|
+
* Faulted runs are skipped rather than compared: a golden describes what the
|
|
116
|
+
* journey produces when nothing is broken, and every fault journey that opts
|
|
117
|
+
* into outputs (`host-disk-full-write`, `python-node-workflow`) declares its
|
|
118
|
+
* faults precisely because the run is *supposed* to fail under them. The skip
|
|
119
|
+
* is reported, never silent.
|
|
120
|
+
*/
|
|
121
|
+
export function checkGoldens(journey, record, options = {}) {
|
|
122
|
+
const { assertions } = journey.manifest;
|
|
123
|
+
const faults = options.faultsApplied ?? [];
|
|
124
|
+
const results = [];
|
|
125
|
+
const skipReason = faults.length > 0
|
|
126
|
+
? `not compared under fault(s): ${faults.join(", ")}`
|
|
127
|
+
: undefined;
|
|
128
|
+
// Normalize once — both goldens read from the same view.
|
|
129
|
+
const normalized = assertions.streamShape ? normalizeRunRecord(record) : null;
|
|
130
|
+
if (assertions.outputs) {
|
|
131
|
+
results.push(skipReason
|
|
132
|
+
? { kind: "outputs", checked: false, skipReason, failures: [] }
|
|
133
|
+
: {
|
|
134
|
+
kind: "outputs",
|
|
135
|
+
checked: true,
|
|
136
|
+
failures: compareOutputs(journey.expected.outputs, record)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (assertions.streamShape) {
|
|
140
|
+
results.push(skipReason
|
|
141
|
+
? { kind: "streamShape", checked: false, skipReason, failures: [] }
|
|
142
|
+
: {
|
|
143
|
+
kind: "streamShape",
|
|
144
|
+
checked: true,
|
|
145
|
+
failures: compareStreamShape(journey.expected.streamShape, record, normalized)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=golden.js.map
|