automategraph 1.5.0 → 1.5.3
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/CHANGELOG.md +20 -0
- package/UPGRADE-PROPOSAL.md +141 -0
- package/docs/diagrams/c19-resilience-scenario.md +61 -0
- package/docs/diagrams/c19-resilience-scenario.mmd +28 -0
- package/docs/diagrams/c19-resilience-scenario.svg +181 -0
- package/examples/README.md +206 -0
- package/examples/branching-edges.ts +192 -0
- package/examples/branching-graph.ts +333 -0
- package/examples/branching-router.ts +475 -0
- package/examples/json-graph.ts +231 -0
- package/examples/json-scripts.ts +152 -0
- package/examples/order-flow.functions.cjs +32 -0
- package/examples/order-flow.graph.json +27 -0
- package/examples/order-flow.scripted.graph.json +68 -0
- package/js/examples/branching-edges.js +133 -0
- package/js/examples/branching-edges.js.map +1 -0
- package/js/examples/branching-graph.js +232 -0
- package/js/examples/branching-graph.js.map +1 -0
- package/js/examples/branching-router.js +305 -0
- package/js/examples/branching-router.js.map +1 -0
- package/js/examples/json-graph.js +197 -0
- package/js/examples/json-graph.js.map +1 -0
- package/js/examples/json-scripts.js +131 -0
- package/js/examples/json-scripts.js.map +1 -0
- package/js/tests/async.test.js +592 -0
- package/js/tests/async.test.js.map +1 -0
- package/js/tests/complex.test.js +972 -0
- package/js/tests/complex.test.js.map +1 -0
- package/js/tests/conformance.test.js +365 -0
- package/js/tests/conformance.test.js.map +1 -0
- package/js/tests/edges.test.js +702 -0
- package/js/tests/edges.test.js.map +1 -0
- package/js/tests/graphjson.test.js +504 -0
- package/js/tests/graphjson.test.js.map +1 -0
- package/js/tests/harness.js +192 -0
- package/js/tests/harness.js.map +1 -0
- package/js/tests/helpers.js +136 -0
- package/js/tests/helpers.js.map +1 -0
- package/js/tests/lint.test.js +317 -0
- package/js/tests/lint.test.js.map +1 -0
- package/js/tests/registry.test.js +705 -0
- package/js/tests/registry.test.js.map +1 -0
- package/js/tests/report.js +422 -0
- package/js/tests/report.js.map +1 -0
- package/js/tests/runner.js +67 -0
- package/js/tests/runner.js.map +1 -0
- package/js/tests/scripts.test.js +552 -0
- package/js/tests/scripts.test.js.map +1 -0
- package/js/tests/unit.test.js +1239 -0
- package/js/tests/unit.test.js.map +1 -0
- package/js/tests/upgrade.test.js +618 -0
- package/js/tests/upgrade.test.js.map +1 -0
- package/package.json +2 -9
- package/report/index.html +3727 -0
- package/report/report.json +3053 -0
- package/tests/async.test.ts +668 -0
- package/tests/complex.test.ts +1123 -0
- package/tests/conformance.test.ts +442 -0
- package/tests/edges.test.ts +848 -0
- package/tests/fixtures/scripts/bad-registration.cjs +4 -0
- package/tests/fixtures/scripts/broken.cjs +2 -0
- package/tests/fixtures/scripts/cjs-default.js +4 -0
- package/tests/fixtures/scripts/collide-a.cjs +4 -0
- package/tests/fixtures/scripts/collide-b.cjs +4 -0
- package/tests/fixtures/scripts/empty.cjs +2 -0
- package/tests/fixtures/scripts/esm-functions.mjs +10 -0
- package/tests/fixtures/scripts/esm-syntax.js +3 -0
- package/tests/fixtures/scripts/global-channel.cjs +6 -0
- package/tests/fixtures/scripts/global-channel.mjs +7 -0
- package/tests/fixtures/scripts/mismatch.cjs +4 -0
- package/tests/fixtures/scripts/needs-sibling.cjs +7 -0
- package/tests/fixtures/scripts/order.cjs +14 -0
- package/tests/fixtures/scripts/sibling-helper.cjs +3 -0
- package/tests/fixtures/scripts/throwing-global.mjs +6 -0
- package/tests/graphjson.test.ts +737 -0
- package/tests/harness.ts +291 -0
- package/tests/helpers.ts +216 -0
- package/tests/lint.test.ts +381 -0
- package/tests/registry.test.ts +1134 -0
- package/tests/report.ts +452 -0
- package/tests/runner.ts +58 -0
- package/tests/scripts.test.ts +970 -0
- package/tests/unit.test.ts +1192 -0
- package/tests/upgrade.test.ts +725 -0
- package/tools/server.mjs +152 -0
- package/tsconfig.browser.json +19 -0
- package/tsconfig.json +18 -0
- package/tsconfig.publish.json +13 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* STORAGE ADAPTER CONFORMANCE PACK
|
|
3
|
+
* ------------------------------------------------------------
|
|
4
|
+
* The adapter contract (src/storage-adapter.ts) is only worth
|
|
5
|
+
* something if it is enforced. These tests do three things:
|
|
6
|
+
*
|
|
7
|
+
* 1. prove the reference adapter (InMemoryStorage) passes
|
|
8
|
+
* 2. prove the checker itself has teeth — a deliberately
|
|
9
|
+
* broken adapter must produce specific violations
|
|
10
|
+
* 3. cover the wire format helpers (serialize/deserialize,
|
|
11
|
+
* stable output, corruption detection, comparison)
|
|
12
|
+
* ============================================================ */
|
|
13
|
+
|
|
14
|
+
import { InMemoryStorage, ExecutionSnapshot } from "../src/automategraph";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
StorageAdapter,
|
|
18
|
+
compareSnapshots,
|
|
19
|
+
defineStorageAdapter,
|
|
20
|
+
deserializeSnapshot,
|
|
21
|
+
serializeSnapshot,
|
|
22
|
+
snapshotShapeViolations,
|
|
23
|
+
storageAdapterViolations
|
|
24
|
+
} from "../src/storage-adapter";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
ConformanceReport,
|
|
28
|
+
StorageConformanceFixture,
|
|
29
|
+
conformanceSnapshot,
|
|
30
|
+
runStorageConformance
|
|
31
|
+
} from "../src/storage-conformance";
|
|
32
|
+
|
|
33
|
+
import { suite } from "./harness";
|
|
34
|
+
|
|
35
|
+
const conf = suite("conformance", "Storage adapter contract");
|
|
36
|
+
const wire = suite("conformance", "Snapshot wire format");
|
|
37
|
+
|
|
38
|
+
const BIG = { timeoutMs: 20_000 };
|
|
39
|
+
|
|
40
|
+
/* ------------------------- fixtures ------------------------- */
|
|
41
|
+
|
|
42
|
+
/** Reference adapter: the in-memory implementation shipped with the engine. */
|
|
43
|
+
const memoryFixture: StorageConformanceFixture = {
|
|
44
|
+
name: "InMemoryStorage",
|
|
45
|
+
create: () => new InMemoryStorage()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Same adapter, but able to inject a failing save (atomicity checks). */
|
|
49
|
+
class FaultInjectingAdapter implements StorageAdapter {
|
|
50
|
+
readonly id = "test.fault-injecting";
|
|
51
|
+
readonly version = "1.0.0";
|
|
52
|
+
readonly capabilities = {
|
|
53
|
+
atomic: true,
|
|
54
|
+
durable: false,
|
|
55
|
+
list: true,
|
|
56
|
+
delete: true,
|
|
57
|
+
concurrentExecutions: true
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
private readonly inner = new InMemoryStorage();
|
|
61
|
+
private failNext = false;
|
|
62
|
+
|
|
63
|
+
failNextSave(): void {
|
|
64
|
+
this.failNext = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async saveAtomic(snapshot: ExecutionSnapshot): Promise<void> {
|
|
68
|
+
if (this.failNext) {
|
|
69
|
+
this.failNext = false;
|
|
70
|
+
// Correct behaviour: reject WITHOUT touching the stored snapshot.
|
|
71
|
+
throw new Error("injected write failure");
|
|
72
|
+
}
|
|
73
|
+
return this.inner.saveAtomic(snapshot);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
load(executionId: string) {
|
|
77
|
+
return this.inner.load(executionId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
listExecutionIds(): Promise<string[]> {
|
|
81
|
+
return this.inner.listExecutionIds();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
delete(executionId: string): Promise<boolean> {
|
|
85
|
+
return this.inner.delete(executionId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const faultFixture: StorageConformanceFixture = {
|
|
90
|
+
name: "FaultInjectingAdapter",
|
|
91
|
+
create: () => new FaultInjectingAdapter(),
|
|
92
|
+
failNextSave: adapter => (adapter as FaultInjectingAdapter).failNextSave()
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Deliberately broken adapter: aliases snapshots on save AND load,
|
|
97
|
+
* ignores deletes and returns an empty list. The checker must catch all
|
|
98
|
+
* of it — otherwise "conformance" means nothing.
|
|
99
|
+
*/
|
|
100
|
+
class BrokenAdapter implements StorageAdapter {
|
|
101
|
+
readonly id = "test.broken";
|
|
102
|
+
readonly version = "0.0.1";
|
|
103
|
+
readonly capabilities = { list: true, delete: true };
|
|
104
|
+
|
|
105
|
+
private readonly store = new Map<string, ExecutionSnapshot>();
|
|
106
|
+
|
|
107
|
+
async saveAtomic(snapshot: ExecutionSnapshot): Promise<void> {
|
|
108
|
+
this.store.set(snapshot.executionId, snapshot); // no copy: aliasing bug
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async load(executionId: string): Promise<ExecutionSnapshot | null> {
|
|
112
|
+
return this.store.get(executionId) ?? null; // no copy: aliasing bug
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async listExecutionIds(): Promise<string[]> {
|
|
116
|
+
return []; // broken
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async delete(): Promise<boolean> {
|
|
120
|
+
return false; // broken
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Commits first, then fails — a torn write. */
|
|
125
|
+
class TornSaveAdapter implements StorageAdapter {
|
|
126
|
+
readonly id = "test.torn-save";
|
|
127
|
+
readonly version = "0.0.1";
|
|
128
|
+
readonly capabilities = { atomic: false, list: true, delete: true };
|
|
129
|
+
|
|
130
|
+
private readonly inner = new InMemoryStorage();
|
|
131
|
+
private failNext = false;
|
|
132
|
+
|
|
133
|
+
failNextSave(): void {
|
|
134
|
+
this.failNext = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async saveAtomic(snapshot: ExecutionSnapshot): Promise<void> {
|
|
138
|
+
await this.inner.saveAtomic(snapshot); // committed too early
|
|
139
|
+
if (this.failNext) {
|
|
140
|
+
this.failNext = false;
|
|
141
|
+
throw new Error("failure after commit");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
load(executionId: string) {
|
|
146
|
+
return this.inner.load(executionId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
listExecutionIds() {
|
|
150
|
+
return this.inner.listExecutionIds();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
delete(executionId: string) {
|
|
154
|
+
return this.inner.delete(executionId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tornFixture: StorageConformanceFixture = {
|
|
159
|
+
name: "TornSaveAdapter",
|
|
160
|
+
create: () => new TornSaveAdapter(),
|
|
161
|
+
failNextSave: adapter => (adapter as TornSaveAdapter).failNextSave()
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const checks = (report: ConformanceReport, code: string): boolean =>
|
|
165
|
+
report.violations.some(violation => violation.code === code);
|
|
166
|
+
|
|
167
|
+
/* ------------------------- tests ------------------------- */
|
|
168
|
+
|
|
169
|
+
conf.t(
|
|
170
|
+
"InMemoryStorage passes the full conformance suite with zero violations",
|
|
171
|
+
async t => {
|
|
172
|
+
const report = await runStorageConformance(memoryFixture);
|
|
173
|
+
|
|
174
|
+
if (!report.passed) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`violations: ${report.violations.map(v => `${v.code}: ${v.message}`).join(" | ")}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (report.adapterId !== "automategraph.memory-storage") throw new Error(report.adapterId);
|
|
180
|
+
if (report.adapterVersion !== "1.1.0") throw new Error(report.adapterVersion);
|
|
181
|
+
if (report.checks < 10) throw new Error(`checks=${report.checks}`);
|
|
182
|
+
if (report.violations.length !== 0) throw new Error("expected zero violations");
|
|
183
|
+
|
|
184
|
+
// the memory adapter cannot inject faults, so atomicity is reported as
|
|
185
|
+
// skipped rather than silently assumed to hold
|
|
186
|
+
if (!report.skipped.some(skip => skip.startsWith("FAILED_SAVE_PRESERVES_PREVIOUS"))) {
|
|
187
|
+
throw new Error("atomicity should be skipped when no fault injection exists");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
t.log("checks executed", report.checks);
|
|
191
|
+
t.log("skipped", report.skipped.length, report.skipped);
|
|
192
|
+
},
|
|
193
|
+
BIG
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
conf.t(
|
|
197
|
+
"an adapter with fault injection proves atomicity instead of skipping it",
|
|
198
|
+
async t => {
|
|
199
|
+
const report = await runStorageConformance(faultFixture);
|
|
200
|
+
|
|
201
|
+
if (!report.passed) {
|
|
202
|
+
throw new Error(report.violations.map(v => v.message).join(" | "));
|
|
203
|
+
}
|
|
204
|
+
if (report.skipped.some(skip => skip.startsWith("FAILED_SAVE_PRESERVES_PREVIOUS"))) {
|
|
205
|
+
throw new Error("atomicity check was skipped despite failNextSave()");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
t.log("checks executed", report.checks);
|
|
209
|
+
t.log("skipped", report.skipped);
|
|
210
|
+
},
|
|
211
|
+
BIG
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
conf.t(
|
|
215
|
+
"the checker catches aliasing, broken listing and broken deletion",
|
|
216
|
+
async t => {
|
|
217
|
+
const report = await runStorageConformance({
|
|
218
|
+
name: "BrokenAdapter",
|
|
219
|
+
create: () => new BrokenAdapter()
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (report.passed) throw new Error("the broken adapter was wrongly accepted");
|
|
223
|
+
|
|
224
|
+
for (const code of [
|
|
225
|
+
"NO_ALIASING_ON_SAVE",
|
|
226
|
+
"NO_ALIASING_ON_LOAD",
|
|
227
|
+
"LIST",
|
|
228
|
+
"DELETE_IDEMPOTENT"
|
|
229
|
+
]) {
|
|
230
|
+
if (!checks(report, code)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`missing ${code}; got ${report.violations.map(v => v.code).join(",")}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
t.log("violations detected", report.violations.map(v => v.code).join(", "));
|
|
238
|
+
},
|
|
239
|
+
BIG
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
conf.t(
|
|
243
|
+
"the checker catches a torn write that commits before failing",
|
|
244
|
+
async t => {
|
|
245
|
+
const report = await runStorageConformance(tornFixture);
|
|
246
|
+
|
|
247
|
+
if (report.passed) throw new Error("torn writes were wrongly accepted");
|
|
248
|
+
if (!checks(report, "FAILED_SAVE_PRESERVES_PREVIOUS")) {
|
|
249
|
+
throw new Error(`violations=${report.violations.map(v => v.code).join(",")}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
t.log("finding: the conformance suite distinguishes 'rejects' from 'rejects without " +
|
|
253
|
+
"corrupting state' — adapters must checkpoint before commit, not after");
|
|
254
|
+
},
|
|
255
|
+
BIG
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
conf.t(
|
|
259
|
+
"missing required members are reported as SHAPE violations",
|
|
260
|
+
async t => {
|
|
261
|
+
const incomplete = { id: "x", version: "1" } as unknown as StorageAdapter;
|
|
262
|
+
const violations = storageAdapterViolations(incomplete);
|
|
263
|
+
|
|
264
|
+
if (!violations.some(violation => violation.includes("saveAtomic"))) throw new Error("saveAtomic");
|
|
265
|
+
if (!violations.some(violation => violation.includes("load"))) throw new Error("load");
|
|
266
|
+
|
|
267
|
+
const report = await runStorageConformance({
|
|
268
|
+
name: "Incomplete",
|
|
269
|
+
create: () => incomplete
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (report.passed) throw new Error("incomplete adapter accepted");
|
|
273
|
+
if (!checks(report, "SHAPE")) throw new Error("SHAPE not reported");
|
|
274
|
+
},
|
|
275
|
+
BIG
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
conf.t(
|
|
279
|
+
"an adapter that claims a capability it does not implement is rejected (defineStorageAdapter)",
|
|
280
|
+
() => {
|
|
281
|
+
const liar = {
|
|
282
|
+
id: "test.liar",
|
|
283
|
+
version: "1.0.0",
|
|
284
|
+
capabilities: { list: true },
|
|
285
|
+
saveAtomic: async () => undefined,
|
|
286
|
+
load: async () => null
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
defineStorageAdapter(liar as unknown as StorageAdapter);
|
|
291
|
+
throw new Error("expected throw");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!(error as { code?: string }).code) throw error;
|
|
294
|
+
if ((error as { code: string }).code !== "STORAGE_ADAPTER_INVALID") {
|
|
295
|
+
throw new Error((error as { code: string }).code);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// and a consistent adapter is accepted, identity-preserved
|
|
300
|
+
const storage = new InMemoryStorage();
|
|
301
|
+
if (defineStorageAdapter(storage) !== storage) throw new Error("identity broken");
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
conf.t(
|
|
306
|
+
"the engine accepts any conformant adapter as its storage plugin",
|
|
307
|
+
async t => {
|
|
308
|
+
const { GraphExecutionEngine } = await import("../src/automategraph");
|
|
309
|
+
const { g, node: build } = await import("./helpers");
|
|
310
|
+
|
|
311
|
+
const adapter = new FaultInjectingAdapter();
|
|
312
|
+
const definition = g({
|
|
313
|
+
nodes: [build("a", () => "A", { next: ["b"] }), build("b", () => "B")],
|
|
314
|
+
entryNodes: ["a"],
|
|
315
|
+
terminalNodes: ["b"]
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const report = await new GraphExecutionEngine(definition, { storage: adapter }).run();
|
|
319
|
+
|
|
320
|
+
if (report.status !== "COMPLETED") throw new Error(report.status);
|
|
321
|
+
|
|
322
|
+
const stored = await adapter.load(report.executionId);
|
|
323
|
+
if (!stored) throw new Error("execution was not persisted");
|
|
324
|
+
if (stored.graphId !== definition.id) throw new Error("stored snapshot mismatch");
|
|
325
|
+
if (stored.pluginVersions && Object.keys(stored.pluginVersions).length !== 0) {
|
|
326
|
+
throw new Error("unexpected plugin versions");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
t.log("snapshots stored", (await adapter.listExecutionIds()).length);
|
|
330
|
+
},
|
|
331
|
+
BIG
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
/* ------------------------- wire format ------------------------- */
|
|
335
|
+
|
|
336
|
+
wire.t(
|
|
337
|
+
"serializeSnapshot is deterministic and round-trips through JSON",
|
|
338
|
+
() => {
|
|
339
|
+
const snapshot = conformanceSnapshot("wire-1");
|
|
340
|
+
const first = serializeSnapshot(snapshot);
|
|
341
|
+
const second = serializeSnapshot(conformanceSnapshot("wire-1"));
|
|
342
|
+
|
|
343
|
+
if (first !== second) throw new Error("serialization is not deterministic");
|
|
344
|
+
|
|
345
|
+
const revived = deserializeSnapshot(first);
|
|
346
|
+
const differences = compareSnapshots(snapshot, revived);
|
|
347
|
+
if (differences.length > 0) throw new Error(differences.join(","));
|
|
348
|
+
|
|
349
|
+
// key order of the source object must not matter
|
|
350
|
+
const shuffled: ExecutionSnapshot = {
|
|
351
|
+
timestamp: snapshot.timestamp,
|
|
352
|
+
pluginVersions: snapshot.pluginVersions,
|
|
353
|
+
errors: snapshot.errors,
|
|
354
|
+
results: snapshot.results,
|
|
355
|
+
outputs: snapshot.outputs,
|
|
356
|
+
status: snapshot.status,
|
|
357
|
+
graphVersion: snapshot.graphVersion,
|
|
358
|
+
graphId: snapshot.graphId,
|
|
359
|
+
executionId: snapshot.executionId
|
|
360
|
+
};
|
|
361
|
+
if (serializeSnapshot(shuffled) !== first) throw new Error("key order changed the bytes");
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
wire.t(
|
|
366
|
+
"corrupt or unsupported snapshots are rejected with typed errors",
|
|
367
|
+
() => {
|
|
368
|
+
for (const [payload, expectedCode] of [
|
|
369
|
+
["{not json", "STORAGE_CORRUPT_SNAPSHOT"],
|
|
370
|
+
[JSON.stringify({ hello: "world" }), "STORAGE_CORRUPT_SNAPSHOT"],
|
|
371
|
+
[
|
|
372
|
+
JSON.stringify({ format: "automategraph.snapshot", formatVersion: 99, snapshot: {} }),
|
|
373
|
+
"STORAGE_FORMAT_UNSUPPORTED"
|
|
374
|
+
],
|
|
375
|
+
[
|
|
376
|
+
JSON.stringify({
|
|
377
|
+
format: "automategraph.snapshot",
|
|
378
|
+
formatVersion: 1,
|
|
379
|
+
snapshot: { executionId: "e" }
|
|
380
|
+
}),
|
|
381
|
+
"STORAGE_CORRUPT_SNAPSHOT"
|
|
382
|
+
]
|
|
383
|
+
] as Array<[string, string]>) {
|
|
384
|
+
try {
|
|
385
|
+
deserializeSnapshot(payload);
|
|
386
|
+
throw new Error(`expected throw for ${payload.slice(0, 40)}`);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
const code = (error as { code?: string }).code;
|
|
389
|
+
if (code !== expectedCode) throw new Error(`got ${String(code)}, expected ${expectedCode}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
wire.t(
|
|
396
|
+
"snapshotShapeViolations pinpoints every malformed field",
|
|
397
|
+
() => {
|
|
398
|
+
const violations = snapshotShapeViolations({
|
|
399
|
+
executionId: "e",
|
|
400
|
+
graphId: "",
|
|
401
|
+
graphVersion: "1",
|
|
402
|
+
status: "NOT_A_STATUS",
|
|
403
|
+
outputs: null,
|
|
404
|
+
results: [{ nodeId: "a" }],
|
|
405
|
+
errors: [{}],
|
|
406
|
+
pluginVersions: {},
|
|
407
|
+
timestamp: "2026-01-01T00:00:00.000Z"
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const joined = violations.join(" | ");
|
|
411
|
+
for (const expected of [
|
|
412
|
+
"graphId",
|
|
413
|
+
"status must be a GraphStatus",
|
|
414
|
+
"outputs must be an object",
|
|
415
|
+
"results[0].activationId",
|
|
416
|
+
"results[0].attempt",
|
|
417
|
+
"errors[0]"
|
|
418
|
+
]) {
|
|
419
|
+
if (!joined.includes(expected)) throw new Error(`missing "${expected}" in: ${joined}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
wire.t(
|
|
425
|
+
"compareSnapshots reports structural differences precisely",
|
|
426
|
+
() => {
|
|
427
|
+
const base = conformanceSnapshot("wire-compare");
|
|
428
|
+
if (compareSnapshots(base, conformanceSnapshot("wire-compare")).length !== 0) {
|
|
429
|
+
throw new Error("identical snapshots reported as different");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const changed: ExecutionSnapshot = {
|
|
433
|
+
...base,
|
|
434
|
+
status: "COMPLETED",
|
|
435
|
+
outputs: { a: { n: 2 } }
|
|
436
|
+
};
|
|
437
|
+
const differences = compareSnapshots(base, changed);
|
|
438
|
+
|
|
439
|
+
if (!differences.includes("status differs")) throw new Error(differences.join(","));
|
|
440
|
+
if (!differences.includes("outputs differs")) throw new Error(differences.join(","));
|
|
441
|
+
}
|
|
442
|
+
);
|