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
package/tests/report.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* Static web report generator — writes:
|
|
3
|
+
* report/index.html (self-contained dashboard, no network deps)
|
|
4
|
+
* report/report.json (machine-readable full results)
|
|
5
|
+
*
|
|
6
|
+
* v1.1: fully kind-driven. Adding a new test pack (see TEST_KINDS
|
|
7
|
+
* in harness.ts) requires no changes here.
|
|
8
|
+
* ============================================================ */
|
|
9
|
+
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
|
|
13
|
+
import { KindTotals, RunSummary, TestKind, TestResultRecord } from "./harness";
|
|
14
|
+
|
|
15
|
+
const KIND_META: Record<TestKind, { badge: string; blurb: string; color: string }> = {
|
|
16
|
+
unit: {
|
|
17
|
+
badge: "UNIT",
|
|
18
|
+
blurb: "constructor validation, error model, event bus, storage, freezing, lifecycle, scheduler, retry, cancellation",
|
|
19
|
+
color: "#7c9cff"
|
|
20
|
+
},
|
|
21
|
+
integration: {
|
|
22
|
+
badge: "COMPLEX",
|
|
23
|
+
blurb: "end-to-end pipelines, failure semantics, concurrency & scale, listeners, plugin lifecycle, cancellation",
|
|
24
|
+
color: "#c084fc"
|
|
25
|
+
},
|
|
26
|
+
async: {
|
|
27
|
+
badge: "ASYNC",
|
|
28
|
+
blurb: "async node callbacks across every path: success, retry, ONTHROW, timeout, abort, fan-out, checkpoints",
|
|
29
|
+
color: "#4fd1c5"
|
|
30
|
+
},
|
|
31
|
+
upgrade: {
|
|
32
|
+
badge: "UPGRADE",
|
|
33
|
+
blurb: "Wave 1 (v1.1) guarantees: drain deadline, completion policies, sticky FAIL_FAST, FALLBACK routing, budgets, linting, isomorphic build",
|
|
34
|
+
color: "#f0b429"
|
|
35
|
+
},
|
|
36
|
+
conformance: {
|
|
37
|
+
badge: "CONFORMANCE",
|
|
38
|
+
blurb: "the storage adapter contract is enforced by a checker with teeth, plus the snapshot wire format",
|
|
39
|
+
color: "#31c48d"
|
|
40
|
+
},
|
|
41
|
+
lint: {
|
|
42
|
+
badge: "LINT",
|
|
43
|
+
blurb: "the SUCCESS_DEAD_END rule: non-terminal nodes whose success path has nowhere to go",
|
|
44
|
+
color: "#fb923c"
|
|
45
|
+
},
|
|
46
|
+
edges: {
|
|
47
|
+
badge: "EDGES",
|
|
48
|
+
blurb: "typed edges: deep-equality/t predicate routing, else edges, parallel edges and explicit Next/goto routing",
|
|
49
|
+
color: "#38bdf8"
|
|
50
|
+
},
|
|
51
|
+
registry: {
|
|
52
|
+
badge: "REGISTRY",
|
|
53
|
+
blurb: "the function registry: run/test by name, lazy resolution, UNKNOWN_FUNCTION, fingerprints",
|
|
54
|
+
color: "#a78bfa"
|
|
55
|
+
},
|
|
56
|
+
graphjson: {
|
|
57
|
+
badge: "JSON",
|
|
58
|
+
blurb: "graph data files: parsing a document into a definition, total validation, engine from JSON",
|
|
59
|
+
color: "#34d399"
|
|
60
|
+
},
|
|
61
|
+
scripts: {
|
|
62
|
+
badge: "SCRIPTS",
|
|
63
|
+
blurb: "script loading: exported and self-registered functions, format detection, process-global namespace freeze",
|
|
64
|
+
color: "#f472b6"
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const UNIT_CATEGORIES: Array<[string, string]> = [
|
|
69
|
+
["Graph validation", "Constructor-time rejection of malformed graph definitions"],
|
|
70
|
+
["Error classes & utilities", "Error hierarchy, codes, id/time generation, throw coercion"],
|
|
71
|
+
["EventBus", "Required vs non-required listeners, ordering, isolation, teardown"],
|
|
72
|
+
["InMemoryStorage", "Atomic snapshot semantics and copy isolation"],
|
|
73
|
+
["Graph freezing", "Immutability of definitions after construction"],
|
|
74
|
+
["Engine lifecycle", "State machine, single-run guard, plugin init/shutdown"],
|
|
75
|
+
["Execution & scheduler", "Scheduling, fan-out, contexts, events, checkpoints, settlement"],
|
|
76
|
+
["Retry", "RETRY policy looping, delays, attempt accounting"],
|
|
77
|
+
["Cancellation", "Cooperative abort, cancellation events, terminal no-ops"]
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const escapeHtml = (value: string): string =>
|
|
81
|
+
value
|
|
82
|
+
.replace(/&/g, "&")
|
|
83
|
+
.replace(/</g, "<")
|
|
84
|
+
.replace(/>/g, ">")
|
|
85
|
+
.replace(/"/g, """)
|
|
86
|
+
.replace(/'/g, "'");
|
|
87
|
+
|
|
88
|
+
export function findingsOf(record: TestResultRecord): string[] {
|
|
89
|
+
const derived = record.logs
|
|
90
|
+
.filter(line => line.startsWith("finding:"))
|
|
91
|
+
.map(line => line.replace(/^finding:\s*/, ""));
|
|
92
|
+
if (record.finding) derived.unshift(record.finding);
|
|
93
|
+
return [...new Set(derived)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderTestRow(record: TestResultRecord): string {
|
|
97
|
+
const findings = findingsOf(record);
|
|
98
|
+
const isFinding = findings.length > 0;
|
|
99
|
+
const classes = [
|
|
100
|
+
"row",
|
|
101
|
+
record.status === "passed" ? "ok" : "bad",
|
|
102
|
+
record.kind
|
|
103
|
+
].join(" ");
|
|
104
|
+
|
|
105
|
+
const details: string[] = [];
|
|
106
|
+
|
|
107
|
+
if (isFinding) {
|
|
108
|
+
details.push(
|
|
109
|
+
`<div class="detail finding"><span class="tag">findings</span><ul>` +
|
|
110
|
+
findings.map(line => `<li>${escapeHtml(line)}</li>`).join("") +
|
|
111
|
+
"</ul></div>"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (record.logs.length > 0) {
|
|
116
|
+
details.push(
|
|
117
|
+
`<div class="detail"><span class="tag">observations</span><ul>` +
|
|
118
|
+
record.logs.map(line => `<li>${escapeHtml(line)}</li>`).join("") +
|
|
119
|
+
"</ul></div>"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (record.error) {
|
|
124
|
+
details.push(
|
|
125
|
+
`<div class="detail error"><span class="tag">error</span>` +
|
|
126
|
+
`<div class="errline">${escapeHtml(record.error.name)}: ${escapeHtml(record.error.message)}</div>` +
|
|
127
|
+
(record.error.stack
|
|
128
|
+
? `<pre>${escapeHtml(record.error.stack.split("\n").slice(0, 8).join("\n"))}</pre>`
|
|
129
|
+
: "") +
|
|
130
|
+
"</div>"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `
|
|
135
|
+
<details class="${classes}" data-kind="${record.kind}" data-status="${record.status}" data-finding="${isFinding}" data-search="${escapeHtml(
|
|
136
|
+
(record.id + " " + record.category + " " + record.name + " " + findings.join(" ")).toLowerCase()
|
|
137
|
+
)}">
|
|
138
|
+
<summary>
|
|
139
|
+
<span class="pill ${record.status}">${record.status === "passed" ? "PASS" : "FAIL"}</span>
|
|
140
|
+
<span class="id">${escapeHtml(record.id)}</span>
|
|
141
|
+
<span class="cat">${escapeHtml(record.category)}</span>
|
|
142
|
+
<span class="name">${escapeHtml(record.name)}</span>
|
|
143
|
+
${isFinding ? '<span class="star" title="documents an engine semantic">finding</span>' : ""}
|
|
144
|
+
<span class="ms">${record.durationMs} ms</span>
|
|
145
|
+
</summary>
|
|
146
|
+
${details.join("") || '<div class="detail muted">No additional observations.</div>'}
|
|
147
|
+
</details>`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function bar(label: string, kind: TestKind, totals: KindTotals, durationMs: number): string {
|
|
151
|
+
const pct = totals.total === 0 ? 0 : Math.round((totals.passed / totals.total) * 100);
|
|
152
|
+
return `
|
|
153
|
+
<div class="barrow">
|
|
154
|
+
<div class="barlabel"><span class="badge ${kind}">${KIND_META[kind]?.badge ?? kind.toUpperCase()}</span> ${escapeHtml(label)}</div>
|
|
155
|
+
<div class="track"><div class="fill ${totals.failed > 0 ? "warn" : ""}" style="width:${pct}%">${pct}%</div></div>
|
|
156
|
+
<div class="barmeta">${totals.passed}/${totals.total} · ${durationMs} ms</div>
|
|
157
|
+
</div>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function writeReport(summary: RunSummary, workspaceRoot: string): { htmlPath: string; jsonPath: string } {
|
|
161
|
+
const reportDir = path.join(workspaceRoot, "report");
|
|
162
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
163
|
+
|
|
164
|
+
const jsonPath = path.join(reportDir, "report.json");
|
|
165
|
+
fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), "utf8");
|
|
166
|
+
|
|
167
|
+
const { totals } = summary;
|
|
168
|
+
const kinds = Object.keys(KIND_META) as TestKind[];
|
|
169
|
+
const findings = summary.tests.flatMap(test =>
|
|
170
|
+
findingsOf(test).map(text => ({ test, text }))
|
|
171
|
+
);
|
|
172
|
+
const failedTests = summary.tests.filter(test => test.status === "failed");
|
|
173
|
+
|
|
174
|
+
const testsOfKind = (kind: TestKind): TestResultRecord[] =>
|
|
175
|
+
summary.tests.filter(test => test.kind === kind);
|
|
176
|
+
|
|
177
|
+
const categoryDurations = new Map(
|
|
178
|
+
summary.byCategory.map(entry => [`${entry.kind}::${entry.category}`, entry.durationMs])
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const kindSummary = kinds
|
|
182
|
+
.map(kind => `${totals.byKind[kind].total} ${KIND_META[kind].badge.toLowerCase()}`)
|
|
183
|
+
.join(" + ");
|
|
184
|
+
|
|
185
|
+
const processHygiene =
|
|
186
|
+
summary.unhandledRejections.length === 0
|
|
187
|
+
? `<span class="pill passed">clean</span> no unhandled rejections observed during the run`
|
|
188
|
+
: `<span class="pill failed">noisy</span> ${summary.unhandledRejections.length} unhandled rejection(s) observed`;
|
|
189
|
+
|
|
190
|
+
const inventoryTable = (kind: TestKind): string => {
|
|
191
|
+
const tests = testsOfKind(kind);
|
|
192
|
+
if (tests.length === 0) return "";
|
|
193
|
+
return `
|
|
194
|
+
<h3>${tests.length} ${KIND_META[kind].badge} cases — ${escapeHtml(KIND_META[kind].blurb)}</h3>
|
|
195
|
+
<table>
|
|
196
|
+
<tr><th>#</th><th>Case</th><th>Area</th><th>Time</th></tr>
|
|
197
|
+
${tests
|
|
198
|
+
.map(
|
|
199
|
+
test =>
|
|
200
|
+
`<tr><td>${escapeHtml(test.id)}</td><td>${escapeHtml(test.name)}</td>` +
|
|
201
|
+
`<td class="muted">${escapeHtml(test.category)}</td><td>${test.durationMs} ms</td></tr>`
|
|
202
|
+
)
|
|
203
|
+
.join("")}
|
|
204
|
+
</table>`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const html = `<!doctype html>
|
|
208
|
+
<html lang="en">
|
|
209
|
+
<head>
|
|
210
|
+
<meta charset="utf-8"/>
|
|
211
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
212
|
+
<title>AutomateGraph Execution Engine — Test Report (${totals.total} tests)</title>
|
|
213
|
+
<style>
|
|
214
|
+
:root{
|
|
215
|
+
--bg:#0b1020; --panel:#121a33; --panel2:#0f1730; --line:#24304f;
|
|
216
|
+
--txt:#e7ecf7; --muted:#94a3c4; --accent:#6ea8fe; --ok:#31c48d; --bad:#f4645f;
|
|
217
|
+
--warn:#f0b429; --unit:#7c9cff; --complex:#c084fc; --async:#4fd1c5;
|
|
218
|
+
--upgrade:#f0b429; --conformance:#31c48d;
|
|
219
|
+
}
|
|
220
|
+
*{box-sizing:border-box}
|
|
221
|
+
body{margin:0;background:radial-gradient(1200px 600px at 15% -10%,#16224a 0%,var(--bg) 55%);color:var(--txt);
|
|
222
|
+
font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
223
|
+
.wrap{max-width:1180px;margin:0 auto;padding:32px 20px 80px}
|
|
224
|
+
h1{font-size:30px;margin:0 0 6px;letter-spacing:-.4px}
|
|
225
|
+
h2{font-size:19px;margin:34px 0 12px;letter-spacing:-.2px}
|
|
226
|
+
h3{font-size:14px;margin:20px 0 8px;color:var(--muted);text-transform:uppercase;letter-spacing:.07em}
|
|
227
|
+
.sub{color:var(--muted);margin:0 0 22px}
|
|
228
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:22px 0 8px}
|
|
229
|
+
.card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
|
|
230
|
+
border-radius:14px;padding:14px 16px}
|
|
231
|
+
.card .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}
|
|
232
|
+
.card .v{font-size:25px;font-weight:700;margin-top:4px}
|
|
233
|
+
.card.good .v{color:var(--ok)} .card.bad .v{color:var(--bad)}
|
|
234
|
+
.barrow{display:grid;grid-template-columns:340px 1fr 130px;gap:12px;align-items:center;padding:6px 0;border-bottom:1px dashed #1c2540}
|
|
235
|
+
.barlabel{font-size:13.5px}
|
|
236
|
+
.track{background:#0a1024;border:1px solid var(--line);border-radius:999px;height:16px;overflow:hidden}
|
|
237
|
+
.fill{height:100%;background:linear-gradient(90deg,#2f7cf6,#31c48d);color:#04122b;font-size:10.5px;
|
|
238
|
+
font-weight:700;text-align:right;padding-right:6px;line-height:16px}
|
|
239
|
+
.fill.warn{background:linear-gradient(90deg,#f0b429,#f4645f)}
|
|
240
|
+
.barmeta{color:var(--muted);font-size:12.5px;text-align:right}
|
|
241
|
+
.badge{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.06em;padding:2px 7px;border-radius:6px;vertical-align:middle}
|
|
242
|
+
.badge.unit{background:#1b2a55;color:var(--unit)}
|
|
243
|
+
.badge.integration{background:#331b55;color:var(--complex)}
|
|
244
|
+
.badge.async{background:#123a3a;color:var(--async)}
|
|
245
|
+
.badge.upgrade{background:#3a2b0d;color:var(--upgrade)}
|
|
246
|
+
.badge.conformance{background:#0f3627;color:var(--conformance)}
|
|
247
|
+
.badge.lint{background:#3a2410;color:#fb923c}
|
|
248
|
+
.badge.edges{background:#0d2a3a;color:#38bdf8}
|
|
249
|
+
.badge.registry{background:#2a1f45;color:#a78bfa}
|
|
250
|
+
.badge.graphjson{background:#0e3229;color:#34d399}
|
|
251
|
+
.badge.scripts{background:#3d1f2f;color:#f472b6}
|
|
252
|
+
.pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px}
|
|
253
|
+
.pill.passed{background:#10322a;color:var(--ok);border:1px solid #1c5b46}
|
|
254
|
+
.pill.failed{background:#3a1620;color:var(--bad);border:1px solid #7a2b32}
|
|
255
|
+
.findings{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px}
|
|
256
|
+
.fcard{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--warn);border-radius:12px;padding:12px 14px}
|
|
257
|
+
.fcard .fid{color:var(--warn);font-weight:700;font-size:12px}
|
|
258
|
+
.fcard .ftitle{font-weight:600;margin:2px 0 6px}
|
|
259
|
+
.fcard .fbody{color:#c8d3ec;font-size:13.5px}
|
|
260
|
+
.toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:14px 0}
|
|
261
|
+
.btn{background:#16204a;border:1px solid var(--line);color:var(--txt);border-radius:999px;padding:6px 13px;
|
|
262
|
+
font-size:13px;cursor:pointer}
|
|
263
|
+
.btn.active{background:#233b7a;border-color:#3b57a6}
|
|
264
|
+
input[type=search]{background:#0d142c;border:1px solid var(--line);color:var(--txt);border-radius:999px;
|
|
265
|
+
padding:7px 14px;min-width:260px;font-size:13.5px}
|
|
266
|
+
.row{border:1px solid var(--line);border-radius:10px;margin:6px 0;background:var(--panel2)}
|
|
267
|
+
.row summary{display:grid;grid-template-columns:64px 62px 210px 1fr 74px 78px;gap:10px;align-items:center;
|
|
268
|
+
padding:9px 12px;cursor:pointer;list-style:none}
|
|
269
|
+
.row summary::-webkit-details-marker{display:none}
|
|
270
|
+
.row.ok{border-left:3px solid var(--ok)} .row.bad{border-left:3px solid var(--bad)}
|
|
271
|
+
.row .id{color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums}
|
|
272
|
+
.row .cat{color:#b9c6e6;font-size:13px}
|
|
273
|
+
.row .name{font-size:13.5px}
|
|
274
|
+
.row .ms{color:var(--muted);font-size:12px;text-align:right}
|
|
275
|
+
.row .star{color:var(--warn);font-size:11px;border:1px solid #5f4a12;background:#2a2208;border-radius:6px;
|
|
276
|
+
padding:1px 6px;justify-self:start}
|
|
277
|
+
.detail{padding:10px 14px 12px;border-top:1px solid #1a2340;font-size:13.5px;color:#c8d3ec}
|
|
278
|
+
.detail .tag{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.06em;color:#8fa6d8;
|
|
279
|
+
text-transform:uppercase;margin-right:8px}
|
|
280
|
+
.detail ul{margin:6px 0 0 18px;padding:0}
|
|
281
|
+
.detail.error .errline{color:#ffb3b0;font-weight:600}
|
|
282
|
+
pre{background:#080d1c;border:1px solid #1c2540;border-radius:8px;padding:10px;overflow:auto;font-size:12.5px;color:#b9c6e6}
|
|
283
|
+
.muted{color:var(--muted)}
|
|
284
|
+
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
|
285
|
+
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #1c2540;vertical-align:top}
|
|
286
|
+
th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.06em}
|
|
287
|
+
code{background:#0d142c;border:1px solid #1c2540;border-radius:6px;padding:1px 6px;font-size:12.5px}
|
|
288
|
+
.note{background:#101a38;border:1px solid var(--line);border-radius:12px;padding:12px 16px;color:#c8d3ec;font-size:13.5px}
|
|
289
|
+
.note.warn{border-color:#5f4a12;background:#241d09}
|
|
290
|
+
footer{color:var(--muted);font-size:12.5px;margin-top:28px;border-top:1px solid #1c2540;padding-top:14px}
|
|
291
|
+
</style>
|
|
292
|
+
</head>
|
|
293
|
+
<body>
|
|
294
|
+
<div class="wrap">
|
|
295
|
+
<h1>AutomateGraph Execution Engine v1.5 — Test Report</h1>
|
|
296
|
+
<p class="sub">${totals.total} tests (${kindSummary}) executed against the single-file isomorphic core.
|
|
297
|
+
Generated ${escapeHtml(summary.generatedAt)} · Node ${escapeHtml(summary.nodeVersion)} · wall clock ${summary.durationMs} ms.</p>
|
|
298
|
+
|
|
299
|
+
<div class="cards">
|
|
300
|
+
<div class="card"><div class="k">Total</div><div class="v">${totals.total}</div></div>
|
|
301
|
+
<div class="card good"><div class="k">Passed</div><div class="v">${totals.passed}</div></div>
|
|
302
|
+
<div class="card ${totals.failed ? "bad" : "good"}"><div class="k">Failed</div><div class="v">${totals.failed}</div></div>
|
|
303
|
+
${kinds
|
|
304
|
+
.map(
|
|
305
|
+
kind =>
|
|
306
|
+
`<div class="card"><div class="k">${KIND_META[kind].badge}</div><div class="v">${totals.byKind[kind].passed}/${totals.byKind[kind].total}</div></div>`
|
|
307
|
+
)
|
|
308
|
+
.join("")}
|
|
309
|
+
<div class="card"><div class="k">Findings</div><div class="v">${findings.length}</div></div>
|
|
310
|
+
<div class="card"><div class="k">Duration</div><div class="v">${(summary.durationMs / 1000).toFixed(2)}s</div></div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<p class="note">Process hygiene on shutdown: ${processHygiene}. Each test runs in sequence with a per-test
|
|
314
|
+
timeout, so a hang or a leaked promise surfaces in this report instead of hanging the suite.</p>
|
|
315
|
+
|
|
316
|
+
<p class="note warn"><strong>v1.1 changed two observable behaviours:</strong> a cooperative callback that
|
|
317
|
+
<em>rejects because of</em> an abort now settles <code>CANCELLED</code> (was <code>FAILED</code> + unhandled error), and
|
|
318
|
+
<code>FAIL_FAST</code> is sticky at node level (was a one-off queue flush). <strong>v1.2 added the
|
|
319
|
+
<code>SUCCESS_DEAD_END</code> lint error.</strong> <strong>v1.3 adds typed edges</strong> — <code>next</code> now accepts
|
|
320
|
+
<code>{ nextNode, result, test, parallel }</code> objects (exclusive, ordered, first-match-wins, deep
|
|
321
|
+
equality) alongside the original strings (parallel, unchanged), plus explicit routing via
|
|
322
|
+
<code>return new Next(target, value)</code> or <code>context.goto(target)</code>. <strong>v1.4 adds the
|
|
323
|
+
function registry</strong> — <code>run</code> and edge <code>test</code> may name a registered function,
|
|
324
|
+
resolved lazily at evaluation time, with <code>{ fingerprint, names }</code> reported on
|
|
325
|
+
every run — plus a JSON loader (<code>src/graph-json.ts</code>) that turns a graph
|
|
326
|
+
document into a definition with total, path-carrying validation. <strong>v1.5 loads scripts</strong> —
|
|
327
|
+
a document may declare where its functions live (<code>path</code>, inline
|
|
328
|
+
<code>source</code> or <code>url</code>), the script either exports them or registers itself through
|
|
329
|
+
<code>globalThis.GraphEngine.registerEngineNamespace()</code>, and a namespace is frozen once per
|
|
330
|
+
process. Purely additive: every v1.0–v1.4 graph keeps its behaviour. See
|
|
331
|
+
<code>CHANGELOG.md</code>.</p>
|
|
332
|
+
|
|
333
|
+
<h2>Results by category</h2>
|
|
334
|
+
${summary.byCategory
|
|
335
|
+
.map(entry =>
|
|
336
|
+
bar(
|
|
337
|
+
entry.category,
|
|
338
|
+
entry.kind,
|
|
339
|
+
{ total: entry.total, passed: entry.passed, failed: entry.failed },
|
|
340
|
+
entry.durationMs
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
.join("")}
|
|
344
|
+
|
|
345
|
+
<h2>Engine semantics documented by the findings (${findings.length})</h2>
|
|
346
|
+
<div class="findings">
|
|
347
|
+
${findings
|
|
348
|
+
.map(
|
|
349
|
+
({ test, text }) => `<div class="fcard">
|
|
350
|
+
<div class="fid">${escapeHtml(test.id)} · ${escapeHtml(KIND_META[test.kind]?.badge ?? test.kind)}</div>
|
|
351
|
+
<div class="ftitle">${escapeHtml(test.name)}</div>
|
|
352
|
+
<div class="fbody">${escapeHtml(text)}</div>
|
|
353
|
+
</div>`
|
|
354
|
+
)
|
|
355
|
+
.join("")}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<h2>Test inventory</h2>
|
|
359
|
+
<h3>${UNIT_CATEGORIES.length} unit categories (${totals.byKind.unit.total} tests)</h3>
|
|
360
|
+
<table>
|
|
361
|
+
<tr><th>Category</th><th>Focus</th><th>Count</th><th>Time</th></tr>
|
|
362
|
+
${UNIT_CATEGORIES.map(([category, focus]) => {
|
|
363
|
+
const count = summary.tests.filter(
|
|
364
|
+
test => test.kind === "unit" && test.category === category
|
|
365
|
+
).length;
|
|
366
|
+
const duration = categoryDurations.get(`unit::${category}`) ?? 0;
|
|
367
|
+
return `<tr><td>${escapeHtml(category)}</td><td class="muted">${escapeHtml(focus)}</td><td>${count}</td><td>${duration} ms</td></tr>`;
|
|
368
|
+
}).join("")}
|
|
369
|
+
</table>
|
|
370
|
+
${(["integration", "async", "upgrade", "conformance", "lint", "edges", "registry", "graphjson", "scripts"] as TestKind[]).map(inventoryTable).join("")}
|
|
371
|
+
|
|
372
|
+
<h2>All test results</h2>
|
|
373
|
+
<div class="toolbar">
|
|
374
|
+
<button class="btn active" data-filter="all">All (${totals.total})</button>
|
|
375
|
+
${kinds
|
|
376
|
+
.map(
|
|
377
|
+
kind =>
|
|
378
|
+
`<button class="btn" data-filter="${kind}">${KIND_META[kind].badge} (${totals.byKind[kind].total})</button>`
|
|
379
|
+
)
|
|
380
|
+
.join("")}
|
|
381
|
+
<button class="btn" data-filter="failed">Failed (${totals.failed})</button>
|
|
382
|
+
<button class="btn" data-filter="finding">Findings (${findings.length})</button>
|
|
383
|
+
<input type="search" id="q" placeholder="Search id, category, test name…"/>
|
|
384
|
+
</div>
|
|
385
|
+
<div id="rows">
|
|
386
|
+
${kinds
|
|
387
|
+
.map(kind => testsOfKind(kind).map(renderTestRow).join(""))
|
|
388
|
+
.join("")}
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<h2>How the suite was run</h2>
|
|
392
|
+
<div class="note">
|
|
393
|
+
<p style="margin-top:0"><code>npx tsc -p tsconfig.json</code> compiles <code>src/</code> and the tests to <code>js/</code>,
|
|
394
|
+
<code>npx tsc -p tsconfig.browser.json</code> proves the core is isomorphic (DOM libs, no Node types — asserted by the
|
|
395
|
+
UPGRADE pack), then <code>node js/tests/runner.js</code> executes all ${totals.total} cases (${kinds
|
|
396
|
+
.map(kind => `${totals.byKind[kind].total} ${KIND_META[kind].badge.toLowerCase()}`)
|
|
397
|
+
.join(" + ")}) and writes
|
|
398
|
+
<code>report/report.json</code> plus this page. No third-party test framework: <code>tests/harness.ts</code> provides
|
|
399
|
+
exception-based checks, per-test timeouts and log capture.</p>
|
|
400
|
+
<p>Artifacts: <code>src/automategraph.ts</code> (engine), <code>src/storage-adapter.ts</code> (adapter contract +
|
|
401
|
+
wire format), <code>src/storage-conformance.ts</code> (adapter checker), <code>tests/unit.test.ts</code>,
|
|
402
|
+
<code>tests/complex.test.ts</code>, <code>tests/async.test.ts</code>, <code>tests/upgrade.test.ts</code>,
|
|
403
|
+
<code>tests/conformance.test.ts</code>, <code>tests/lint.test.ts</code>, <code>tests/edges.test.ts</code>, <code>tests/registry.test.ts</code>, <code>tests/graphjson.test.ts</code>, <code>tests/scripts.test.ts</code>, <code>docs/diagrams/</code>,
|
|
404
|
+
<code>CHANGELOG.md</code>, <code>UPGRADE-PROPOSAL.md</code>.</p>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<footer>
|
|
408
|
+
${failedTests.length === 0
|
|
409
|
+
? `All ${totals.total} tests passed. Findings above are deliberate documentation of observed engine behaviour, not failures.`
|
|
410
|
+
: `${failedTests.length} failing test(s): ${failedTests.map(test => escapeHtml(test.id)).join(", ")}`}
|
|
411
|
+
</footer>
|
|
412
|
+
</div>
|
|
413
|
+
<script>
|
|
414
|
+
(function () {
|
|
415
|
+
var active = "all";
|
|
416
|
+
var buttons = Array.prototype.slice.call(document.querySelectorAll(".btn[data-filter]"));
|
|
417
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll("#rows > details"));
|
|
418
|
+
var query = document.getElementById("q");
|
|
419
|
+
|
|
420
|
+
function apply() {
|
|
421
|
+
var needle = (query.value || "").trim().toLowerCase();
|
|
422
|
+
rows.forEach(function (row) {
|
|
423
|
+
var kindOk =
|
|
424
|
+
active === "all" ||
|
|
425
|
+
(active === "failed" && row.dataset.status === "failed") ||
|
|
426
|
+
(active === "finding" && row.dataset.finding === "true") ||
|
|
427
|
+
row.dataset.kind === active;
|
|
428
|
+
var textOk = needle === "" || row.dataset.search.indexOf(needle) !== -1;
|
|
429
|
+
row.style.display = kindOk && textOk ? "" : "none";
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
buttons.forEach(function (button) {
|
|
434
|
+
button.addEventListener("click", function () {
|
|
435
|
+
active = button.dataset.filter;
|
|
436
|
+
buttons.forEach(function (other) { other.classList.toggle("active", other === button); });
|
|
437
|
+
apply();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
query.addEventListener("input", apply);
|
|
442
|
+
apply();
|
|
443
|
+
})();
|
|
444
|
+
</script>
|
|
445
|
+
</body>
|
|
446
|
+
</html>`;
|
|
447
|
+
|
|
448
|
+
const htmlPath = path.join(reportDir, "index.html");
|
|
449
|
+
fs.writeFileSync(htmlPath, html, "utf8");
|
|
450
|
+
|
|
451
|
+
return { htmlPath, jsonPath };
|
|
452
|
+
}
|
package/tests/runner.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* Test entry point — runs every registered case, prints a
|
|
3
|
+
* summary and writes the web report (report/index.html + JSON).
|
|
4
|
+
* ============================================================ */
|
|
5
|
+
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
|
|
8
|
+
import { TEST_KINDS, runAll } from "./harness";
|
|
9
|
+
import { findingsOf, writeReport } from "./report";
|
|
10
|
+
|
|
11
|
+
import "./unit.test";
|
|
12
|
+
import "./complex.test";
|
|
13
|
+
import "./async.test";
|
|
14
|
+
import "./upgrade.test";
|
|
15
|
+
import "./conformance.test";
|
|
16
|
+
import "./lint.test";
|
|
17
|
+
import "./edges.test";
|
|
18
|
+
import "./registry.test";
|
|
19
|
+
import "./graphjson.test";
|
|
20
|
+
import "./scripts.test";
|
|
21
|
+
|
|
22
|
+
async function main(): Promise<void> {
|
|
23
|
+
const summary = await runAll();
|
|
24
|
+
|
|
25
|
+
const workspaceRoot = path.resolve(__dirname, "..", "..");
|
|
26
|
+
const { htmlPath, jsonPath } = writeReport(summary, workspaceRoot);
|
|
27
|
+
|
|
28
|
+
process.stdout.write("\n────────────────────────────────────────────\n");
|
|
29
|
+
|
|
30
|
+
for (const kind of TEST_KINDS) {
|
|
31
|
+
const totals = summary.totals.byKind[kind];
|
|
32
|
+
if (totals.total === 0) continue;
|
|
33
|
+
const label = kind.toUpperCase().padEnd(12, " ");
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
`${label}${totals.passed}/${totals.total} passed\n`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const findingCount = summary.tests.reduce(
|
|
40
|
+
(total, test) => total + findingsOf(test).length,
|
|
41
|
+
0
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
`TOTAL ${summary.totals.passed}/${summary.totals.total} passed in ${summary.durationMs}ms\n`
|
|
46
|
+
);
|
|
47
|
+
process.stdout.write(`FINDINGS ${findingCount}\n`);
|
|
48
|
+
process.stdout.write(`REPORT ${htmlPath}\n`);
|
|
49
|
+
process.stdout.write(`JSON ${jsonPath}\n`);
|
|
50
|
+
if (summary.unhandledRejections.length > 0) {
|
|
51
|
+
process.stdout.write(`WARN ${summary.unhandledRejections.length} unhandled rejection(s)\n`);
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write("────────────────────────────────────────────\n");
|
|
54
|
+
|
|
55
|
+
process.exit(summary.totals.failed > 0 ? 1 : 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void main();
|