automategraph 1.5.1 → 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 +18 -5
- 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,381 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* LINT PACK — success dead-end rule (v1.2)
|
|
3
|
+
* ------------------------------------------------------------
|
|
4
|
+
* Closes the gap found while explaining the C-19 resilience
|
|
5
|
+
* fixture: a non-terminal node whose success path has nowhere to
|
|
6
|
+
* go (no `next`) used to pass linting as long as it declared an
|
|
7
|
+
* error transition.
|
|
8
|
+
*
|
|
9
|
+
* New code:
|
|
10
|
+
* SUCCESS_DEAD_END (error tier) — the node can only "continue"
|
|
11
|
+
* when it fails; a successful output is silently unreachable.
|
|
12
|
+
* DEAD_END_NODE stays for the total case (no transitions at all).
|
|
13
|
+
* The two are mutually exclusive: every node with no `next` gets
|
|
14
|
+
* exactly one of them.
|
|
15
|
+
* ============================================================ */
|
|
16
|
+
|
|
17
|
+
import { GraphExecutionEngine, lintGraph } from "../src/automategraph";
|
|
18
|
+
import { capture, constNode, g, logCollector, node, sleep } from "./helpers";
|
|
19
|
+
import { suite } from "./harness";
|
|
20
|
+
|
|
21
|
+
const lint = suite("lint", "Success dead-end rule");
|
|
22
|
+
|
|
23
|
+
const BIG = { timeoutMs: 20_000 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The exact C-19 resilience fixture (with its test gate), rebuilt per
|
|
27
|
+
* call so each test gets a fresh gate promise.
|
|
28
|
+
*/
|
|
29
|
+
function resilienceFixture(options: { slowNext?: string[] } = {}) {
|
|
30
|
+
let releaseFast: () => void = () => {};
|
|
31
|
+
const fallbackDone = new Promise<void>(resolve => { releaseFast = resolve; });
|
|
32
|
+
|
|
33
|
+
const definition = g({
|
|
34
|
+
nodes: [
|
|
35
|
+
node("start", () => 1, { next: ["slow-vendor", "fast-vendor"] }),
|
|
36
|
+
node("slow-vendor", async () => { await sleep(80); return "vendor-result"; }, {
|
|
37
|
+
timeoutMs: 25,
|
|
38
|
+
onThrow: "vendor-fallback",
|
|
39
|
+
...(options.slowNext ? { next: options.slowNext } : {})
|
|
40
|
+
}),
|
|
41
|
+
node("vendor-fallback", () => { releaseFast(); return "cached-price"; }, { next: ["decide"] }),
|
|
42
|
+
node("fast-vendor", async () => {
|
|
43
|
+
await Promise.race([fallbackDone, sleep(2000)]);
|
|
44
|
+
return "fresh-price";
|
|
45
|
+
}, { next: ["decide"] }),
|
|
46
|
+
node("decide", async context => {
|
|
47
|
+
await context.emit("pricing.decided", { fallback: context.outputs["vendor-fallback"] });
|
|
48
|
+
return "decided";
|
|
49
|
+
}, { next: ["finish"] }),
|
|
50
|
+
constNode("finish", "DONE")
|
|
51
|
+
],
|
|
52
|
+
entryNodes: ["start"],
|
|
53
|
+
terminalNodes: ["finish"]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return definition;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const codesFor = (definition: Parameters<typeof lintGraph>[0], nodeId: string): string[] =>
|
|
60
|
+
lintGraph(definition)
|
|
61
|
+
.filter(issue => issue.nodeId === nodeId)
|
|
62
|
+
.map(issue => issue.code);
|
|
63
|
+
|
|
64
|
+
/* ------------------------------ the rule ------------------------------ */
|
|
65
|
+
|
|
66
|
+
lint.t(
|
|
67
|
+
"the exact C-19 fixture is flagged: slow-vendor succeeds into a void",
|
|
68
|
+
async t => {
|
|
69
|
+
const issues = lintGraph(resilienceFixture());
|
|
70
|
+
const slowIssues = issues.filter(issue => issue.nodeId === "slow-vendor");
|
|
71
|
+
|
|
72
|
+
if (slowIssues.length !== 1) {
|
|
73
|
+
throw new Error(`slow-vendor issues=${slowIssues.map(i => i.code).join(",") || "none"}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const issue = slowIssues[0];
|
|
77
|
+
if (issue.code !== "SUCCESS_DEAD_END") throw new Error(issue.code);
|
|
78
|
+
if (issue.level !== "error") throw new Error(issue.level);
|
|
79
|
+
if (!issue.message.includes('"slow-vendor"')) throw new Error(issue.message);
|
|
80
|
+
if (!issue.message.includes("no next target")) throw new Error(issue.message);
|
|
81
|
+
|
|
82
|
+
// mutual exclusivity: the old code must not also fire here
|
|
83
|
+
if (codesFor(resilienceFixture(), "slow-vendor").includes("DEAD_END_NODE")) {
|
|
84
|
+
throw new Error("both rules fired for the same node");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
t.log("issue", `${issue.level}/${issue.code} @ ${issue.nodeId}`);
|
|
88
|
+
t.log(
|
|
89
|
+
"finding: a node can declare onThrow and still be unreachable on its success path — " +
|
|
90
|
+
"the previous DEAD_END_NODE rule only asked whether *any* transition existed"
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
BIG
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
lint.t(
|
|
97
|
+
"the fix suggested by the message clears the issue (next declared)",
|
|
98
|
+
async t => {
|
|
99
|
+
const fixed = resilienceFixture({ slowNext: ["decide"] });
|
|
100
|
+
const slowIssues = codesFor(fixed, "slow-vendor");
|
|
101
|
+
|
|
102
|
+
if (slowIssues.includes("SUCCESS_DEAD_END")) throw new Error("issue survived the fix");
|
|
103
|
+
if (slowIssues.length !== 0) throw new Error(slowIssues.join(","));
|
|
104
|
+
|
|
105
|
+
t.log("slow-vendor issues after adding next", slowIssues.length);
|
|
106
|
+
},
|
|
107
|
+
BIG
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
lint.t(
|
|
111
|
+
"declared terminal nodes are never flagged, even without next",
|
|
112
|
+
() => {
|
|
113
|
+
const definition = g({
|
|
114
|
+
nodes: [constNode("start", "S", { next: ["end"] }), constNode("end", "E")],
|
|
115
|
+
entryNodes: ["start"],
|
|
116
|
+
terminalNodes: ["end"]
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const issues = lintGraph(definition);
|
|
120
|
+
if (issues.some(issue => issue.nodeId === "end")) {
|
|
121
|
+
throw new Error(`terminal flagged: ${issues.map(i => i.code).join(",")}`);
|
|
122
|
+
}
|
|
123
|
+
if (issues.length !== 0) throw new Error(issues.map(i => i.code).join(","));
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
lint.t(
|
|
128
|
+
"a node with no transitions at all is DEAD_END_NODE, not SUCCESS_DEAD_END",
|
|
129
|
+
() => {
|
|
130
|
+
const definition = g({
|
|
131
|
+
nodes: [
|
|
132
|
+
constNode("start", "S", { next: ["dead", "end"] }),
|
|
133
|
+
constNode("dead", "D"),
|
|
134
|
+
constNode("end", "E")
|
|
135
|
+
],
|
|
136
|
+
entryNodes: ["start"],
|
|
137
|
+
terminalNodes: ["end"]
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const codes = codesFor(definition, "dead");
|
|
141
|
+
if (!codes.includes("DEAD_END_NODE")) throw new Error(`codes=${codes.join(",")}`);
|
|
142
|
+
if (codes.includes("SUCCESS_DEAD_END")) throw new Error("wrong rule fired");
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
lint.t(
|
|
147
|
+
"an ignored fallback does not count as a live error transition",
|
|
148
|
+
() => {
|
|
149
|
+
const definition = g({
|
|
150
|
+
nodes: [
|
|
151
|
+
constNode("start", "S", { next: ["orphanish", "end"] }),
|
|
152
|
+
// fallback declared, but the effective policy is CONTINUE ⇒ inert
|
|
153
|
+
node("orphanish", () => "O", { fallback: "end" }),
|
|
154
|
+
constNode("end", "E")
|
|
155
|
+
],
|
|
156
|
+
entryNodes: ["start"],
|
|
157
|
+
terminalNodes: ["end"]
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const codes = codesFor(definition, "orphanish");
|
|
161
|
+
if (!codes.includes("DEAD_END_NODE")) throw new Error(`codes=${codes.join(",")}`);
|
|
162
|
+
if (!codes.includes("FALLBACK_TARGET_IGNORED")) throw new Error(`codes=${codes.join(",")}`);
|
|
163
|
+
if (codes.includes("SUCCESS_DEAD_END")) throw new Error("inert fallback treated as live");
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
lint.t(
|
|
168
|
+
"a live fallback (policy FALLBACK) makes it SUCCESS_DEAD_END, not a total dead end",
|
|
169
|
+
() => {
|
|
170
|
+
const definition = g({
|
|
171
|
+
nodes: [
|
|
172
|
+
constNode("start", "S", { next: ["primary", "end"] }),
|
|
173
|
+
node("primary", () => "P", { errorPolicy: "FALLBACK", fallback: "end" }),
|
|
174
|
+
constNode("end", "E")
|
|
175
|
+
],
|
|
176
|
+
entryNodes: ["start"],
|
|
177
|
+
terminalNodes: ["end"]
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const codes = codesFor(definition, "primary");
|
|
181
|
+
if (!codes.includes("SUCCESS_DEAD_END")) throw new Error(`codes=${codes.join(",")}`);
|
|
182
|
+
if (codes.includes("DEAD_END_NODE")) throw new Error("total dead end wrongly reported");
|
|
183
|
+
if (codes.includes("FALLBACK_TARGET_IGNORED")) throw new Error("fallback should be live");
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
lint.t(
|
|
188
|
+
"onThrow alone is treated as a live error transition",
|
|
189
|
+
() => {
|
|
190
|
+
const definition = g({
|
|
191
|
+
nodes: [
|
|
192
|
+
constNode("start", "S", { next: ["risky", "end"] }),
|
|
193
|
+
node("risky", () => "R", { onThrow: "end" }),
|
|
194
|
+
constNode("end", "E")
|
|
195
|
+
],
|
|
196
|
+
entryNodes: ["start"],
|
|
197
|
+
terminalNodes: ["end"]
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const codes = codesFor(definition, "risky");
|
|
201
|
+
if (codes.length !== 1 || codes[0] !== "SUCCESS_DEAD_END") {
|
|
202
|
+
throw new Error(`codes=${codes.join(",")}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
lint.t(
|
|
208
|
+
"unreachable nodes with a success path stay unreachable-only (no noise)",
|
|
209
|
+
() => {
|
|
210
|
+
const definition = g({
|
|
211
|
+
nodes: [
|
|
212
|
+
constNode("start", "S", { next: ["end"] }),
|
|
213
|
+
constNode("end", "E"),
|
|
214
|
+
constNode("floating", "F", { next: ["end"] })
|
|
215
|
+
],
|
|
216
|
+
entryNodes: ["start"],
|
|
217
|
+
terminalNodes: ["end"]
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const codes = codesFor(definition, "floating");
|
|
221
|
+
if (!codes.includes("UNREACHABLE_NODE")) throw new Error(`codes=${codes.join(",")}`);
|
|
222
|
+
if (codes.includes("SUCCESS_DEAD_END")) throw new Error("unexpected success dead-end");
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
lint.t(
|
|
227
|
+
"every issue has a stable shape and a known code",
|
|
228
|
+
t => {
|
|
229
|
+
const known = new Set([
|
|
230
|
+
"DUPLICATE_ENTRY",
|
|
231
|
+
"DUPLICATE_TERMINAL",
|
|
232
|
+
"UNREACHABLE_NODE",
|
|
233
|
+
"UNREACHABLE_TERMINAL",
|
|
234
|
+
"DEAD_END_NODE",
|
|
235
|
+
"SUCCESS_DEAD_END",
|
|
236
|
+
"FALLBACK_TARGET_IGNORED"
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const messy = g({
|
|
240
|
+
nodes: [
|
|
241
|
+
constNode("start", "S", { next: ["risky", "orphan"] }),
|
|
242
|
+
node("risky", () => "R", { onThrow: "end" }),
|
|
243
|
+
constNode("orphan", "O"),
|
|
244
|
+
constNode("end", "E")
|
|
245
|
+
],
|
|
246
|
+
entryNodes: ["start", "start"],
|
|
247
|
+
terminalNodes: ["end"]
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const issues = lintGraph(messy);
|
|
251
|
+
if (issues.length === 0) throw new Error("expected issues");
|
|
252
|
+
|
|
253
|
+
for (const issue of issues) {
|
|
254
|
+
if (issue.level !== "error" && issue.level !== "warn") throw new Error(issue.level);
|
|
255
|
+
if (!known.has(issue.code)) throw new Error(`unknown code ${issue.code}`);
|
|
256
|
+
if (typeof issue.message !== "string" || issue.message.length === 0) throw new Error("empty");
|
|
257
|
+
if (typeof issue.nodeId !== "string" || issue.nodeId.length === 0) throw new Error("missing nodeId");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const codes = new Set(issues.map(issue => issue.code));
|
|
261
|
+
for (const expected of ["DUPLICATE_ENTRY", "SUCCESS_DEAD_END", "DEAD_END_NODE"]) {
|
|
262
|
+
if (!codes.has(expected as never)) throw new Error(`missing ${expected}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
t.log("codes observed", [...codes].join(", "));
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
/* --------------------------- engine integration --------------------------- */
|
|
270
|
+
|
|
271
|
+
lint.t(
|
|
272
|
+
"lint: \"warn\" logs SUCCESS_DEAD_END and still completes the C-19 run",
|
|
273
|
+
async t => {
|
|
274
|
+
const { lines, onLog } = logCollector();
|
|
275
|
+
const engine = new GraphExecutionEngine(resilienceFixture(), { lint: "warn", onLog });
|
|
276
|
+
|
|
277
|
+
const report = await engine.run();
|
|
278
|
+
|
|
279
|
+
if (report.status !== "COMPLETED") throw new Error(report.status);
|
|
280
|
+
if (!report.results.some(result => result.nodeId === "finish")) throw new Error("finish never ran");
|
|
281
|
+
|
|
282
|
+
const logged = lines.filter(line => line.message.includes("Graph lint [SUCCESS_DEAD_END]"));
|
|
283
|
+
if (logged.length !== 1) throw new Error(`lint log lines=${logged.length}`);
|
|
284
|
+
// the line carries the ISSUE severity ("error"); the mode only decides
|
|
285
|
+
// whether error-tier issues are enforced or merely reported
|
|
286
|
+
if (logged[0].level !== "error") throw new Error(logged[0].level);
|
|
287
|
+
|
|
288
|
+
const issues = report.lintIssues.filter(issue => issue.code === "SUCCESS_DEAD_END");
|
|
289
|
+
if (issues.length !== 1 || issues[0].nodeId !== "slow-vendor") {
|
|
290
|
+
throw new Error("lintIssues missing from the report");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
t.log("run status", report.status, "| issue logged at severity 'error' but not enforced in warn mode");
|
|
294
|
+
},
|
|
295
|
+
BIG
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
lint.t(
|
|
299
|
+
"lint: \"error\" rejects the C-19 graph before any node runs",
|
|
300
|
+
async t => {
|
|
301
|
+
const engine = new GraphExecutionEngine(resilienceFixture(), { lint: "error" });
|
|
302
|
+
const started = await engine.run();
|
|
303
|
+
|
|
304
|
+
if (started.status !== "REJECTED") throw new Error(started.status);
|
|
305
|
+
if (started.results.length !== 0) throw new Error(`ran ${started.results.length} nodes`);
|
|
306
|
+
if (Object.keys(started.outputs).length !== 0) throw new Error("outputs recorded");
|
|
307
|
+
if (!started.errors[0].message.includes("Graph lint reported")) {
|
|
308
|
+
throw new Error(started.errors[0].message);
|
|
309
|
+
}
|
|
310
|
+
if (!started.lintIssues.some(issue => issue.code === "SUCCESS_DEAD_END")) {
|
|
311
|
+
throw new Error("lintIssues missing");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
t.log("finding: lint enforcement is a pre-flight gate — the graph is rejected before " +
|
|
315
|
+
"initialization finishes, so no side effects from node callbacks can occur");
|
|
316
|
+
},
|
|
317
|
+
BIG
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
/* ------------------------------ regression ------------------------------ */
|
|
321
|
+
|
|
322
|
+
lint.t(
|
|
323
|
+
"the C-01 order-pipeline fixture flags audit as DEAD_END_NODE (documented gotcha)",
|
|
324
|
+
async t => {
|
|
325
|
+
const definition = g({
|
|
326
|
+
id: "order-pipeline",
|
|
327
|
+
version: "2.1.0",
|
|
328
|
+
nodes: [
|
|
329
|
+
node("validate", () => ({ orderId: 7, amount: 120 }), { next: ["fraud", "audit"] }),
|
|
330
|
+
node("fraud", () => "clean", { next: ["charge"] }),
|
|
331
|
+
node("charge", () => ({ charged: 120 }), { next: ["ship"] }),
|
|
332
|
+
node("ship", () => "SHIPPED", { next: ["notify"] }),
|
|
333
|
+
constNode("notify", "EMAIL_SENT"),
|
|
334
|
+
constNode("audit", "AUDITED")
|
|
335
|
+
],
|
|
336
|
+
entryNodes: ["validate"],
|
|
337
|
+
terminalNodes: ["notify"]
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const issues = lintGraph(definition);
|
|
341
|
+
|
|
342
|
+
const audit = issues.filter(issue => issue.nodeId === "audit");
|
|
343
|
+
if (audit.length !== 1 || audit[0].code !== "DEAD_END_NODE") {
|
|
344
|
+
throw new Error(`audit issues=${audit.map(i => i.code).join(",") || "none"}`);
|
|
345
|
+
}
|
|
346
|
+
if (issues.some(issue => issue.nodeId === "notify")) throw new Error("terminal flagged");
|
|
347
|
+
|
|
348
|
+
// the fixture still runs fine — linting is off by default
|
|
349
|
+
const report = await new GraphExecutionEngine(definition).run();
|
|
350
|
+
if (report.status !== "COMPLETED") throw new Error(report.status);
|
|
351
|
+
|
|
352
|
+
t.log("audit", `${audit[0].level}/${audit[0].code}`, "| run", report.status);
|
|
353
|
+
},
|
|
354
|
+
BIG
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
lint.t(
|
|
358
|
+
"a graph with no transition problems lints clean and runs",
|
|
359
|
+
async t => {
|
|
360
|
+
const definition = g({
|
|
361
|
+
nodes: [
|
|
362
|
+
node("a", () => "A", { next: ["b"] }),
|
|
363
|
+
node("b", () => "B", { next: ["c"] }),
|
|
364
|
+
constNode("c", "C")
|
|
365
|
+
],
|
|
366
|
+
entryNodes: ["a"],
|
|
367
|
+
terminalNodes: ["c"]
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const issues = lintGraph(definition);
|
|
371
|
+
if (issues.length !== 0) throw new Error(issues.map(issue => issue.code).join(","));
|
|
372
|
+
|
|
373
|
+
const engine = new GraphExecutionEngine(definition, { lint: "error" });
|
|
374
|
+
const captured = capture(engine, ["graph.finished"]);
|
|
375
|
+
const report = await engine.run();
|
|
376
|
+
|
|
377
|
+
if (report.status !== "COMPLETED") throw new Error(report.status);
|
|
378
|
+
if (captured.length !== 1) throw new Error("graph.finished missing");
|
|
379
|
+
},
|
|
380
|
+
BIG
|
|
381
|
+
);
|