@mantaq/inspect 0.4.0 → 0.5.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 +19 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -2
- package/dist/{load-D0_Jl6Wr.mjs → inspect-B7ZQCNM2.mjs} +153 -138
- package/dist/{inspect-DvQtAcwv.d.mts → inspect-vSdt2qtL.d.mts} +3 -3
- package/dist/load-CWubcSZU.mjs +131 -0
- package/dist/record.d.mts +14 -0
- package/dist/record.mjs +154 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +145 -71
- package/package.json +15 -4
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ export default defineConfig({
|
|
|
22
22
|
|
|
23
23
|
The live page runs the real actor, so it needs an exported factory on a `VirtualClock`; instance or `RealClock` machines fall back to the static walk.
|
|
24
24
|
|
|
25
|
+
`vite build` with the plugin also writes the page as a static site: `index.html`, `viz.json` and a script under `base` (set `base: "/"` to put them at the output root). Machines load through your Vite config, and the fuzz runs at build time.
|
|
26
|
+
|
|
25
27
|
Options: `base` (default `/__mantaq/`), `fuzz` (`false`, or `{ seed, iterations }`; default 500 iterations so a save redraws fast).
|
|
26
28
|
|
|
27
29
|
**Library**: one call per machine.
|
|
@@ -34,12 +36,14 @@ const { machine, warnings } = inspect({
|
|
|
34
36
|
id: "cart",
|
|
35
37
|
create: () => createCart(), // a fresh actor every call
|
|
36
38
|
source: { file: "/abs/src/cart.ts", root: "/abs" }, // optional: explanations + payloads from types
|
|
37
|
-
tests: [
|
|
39
|
+
tests: [
|
|
40
|
+
{ name: "add an item", steps: [{ kind: "event", event: "addItem", payload: { sku: "A1" } }] },
|
|
41
|
+
], // optional: named runs for "example tests" coverage
|
|
38
42
|
});
|
|
39
43
|
const json = serializeViz(vizDocument([machine]));
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
`inspectFile(file)` / `inspectModule(file, { load })` do the discovery the Vite plugin does. The building blocks (`toVizMachine`, `annotateMachine`, `fuzz`, `fuzzedMachine`, `withTestCoverage`, `guidance`) stay exported for custom pipelines.
|
|
46
|
+
`inspectFile(file)` / `inspectModule(file, { load })` do the discovery the Vite plugin does, and read recorded runs from `__runs__/*.json` next to the machine file (`{ [machineId]: { name, end, steps }[] }`). A run is replayed on a fresh actor; internal events the replay already produces (timers, synchronous emits) are dropped, and a run that replays to a different state than `end` is dropped with a warning. The building blocks (`toVizMachine`, `annotateMachine`, `fuzz`, `fuzzedMachine`, `withTestCoverage`, `guidance`) stay exported for custom pipelines.
|
|
43
47
|
|
|
44
48
|
## What it needs from your file
|
|
45
49
|
|
|
@@ -56,7 +60,7 @@ Every part that cannot run is skipped with a warning, never guessed.
|
|
|
56
60
|
The viewer's "Coverage from" picker dims elements with zero hits. What dimmed means depends on the source:
|
|
57
61
|
|
|
58
62
|
- **examples + fuzz** (`reached`): neither an example nor fuzzing reached this. The default when both sources are present, and the same merge `expectReached` uses. Fix a miss by adding an example to `tests`.
|
|
59
|
-
- **example tests** (`tested`): no test exercises this. Collected by replaying the
|
|
63
|
+
- **example tests** (`tested`): no test exercises this. Collected by replaying the named runs you pass as `tests`, each on a fresh actor.
|
|
60
64
|
- **fuzz runs** (`coverage`): no fuzz run produced this. Fuzzing reaches almost everything, so little is dimmed. What does stay dimmed is usually a **declared but unreached edge**: the handler lists the target in `to`, but its guards may never allow it. Saga's `compensatingRefund -REFUND_DONE-> failed` is an example. The edge panel calls these out. Fuzzing is not proof, so check the handler.
|
|
61
65
|
|
|
62
66
|
## How each tool works
|
|
@@ -71,6 +75,18 @@ The viewer's "Coverage from" picker dims elements with zero hits. What dimmed me
|
|
|
71
75
|
|
|
72
76
|
**Guidance** (`guidance`, Phase 5): literal seeding plus a branch-distance guide built from AST comparisons on payload and context. It helps when handlers compare payload fields against literals far from the samples, or count in context toward a threshold. See `docs/atlas/phase5-comparison.md`.
|
|
73
77
|
|
|
78
|
+
## Record runs from your tests
|
|
79
|
+
|
|
80
|
+
In a vitest file, wrap the factory once and your tests become runs:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { recordRuns } from "@mantaq/inspect/record";
|
|
84
|
+
|
|
85
|
+
const make = recordRuns(createInviteLinkActor);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every passing test that builds an actor with `make` writes its events and clock moves to `__runs__/<test file>.json` next to the test, keyed by machine id. `inspectModule` and the Vite plugin read those files, so each test shows up in the live page's Runs list and counts as an example run. Run the whole test file to refresh it. With `CI` set, a run that differs from the committed file fails the test, like a stale snapshot. The factory may be called in the test or in `beforeEach`; a retried test is recorded once. Runs with a payload that does not survive JSON (functions, `bigint`, `Map`, class instances) are skipped with a warning. `vitest` is an optional peer dependency.
|
|
89
|
+
|
|
74
90
|
## What it cannot show
|
|
75
91
|
|
|
76
92
|
- **Guards** are not evaluated. A declared edge says "the handler may return this", not "some input reaches it". Fuzz coverage and the AST's branch conditions are the evidence; neither is proof.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as eventCatalog, i as Trace, n as Observation, o as execute, r as RunError, t as FuzzTarget } from "./run-BLt0mwXe.mjs";
|
|
2
|
-
import { a as ModuleOptions, c as machineId, i as InspectedModule, l as vizDocument, n as InspectTarget, o as inspect, r as Inspected, s as inspectModule, t as FuzzSettings, u as AnalysisSource } from "./inspect-
|
|
2
|
+
import { a as ModuleOptions, c as machineId, i as InspectedModule, l as vizDocument, n as InspectTarget, o as inspect, r as Inspected, s as inspectModule, t as FuzzSettings, u as AnalysisSource } from "./inspect-vSdt2qtL.mjs";
|
|
3
3
|
import { VizComparison, VizDocument, VizExplanation, VizFuzzError, VizJson, VizMachine, VizStep } from "@mantaq/viz";
|
|
4
4
|
import { History } from "@mantaq/traversal";
|
|
5
5
|
import { Either } from "@mantaq/utils";
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as toViz, a as vizDocument, c as annotateMachine, d as guidance, f as literalPool, g as shrink, h as fuzz, l as branchDistance, m as withTestCoverage, n as inspectModule, o as analyzeModule, p as fuzzedMachine, r as machineId, s as analyzeSource, t as inspect, u as branchDistanceGuide, v as toVizMachine } from "./inspect-B7ZQCNM2.mjs";
|
|
2
2
|
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
3
|
+
import { r as inspectFile } from "./load-CWubcSZU.mjs";
|
|
3
4
|
import { coveredByAny, unhandledSends } from "@mantaq/viz";
|
|
4
5
|
// src/gate.ts
|
|
5
6
|
/**
|
|
@@ -79,7 +80,7 @@ function missedReach(machine, expected) {
|
|
|
79
80
|
}
|
|
80
81
|
function failOnMiss(machine, misses) {
|
|
81
82
|
if (misses.length === 0) return;
|
|
82
|
-
throw new Error(`${machine.id} missed ${misses.length} expected ${misses.length === 1 ? "reach" : "reaches"}:\n${misses.map((each) => ` - ${each}`).join("\n")}\nAdd
|
|
83
|
+
throw new Error(`${machine.id} missed ${misses.length} expected ${misses.length === 1 ? "reach" : "reaches"}:\n${misses.map((each) => ` - ${each}`).join("\n")}\nAdd a named run to \`tests\`, or raise \`fuzz.iterations\`, to cover it.`);
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
86
|
* Call from a test: throws unless fuzzing or the target's `tests` visited every
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
1
|
+
import { i as execute, r as eventCatalog, t as applyStep } from "./run-B5huYj_r.mjs";
|
|
2
2
|
import { ANY_STATE, VIZ_SCHEMA_VERSION, coveredByAny, isAnyState, routingWarnings, unhandledSends } from "@mantaq/viz";
|
|
3
3
|
import { INITIAL_NODE_ID, buildGraph, parseInternalEventIds } from "@mantaq/traversal";
|
|
4
4
|
import { Either } from "@mantaq/utils";
|
|
5
5
|
import ts from "typescript";
|
|
6
|
-
import { dirname, join, relative,
|
|
6
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
7
7
|
import { DEFAULT_SEED } from "@mantaq/pbt";
|
|
8
8
|
import { VirtualClock } from "@mantaq/core";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
12
10
|
// src/structure.ts
|
|
13
11
|
function byId$1(left, right) {
|
|
14
12
|
if (left.id < right.id) return -1;
|
|
@@ -126,7 +124,8 @@ function toVizMachine(input) {
|
|
|
126
124
|
states,
|
|
127
125
|
events,
|
|
128
126
|
edges,
|
|
129
|
-
fuzz: null
|
|
127
|
+
fuzz: null,
|
|
128
|
+
runs: []
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
131
|
function toViz(inputs) {
|
|
@@ -1557,6 +1556,107 @@ function annotateWith(machine, context) {
|
|
|
1557
1556
|
edges
|
|
1558
1557
|
};
|
|
1559
1558
|
}
|
|
1559
|
+
// src/runs.ts
|
|
1560
|
+
/** Name of the folder, next to the test files, that holds recorded runs. */
|
|
1561
|
+
const RUNS_DIR = "__runs__";
|
|
1562
|
+
function dropOne(list, item) {
|
|
1563
|
+
const at = list.indexOf(item);
|
|
1564
|
+
if (at < 0) return false;
|
|
1565
|
+
list.splice(at, 1);
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Replay a raw recording on a fresh actor and keep only the steps that cannot
|
|
1570
|
+
* be reproduced: an internal event the replay already produced (a timer after
|
|
1571
|
+
* its advance, a synchronous emit after its send) is dropped, one it did not
|
|
1572
|
+
* (a promise result) is kept. Left when the replay ends elsewhere than the
|
|
1573
|
+
* test did.
|
|
1574
|
+
*/
|
|
1575
|
+
function normalizeRun(target, run) {
|
|
1576
|
+
const { actor, clock } = target.create();
|
|
1577
|
+
const internal = new Set(eventCatalog(actor).internal);
|
|
1578
|
+
let produced = [];
|
|
1579
|
+
actor.on("transition", { fn: ({ event }) => void produced.push(event.type) });
|
|
1580
|
+
const known = /* @__PURE__ */ new Set([...eventCatalog(actor).inputs, ...internal]);
|
|
1581
|
+
const unknown = run.steps.find((step) => step.kind === "event" && !known.has(step.event));
|
|
1582
|
+
if (unknown?.kind === "event") {
|
|
1583
|
+
actor.dispose();
|
|
1584
|
+
return Either.left(`unknown event "${unknown.event}"`);
|
|
1585
|
+
}
|
|
1586
|
+
const kept = [];
|
|
1587
|
+
for (const step of run.steps) {
|
|
1588
|
+
if (step.kind === "event" && internal.has(step.event) && dropOne(produced, step.event)) continue;
|
|
1589
|
+
produced = [];
|
|
1590
|
+
applyStep(actor, {
|
|
1591
|
+
step,
|
|
1592
|
+
clock,
|
|
1593
|
+
internal
|
|
1594
|
+
});
|
|
1595
|
+
if (step.kind === "event" && internal.has(step.event)) dropOne(produced, step.event);
|
|
1596
|
+
kept.push(step);
|
|
1597
|
+
}
|
|
1598
|
+
const ended = actor.state.name;
|
|
1599
|
+
actor.dispose();
|
|
1600
|
+
return ended === run.end ? Either.right(kept) : Either.left(`replays to ${ended}, the test ended in ${run.end}`);
|
|
1601
|
+
}
|
|
1602
|
+
function isRecord(value) {
|
|
1603
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1604
|
+
}
|
|
1605
|
+
function isJson(value) {
|
|
1606
|
+
return Either.isRight(Either.from(() => JSON.stringify(value)));
|
|
1607
|
+
}
|
|
1608
|
+
function isStep(value) {
|
|
1609
|
+
if (!isRecord(value)) return false;
|
|
1610
|
+
if (value["kind"] === "advance") return typeof value["ms"] === "number" && value["ms"] >= 0;
|
|
1611
|
+
return value["kind"] === "event" && typeof value["event"] === "string" && isJson(value["payload"]);
|
|
1612
|
+
}
|
|
1613
|
+
function isRecorded(value) {
|
|
1614
|
+
return isRecord(value) && typeof value["name"] === "string" && typeof value["end"] === "string" && Array.isArray(value["steps"]) && value["steps"].every(isStep);
|
|
1615
|
+
}
|
|
1616
|
+
function readFile(file) {
|
|
1617
|
+
const name = basename(file);
|
|
1618
|
+
const text = Either.from(() => JSON.parse(readFileSync(file, "utf8")));
|
|
1619
|
+
if (Either.isLeft(text) || !isRecord(text[1])) return {
|
|
1620
|
+
runs: {},
|
|
1621
|
+
warnings: [`${RUNS_DIR}/${name}: not a JSON object, ignored`]
|
|
1622
|
+
};
|
|
1623
|
+
const entries = Object.entries(text[1]).map(([id, runs]) => {
|
|
1624
|
+
const list = Array.isArray(runs) ? runs : [];
|
|
1625
|
+
const valid = list.filter(isRecorded);
|
|
1626
|
+
return {
|
|
1627
|
+
id,
|
|
1628
|
+
valid,
|
|
1629
|
+
bad: Array.isArray(runs) ? list.length - valid.length : 1
|
|
1630
|
+
};
|
|
1631
|
+
});
|
|
1632
|
+
return {
|
|
1633
|
+
runs: Object.fromEntries(entries.map(({ id, valid }) => [id, valid])),
|
|
1634
|
+
warnings: entries.filter(({ bad }) => bad > 0).map(({ id, bad }) => `${RUNS_DIR}/${name}: ${bad} malformed run(s) for "${id}" ignored`)
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Every run recorded for the test files in `dir`: the `__runs__/*.json` files
|
|
1639
|
+
* next to them, merged by machine id in file-name order, and a warning for
|
|
1640
|
+
* each file or run that could not be read.
|
|
1641
|
+
*/
|
|
1642
|
+
function readRuns(dir) {
|
|
1643
|
+
const listing = Either.from(() => readdirSync(join(dir, RUNS_DIR)).sort());
|
|
1644
|
+
if (Either.isLeft(listing)) return {
|
|
1645
|
+
runs: {},
|
|
1646
|
+
warnings: []
|
|
1647
|
+
};
|
|
1648
|
+
const merged = {};
|
|
1649
|
+
const warnings = [];
|
|
1650
|
+
for (const name of listing[1].filter((each) => each.endsWith(".json"))) {
|
|
1651
|
+
const read = readFile(join(dir, RUNS_DIR, name));
|
|
1652
|
+
warnings.push(...read.warnings);
|
|
1653
|
+
for (const [id, runs] of Object.entries(read.runs)) merged[id] = [...merged[id] ?? [], ...runs];
|
|
1654
|
+
}
|
|
1655
|
+
return {
|
|
1656
|
+
runs: merged,
|
|
1657
|
+
warnings
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1560
1660
|
// src/inspect.ts
|
|
1561
1661
|
const DEFAULT_ITERATIONS = 2e3;
|
|
1562
1662
|
function isVirtualClock(clock) {
|
|
@@ -1601,11 +1701,11 @@ function driven(target, context) {
|
|
|
1601
1701
|
const fuzzed = settings === false ? structure : fuzzedMachine(run[1], fuzz(run[1], {
|
|
1602
1702
|
seed: settings?.seed ?? DEFAULT_SEED,
|
|
1603
1703
|
iterations: settings?.iterations ?? DEFAULT_ITERATIONS,
|
|
1604
|
-
...target.tests === void 0 ? {} : { initial: target.tests },
|
|
1704
|
+
...target.tests === void 0 ? {} : { initial: target.tests.map((run) => run.steps) },
|
|
1605
1705
|
...guidance(structure)
|
|
1606
1706
|
}));
|
|
1607
1707
|
return {
|
|
1608
|
-
machine: target.tests === void 0 ? fuzzed : withTestCoverage(run[1], fuzzed, target.tests),
|
|
1708
|
+
machine: target.tests === void 0 ? fuzzed : withTestCoverage(run[1], fuzzed, target.tests.map((each) => each.steps)),
|
|
1609
1709
|
skipped: []
|
|
1610
1710
|
};
|
|
1611
1711
|
}
|
|
@@ -1624,7 +1724,10 @@ function inspectWith(target, analysis) {
|
|
|
1624
1724
|
});
|
|
1625
1725
|
const quiet = target.fuzz === false && target.tests === void 0;
|
|
1626
1726
|
const unanalyzed = analysis === null ? [] : [Either.getLeft(analysis)].filter((each) => each !== void 0);
|
|
1627
|
-
const annotated = annotate(
|
|
1727
|
+
const annotated = annotate({
|
|
1728
|
+
...machine,
|
|
1729
|
+
runs: [...target.tests ?? []]
|
|
1730
|
+
});
|
|
1628
1731
|
return {
|
|
1629
1732
|
machine: annotated,
|
|
1630
1733
|
warnings: [
|
|
@@ -1678,6 +1781,33 @@ function actorFactory(found, value) {
|
|
|
1678
1781
|
return isActor(actor) ? actor : sample;
|
|
1679
1782
|
};
|
|
1680
1783
|
}
|
|
1784
|
+
/** The recorded runs of one machine that replay faithfully, and a warning for each that does not. */
|
|
1785
|
+
function replayedRuns(id, create, recorded) {
|
|
1786
|
+
const found = recorded[id] ?? [];
|
|
1787
|
+
if (found.length === 0) return {
|
|
1788
|
+
runs: [],
|
|
1789
|
+
warnings: []
|
|
1790
|
+
};
|
|
1791
|
+
const run = runner({
|
|
1792
|
+
id,
|
|
1793
|
+
create
|
|
1794
|
+
}, {});
|
|
1795
|
+
if (Either.isLeft(run)) return {
|
|
1796
|
+
runs: [],
|
|
1797
|
+
warnings: [`runs not shown: ${run[0]}`]
|
|
1798
|
+
};
|
|
1799
|
+
const results = found.map((each) => ({
|
|
1800
|
+
each,
|
|
1801
|
+
result: normalizeRun(run[1], each)
|
|
1802
|
+
}));
|
|
1803
|
+
return {
|
|
1804
|
+
runs: results.flatMap(({ each, result }) => Either.isRight(result) ? [{
|
|
1805
|
+
name: each.name,
|
|
1806
|
+
steps: result[1]
|
|
1807
|
+
}] : []),
|
|
1808
|
+
warnings: results.flatMap(({ each, result }) => Either.isLeft(result) ? [`run "${each.name}" dropped: ${result[0]}`] : [])
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1681
1811
|
/**
|
|
1682
1812
|
* Every machine a file exports, found from its types: actors, and functions
|
|
1683
1813
|
* that build one (optionally taking a clock or `{ clock }`). When a file has
|
|
@@ -1703,6 +1833,7 @@ async function inspectModule(file, options) {
|
|
|
1703
1833
|
warnings: [`${file}: no exported actor or actor factory found`]
|
|
1704
1834
|
};
|
|
1705
1835
|
const exports = await options.load(file);
|
|
1836
|
+
const { runs: recorded, warnings: runWarnings } = readRuns(dirname(file));
|
|
1706
1837
|
const results = chosen.map((each) => {
|
|
1707
1838
|
const id = machineId(each.name);
|
|
1708
1839
|
const create = actorFactory(each, exports[each.name]);
|
|
@@ -1711,21 +1842,30 @@ async function inspectModule(file, options) {
|
|
|
1711
1842
|
warnings: [`${id}: ${each.name} did not return an actor`]
|
|
1712
1843
|
};
|
|
1713
1844
|
const fuzzSettings = each.call === "instance" ? false : options.fuzz ?? {};
|
|
1845
|
+
const replayed = each.call === "instance" ? {
|
|
1846
|
+
runs: [],
|
|
1847
|
+
warnings: (recorded[id] ?? []).length === 0 ? [] : ["runs not shown: exported as one actor, so they cannot be replayed safely"]
|
|
1848
|
+
} : replayedRuns(id, create, recorded);
|
|
1714
1849
|
const inspected = inspectWith({
|
|
1715
1850
|
id,
|
|
1716
1851
|
create,
|
|
1717
1852
|
source,
|
|
1718
|
-
fuzz: fuzzSettings
|
|
1853
|
+
fuzz: fuzzSettings,
|
|
1854
|
+
...replayed.runs.length === 0 ? {} : { tests: replayed.runs }
|
|
1719
1855
|
}, analysis);
|
|
1720
1856
|
const instance = each.call === "instance" ? ["not fuzzed: exported as one actor, not a factory"] : [];
|
|
1721
1857
|
return {
|
|
1722
1858
|
machine: inspected.machine,
|
|
1723
|
-
warnings: [
|
|
1859
|
+
warnings: [
|
|
1860
|
+
...instance,
|
|
1861
|
+
...replayed.warnings,
|
|
1862
|
+
...inspected.warnings
|
|
1863
|
+
].map((warning) => `${id}: ${warning}`)
|
|
1724
1864
|
};
|
|
1725
1865
|
});
|
|
1726
1866
|
return {
|
|
1727
1867
|
machines: results.flatMap((result) => result.machine === null ? [] : [result.machine]),
|
|
1728
|
-
warnings: results.flatMap((result) => result.warnings)
|
|
1868
|
+
warnings: [...runWarnings.map((warning) => `${file}: ${warning}`), ...results.flatMap((result) => result.warnings)]
|
|
1729
1869
|
};
|
|
1730
1870
|
}
|
|
1731
1871
|
/** Machines as a document, ready for `serializeViz`. */
|
|
@@ -1735,129 +1875,4 @@ function vizDocument(machines) {
|
|
|
1735
1875
|
machines: [...machines]
|
|
1736
1876
|
};
|
|
1737
1877
|
}
|
|
1738
|
-
|
|
1739
|
-
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
1740
|
-
const ENTRY = "virtual:mantaq-inspect";
|
|
1741
|
-
const RESOLVED = `\0${ENTRY}`;
|
|
1742
|
-
/**
|
|
1743
|
-
* A module that imports the machine file and the inspector side by side. Run
|
|
1744
|
-
* through one module graph, both see the same `@mantaq/core` instance: core
|
|
1745
|
-
* recognizes its state refs by a module-level registry, so a machine loaded
|
|
1746
|
-
* through a bundler and inspected from plain Node would look stateless.
|
|
1747
|
-
*/
|
|
1748
|
-
function entrySource(file) {
|
|
1749
|
-
const inspect = fileURLToPath(import.meta.resolve("@mantaq/inspect"));
|
|
1750
|
-
return [
|
|
1751
|
-
`import * as machines from ${JSON.stringify(file)};`,
|
|
1752
|
-
`import { inspectModule } from ${JSON.stringify(inspect)};`,
|
|
1753
|
-
`export function run(options) {`,
|
|
1754
|
-
` return inspectModule(${JSON.stringify(file)}, { ...options, load: async () => machines });`,
|
|
1755
|
-
`}`
|
|
1756
|
-
].join("\n");
|
|
1757
|
-
}
|
|
1758
|
-
/** Vite plugin serving the entry for each `virtual:mantaq-inspect?file=<path>` id. */
|
|
1759
|
-
function inspectEntry() {
|
|
1760
|
-
return {
|
|
1761
|
-
name: "mantaq-inspect-entry",
|
|
1762
|
-
resolveId: (id) => id.startsWith(ENTRY) ? `\0${id}` : null,
|
|
1763
|
-
load: (id) => {
|
|
1764
|
-
if (!id.startsWith(RESOLVED)) return null;
|
|
1765
|
-
const file = new URLSearchParams(id.slice(id.indexOf("?") + 1)).get("file");
|
|
1766
|
-
return file === null ? null : entrySource(file);
|
|
1767
|
-
}
|
|
1768
|
-
};
|
|
1769
|
-
}
|
|
1770
|
-
function entryId(file) {
|
|
1771
|
-
return `${ENTRY}?${new URLSearchParams({ file }).toString()}`;
|
|
1772
|
-
}
|
|
1773
|
-
/** The `run` export of an entry module, checked: module objects are untyped. */
|
|
1774
|
-
function runOf(module) {
|
|
1775
|
-
const run = typeof module === "object" && module !== null ? Reflect.get(module, "run") : void 0;
|
|
1776
|
-
if (typeof run !== "function") return null;
|
|
1777
|
-
return async (options) => Reflect.apply(run, void 0, [options]);
|
|
1778
|
-
}
|
|
1779
|
-
const ENV_ACCESS = [
|
|
1780
|
-
"import",
|
|
1781
|
-
"meta",
|
|
1782
|
-
"env"
|
|
1783
|
-
].join(".");
|
|
1784
|
-
/**
|
|
1785
|
-
* Replaces the env object with the project's env: `runnerImport` never reads
|
|
1786
|
-
* env files, so `VITE_*` variables would otherwise be missing. A textual
|
|
1787
|
-
* replace: it also rewrites matches in strings and comments, and a frozen
|
|
1788
|
-
* snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
|
|
1789
|
-
*/
|
|
1790
|
-
function projectEnv(env, mode) {
|
|
1791
|
-
const value = JSON.stringify({
|
|
1792
|
-
...env,
|
|
1793
|
-
BASE_URL: "/",
|
|
1794
|
-
MODE: mode,
|
|
1795
|
-
DEV: true,
|
|
1796
|
-
PROD: false,
|
|
1797
|
-
SSR: true
|
|
1798
|
-
});
|
|
1799
|
-
return {
|
|
1800
|
-
name: "mantaq-inspect-env",
|
|
1801
|
-
enforce: "pre",
|
|
1802
|
-
transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
|
|
1803
|
-
code: code.replaceAll(ENV_ACCESS, `(${value})`),
|
|
1804
|
-
map: null
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
/**
|
|
1809
|
-
* The inline config for a load: empty without `--config`; otherwise the
|
|
1810
|
-
* project's Vite config (plugins, aliases, define) plus its env files.
|
|
1811
|
-
*/
|
|
1812
|
-
async function inlineConfig({ config, mode }) {
|
|
1813
|
-
const base = {
|
|
1814
|
-
configFile: false,
|
|
1815
|
-
logLevel: "silent"
|
|
1816
|
-
};
|
|
1817
|
-
if (config === void 0) return {
|
|
1818
|
-
...base,
|
|
1819
|
-
...mode === void 0 ? {} : { mode }
|
|
1820
|
-
};
|
|
1821
|
-
const chosen = mode ?? "development";
|
|
1822
|
-
const user = (await loadConfigFromFile({
|
|
1823
|
-
command: "serve",
|
|
1824
|
-
mode: chosen
|
|
1825
|
-
}, config))?.config ?? {};
|
|
1826
|
-
const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
|
|
1827
|
-
const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
|
|
1828
|
-
return {
|
|
1829
|
-
...user,
|
|
1830
|
-
...base,
|
|
1831
|
-
mode: chosen,
|
|
1832
|
-
plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
/** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
|
|
1836
|
-
async function inspectFile(file, options = {}, load = {}) {
|
|
1837
|
-
if (load.config !== void 0 && !existsSync(load.config)) return {
|
|
1838
|
-
machines: [],
|
|
1839
|
-
warnings: [`${file}: Vite config not found: ${load.config}`],
|
|
1840
|
-
failed: true
|
|
1841
|
-
};
|
|
1842
|
-
const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
|
|
1843
|
-
...inline,
|
|
1844
|
-
plugins: [...inline.plugins ?? [], inspectEntry()]
|
|
1845
|
-
})).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
|
|
1846
|
-
if (Either.isLeft(loaded)) return {
|
|
1847
|
-
machines: [],
|
|
1848
|
-
warnings: [`${file}: ${loaded[0]}`],
|
|
1849
|
-
failed: true
|
|
1850
|
-
};
|
|
1851
|
-
const { module } = loaded[1];
|
|
1852
|
-
const run = runOf(module);
|
|
1853
|
-
if (run === null) return {
|
|
1854
|
-
machines: [],
|
|
1855
|
-
warnings: [`${file}: could not load the inspector`],
|
|
1856
|
-
failed: true
|
|
1857
|
-
};
|
|
1858
|
-
return {
|
|
1859
|
-
...await run(options),
|
|
1860
|
-
failed: false
|
|
1861
|
-
};
|
|
1862
|
-
}
|
|
1863
|
-
export { toVizMachine as S, fuzzedMachine as _, inspect as a, shrink as b, projectRoot as c, analyzeSource as d, annotateMachine as f, literalPool as g, guidance as h, runOf as i, vizDocument as l, branchDistanceGuide as m, inspectEntry as n, inspectModule as o, branchDistance as p, inspectFile as r, machineId as s, entryId as t, analyzeModule as u, withTestCoverage as v, toViz as x, fuzz as y };
|
|
1878
|
+
export { toViz as _, vizDocument as a, annotateMachine as c, guidance as d, literalPool as f, shrink as g, fuzz as h, projectRoot as i, branchDistance as l, withTestCoverage as m, inspectModule as n, analyzeModule as o, fuzzedMachine as p, machineId as r, analyzeSource as s, inspect as t, branchDistanceGuide as u, toVizMachine as v };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VizDocument, VizJson, VizMachine,
|
|
1
|
+
import { VizDocument, VizJson, VizMachine, VizRun } from "@mantaq/viz";
|
|
2
2
|
import { Either } from "@mantaq/utils";
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { AnyActor } from "@mantaq/core";
|
|
@@ -23,8 +23,8 @@ interface InspectTarget {
|
|
|
23
23
|
source?: AnalysisSource;
|
|
24
24
|
/** Sample payloads per event; replace the ones read from types. */
|
|
25
25
|
payloads?: Readonly<Record<string, ReadonlyArray<VizJson>>>;
|
|
26
|
-
/**
|
|
27
|
-
tests?: ReadonlyArray<
|
|
26
|
+
/** Named runs your tests take: "Coverage from: example tests", fuzz seeds, and the live page's Runs list. */
|
|
27
|
+
tests?: ReadonlyArray<VizRun>;
|
|
28
28
|
/** `false` = structure only. */
|
|
29
29
|
fuzz?: false | FuzzSettings;
|
|
30
30
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Either } from "@mantaq/utils";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
|
|
6
|
+
// src/load.ts
|
|
7
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
8
|
+
const ENTRY = "virtual:mantaq-inspect";
|
|
9
|
+
const RESOLVED = `\0${ENTRY}`;
|
|
10
|
+
/**
|
|
11
|
+
* A module that imports the machine file and the inspector side by side. Run
|
|
12
|
+
* through one module graph, both see the same `@mantaq/core` instance: core
|
|
13
|
+
* recognizes its state refs by a module-level registry, so a machine loaded
|
|
14
|
+
* through a bundler and inspected from plain Node would look stateless.
|
|
15
|
+
*/
|
|
16
|
+
function entrySource(file) {
|
|
17
|
+
const inspect = fileURLToPath(import.meta.resolve("@mantaq/inspect"));
|
|
18
|
+
return [
|
|
19
|
+
`import * as machines from ${JSON.stringify(file)};`,
|
|
20
|
+
`import { inspectModule } from ${JSON.stringify(inspect)};`,
|
|
21
|
+
`export function run(options) {`,
|
|
22
|
+
` return inspectModule(${JSON.stringify(file)}, { ...options, load: async () => machines });`,
|
|
23
|
+
`}`
|
|
24
|
+
].join("\n");
|
|
25
|
+
}
|
|
26
|
+
/** Vite plugin serving the entry for each `virtual:mantaq-inspect?file=<path>` id. */
|
|
27
|
+
function inspectEntry() {
|
|
28
|
+
return {
|
|
29
|
+
name: "mantaq-inspect-entry",
|
|
30
|
+
resolveId: (id) => id.startsWith(ENTRY) ? `\0${id}` : null,
|
|
31
|
+
load: (id) => {
|
|
32
|
+
if (!id.startsWith(RESOLVED)) return null;
|
|
33
|
+
const file = new URLSearchParams(id.slice(id.indexOf("?") + 1)).get("file");
|
|
34
|
+
return file === null ? null : entrySource(file);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function entryId(file) {
|
|
39
|
+
return `${ENTRY}?${new URLSearchParams({ file }).toString()}`;
|
|
40
|
+
}
|
|
41
|
+
/** The `run` export of an entry module, checked: module objects are untyped. */
|
|
42
|
+
function runOf(module) {
|
|
43
|
+
const run = typeof module === "object" && module !== null ? Reflect.get(module, "run") : void 0;
|
|
44
|
+
if (typeof run !== "function") return null;
|
|
45
|
+
return async (options) => Reflect.apply(run, void 0, [options]);
|
|
46
|
+
}
|
|
47
|
+
const ENV_ACCESS = [
|
|
48
|
+
"import",
|
|
49
|
+
"meta",
|
|
50
|
+
"env"
|
|
51
|
+
].join(".");
|
|
52
|
+
/**
|
|
53
|
+
* Replaces the env object with the project's env: `runnerImport` never reads
|
|
54
|
+
* env files, so `VITE_*` variables would otherwise be missing. A textual
|
|
55
|
+
* replace: it also rewrites matches in strings and comments, and a frozen
|
|
56
|
+
* snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
|
|
57
|
+
*/
|
|
58
|
+
function projectEnv(env, mode) {
|
|
59
|
+
const value = JSON.stringify({
|
|
60
|
+
...env,
|
|
61
|
+
BASE_URL: "/",
|
|
62
|
+
MODE: mode,
|
|
63
|
+
DEV: true,
|
|
64
|
+
PROD: false,
|
|
65
|
+
SSR: true
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
name: "mantaq-inspect-env",
|
|
69
|
+
enforce: "pre",
|
|
70
|
+
transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
|
|
71
|
+
code: code.replaceAll(ENV_ACCESS, `(${value})`),
|
|
72
|
+
map: null
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The inline config for a load: empty without `--config`; otherwise the
|
|
78
|
+
* project's Vite config (plugins, aliases, define) plus its env files.
|
|
79
|
+
*/
|
|
80
|
+
async function inlineConfig({ config, mode }) {
|
|
81
|
+
const base = {
|
|
82
|
+
configFile: false,
|
|
83
|
+
logLevel: "silent"
|
|
84
|
+
};
|
|
85
|
+
if (config === void 0) return {
|
|
86
|
+
...base,
|
|
87
|
+
...mode === void 0 ? {} : { mode }
|
|
88
|
+
};
|
|
89
|
+
const chosen = mode ?? "development";
|
|
90
|
+
const user = (await loadConfigFromFile({
|
|
91
|
+
command: "serve",
|
|
92
|
+
mode: chosen
|
|
93
|
+
}, config))?.config ?? {};
|
|
94
|
+
const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
|
|
95
|
+
const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
|
|
96
|
+
return {
|
|
97
|
+
...user,
|
|
98
|
+
...base,
|
|
99
|
+
mode: chosen,
|
|
100
|
+
plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
|
|
104
|
+
async function inspectFile(file, options = {}, load = {}) {
|
|
105
|
+
if (load.config !== void 0 && !existsSync(load.config)) return {
|
|
106
|
+
machines: [],
|
|
107
|
+
warnings: [`${file}: Vite config not found: ${load.config}`],
|
|
108
|
+
failed: true
|
|
109
|
+
};
|
|
110
|
+
const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
|
|
111
|
+
...inline,
|
|
112
|
+
plugins: [...inline.plugins ?? [], inspectEntry()]
|
|
113
|
+
})).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
|
|
114
|
+
if (Either.isLeft(loaded)) return {
|
|
115
|
+
machines: [],
|
|
116
|
+
warnings: [`${file}: ${loaded[0]}`],
|
|
117
|
+
failed: true
|
|
118
|
+
};
|
|
119
|
+
const { module } = loaded[1];
|
|
120
|
+
const run = runOf(module);
|
|
121
|
+
if (run === null) return {
|
|
122
|
+
machines: [],
|
|
123
|
+
warnings: [`${file}: could not load the inspector`],
|
|
124
|
+
failed: true
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
...await run(options),
|
|
128
|
+
failed: false
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export { runOf as i, inspectEntry as n, inspectFile as r, entryId as t };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AnyActor } from "@mantaq/core";
|
|
2
|
+
type Made = AnyActor<unknown> | {
|
|
3
|
+
actor: AnyActor<unknown>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a machine factory in a vitest test file so every passing test that
|
|
7
|
+
* builds an actor with it is recorded to `__runs__/<test file>.json` next to
|
|
8
|
+
* the test: the events it handled and how far its clock moved. Run the whole
|
|
9
|
+
* file to refresh it; with `CI` set a stale file fails the test instead.
|
|
10
|
+
* The factory may be called in the test or in `beforeEach`. A retried test is
|
|
11
|
+
* recorded once, from the attempt that passed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function recordRuns<F extends (...args: never[]) => Made>(factory: F): F;
|
|
14
|
+
export {};
|
package/dist/record.mjs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { r as machineId } from "./inspect-B7ZQCNM2.mjs";
|
|
2
|
+
import { Either } from "@mantaq/utils";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
// \0rolldown/runtime.js
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __exportAll = (all, no_symbols) => {
|
|
11
|
+
let target = {};
|
|
12
|
+
for (var name in all) __defProp(target, name, {
|
|
13
|
+
get: all[name],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
17
|
+
return target;
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
21
|
+
key = keys[i];
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
23
|
+
get: ((k) => from[k]).bind(null, key),
|
|
24
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
30
|
+
// ../../node_modules/.pnpm/vite-plus@1.4.0_@types+node@26.2.0_@typescript+native-preview@7.0.0-dev.20260707.2_@vit_594a0913d1d55d70a26e6ccc5f2ebd7d/node_modules/vite-plus/dist/test/index.js
|
|
31
|
+
var test_exports = /* @__PURE__ */ __exportAll({});
|
|
32
|
+
import * as import_vitest from "vitest";
|
|
33
|
+
__reExport(test_exports, import_vitest);
|
|
34
|
+
// src/record.ts
|
|
35
|
+
/** Actors built during the running test attempt, flushed when it ends. */
|
|
36
|
+
let pending = [];
|
|
37
|
+
/** Runs recorded so far, per test file, per machine id. */
|
|
38
|
+
const files = /* @__PURE__ */ new Map();
|
|
39
|
+
function actorOf(made) {
|
|
40
|
+
return "actor" in made ? made.actor : made;
|
|
41
|
+
}
|
|
42
|
+
function isJson(value) {
|
|
43
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
44
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
45
|
+
if (Array.isArray(value)) return value.every(isJson);
|
|
46
|
+
if (typeof value !== "object") return false;
|
|
47
|
+
const proto = Object.getPrototypeOf(value);
|
|
48
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
49
|
+
return Object.values(value).every((each) => each === void 0 || isJson(each));
|
|
50
|
+
}
|
|
51
|
+
function jsonPayload(payload) {
|
|
52
|
+
if (payload !== void 0 && !isJson(payload)) return Either.left("payload is not JSON");
|
|
53
|
+
const text = Either.from(() => JSON.stringify(payload ?? null));
|
|
54
|
+
if (Either.isLeft(text)) return Either.left("payload is not JSON");
|
|
55
|
+
return Either.right(JSON.parse(text[1]));
|
|
56
|
+
}
|
|
57
|
+
function textOf(runs) {
|
|
58
|
+
const body = [...runs].sort(([a], [b]) => a.localeCompare(b)).map(([id, list]) => [id, [...list].sort((a, b) => a.name.localeCompare(b.name))]);
|
|
59
|
+
return `${JSON.stringify(Object.fromEntries(body), null, 2)}\n`;
|
|
60
|
+
}
|
|
61
|
+
function sidecarOf(testPath) {
|
|
62
|
+
return join(dirname(testPath), "__runs__", `${basename(testPath).replace(/\.[cm]?[jt]sx?$/, "")}.json`);
|
|
63
|
+
}
|
|
64
|
+
function check(file, id, run) {
|
|
65
|
+
const committed = Either.from(() => JSON.parse(readFileSync(file, "utf8")));
|
|
66
|
+
const list = Either.isRight(committed) ? committed[1]?.[id] : void 0;
|
|
67
|
+
const found = Array.isArray(list) ? list.find((each) => each?.name === run.name) : void 0;
|
|
68
|
+
if (JSON.stringify(found) !== JSON.stringify(run)) throw new Error(`recordRuns: ${file} is out of date for "${run.name}". Run the tests locally without CI set and commit the result.`);
|
|
69
|
+
}
|
|
70
|
+
function flush(testPath, id, run, runs) {
|
|
71
|
+
const file = sidecarOf(testPath);
|
|
72
|
+
if (process.env["CI"]) return check(file, id, run);
|
|
73
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
74
|
+
writeFileSync(file, textOf(runs));
|
|
75
|
+
}
|
|
76
|
+
function finish(attempt, passed) {
|
|
77
|
+
if (!passed) return;
|
|
78
|
+
const counts = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const each of attempt) {
|
|
80
|
+
if (each.problem !== "") {
|
|
81
|
+
console.warn(`recordRuns: "${each.name}" not recorded, ${each.problem}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const now = each.actor.clock.now();
|
|
85
|
+
if (now > each.seen) each.steps.push({
|
|
86
|
+
kind: "advance",
|
|
87
|
+
ms: now - each.seen
|
|
88
|
+
});
|
|
89
|
+
const count = (counts.get(each.name) ?? 0) + 1;
|
|
90
|
+
counts.set(each.name, count);
|
|
91
|
+
const name = count === 1 ? each.name : `${each.name} #${count}`;
|
|
92
|
+
const runs = files.get(each.testPath) ?? /* @__PURE__ */ new Map();
|
|
93
|
+
files.set(each.testPath, runs);
|
|
94
|
+
const list = runs.get(each.id) ?? [];
|
|
95
|
+
runs.set(each.id, list);
|
|
96
|
+
const run = {
|
|
97
|
+
name,
|
|
98
|
+
end: each.actor.state.name,
|
|
99
|
+
steps: each.steps
|
|
100
|
+
};
|
|
101
|
+
list.push(run);
|
|
102
|
+
flush(each.testPath, each.id, run, runs);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
(0, test_exports.afterEach)(({ task }) => {
|
|
106
|
+
const attempt = pending;
|
|
107
|
+
pending = [];
|
|
108
|
+
finish(attempt, task.result?.state !== "fail");
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Wrap a machine factory in a vitest test file so every passing test that
|
|
112
|
+
* builds an actor with it is recorded to `__runs__/<test file>.json` next to
|
|
113
|
+
* the test: the events it handled and how far its clock moved. Run the whole
|
|
114
|
+
* file to refresh it; with `CI` set a stale file fails the test instead.
|
|
115
|
+
* The factory may be called in the test or in `beforeEach`. A retried test is
|
|
116
|
+
* recorded once, from the attempt that passed.
|
|
117
|
+
*/
|
|
118
|
+
function recordRuns(factory) {
|
|
119
|
+
const id = machineId(factory.name);
|
|
120
|
+
const wrapped = (...args) => {
|
|
121
|
+
const made = factory(...args);
|
|
122
|
+
const actor = actorOf(made);
|
|
123
|
+
const { testPath = "", currentTestName = "" } = test_exports.expect.getState();
|
|
124
|
+
const entry = {
|
|
125
|
+
actor,
|
|
126
|
+
id,
|
|
127
|
+
name: currentTestName,
|
|
128
|
+
testPath,
|
|
129
|
+
steps: [],
|
|
130
|
+
problem: "",
|
|
131
|
+
seen: actor.clock.now()
|
|
132
|
+
};
|
|
133
|
+
pending.push(entry);
|
|
134
|
+
actor.on("transition", { fn: ({ event }) => {
|
|
135
|
+
const now = actor.clock.now();
|
|
136
|
+
if (now > entry.seen) entry.steps.push({
|
|
137
|
+
kind: "advance",
|
|
138
|
+
ms: now - entry.seen
|
|
139
|
+
});
|
|
140
|
+
entry.seen = now;
|
|
141
|
+
const payload = jsonPayload(event.payload);
|
|
142
|
+
if (Either.isLeft(payload)) entry.problem = payload[0];
|
|
143
|
+
else entry.steps.push({
|
|
144
|
+
kind: "event",
|
|
145
|
+
event: event.type,
|
|
146
|
+
payload: payload[1]
|
|
147
|
+
});
|
|
148
|
+
} });
|
|
149
|
+
return made;
|
|
150
|
+
};
|
|
151
|
+
Object.defineProperty(wrapped, "name", { value: factory.name });
|
|
152
|
+
return wrapped;
|
|
153
|
+
}
|
|
154
|
+
export { recordRuns };
|
package/dist/vite.d.mts
CHANGED
package/dist/vite.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as vizDocument, i as projectRoot, o as analyzeModule, r as machineId } from "./inspect-B7ZQCNM2.mjs";
|
|
2
|
+
import { i as runOf, n as inspectEntry, r as inspectFile, t as entryId } from "./load-CWubcSZU.mjs";
|
|
2
3
|
import { UPDATE_EVENT, livePageHtml, serializeViz } from "@mantaq/viz";
|
|
3
4
|
import { Either } from "@mantaq/utils";
|
|
4
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
-
import { globSync, readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, isAbsolute, posix, resolve } from "node:path";
|
|
6
|
+
import { existsSync, globSync, readFileSync } from "node:fs";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { searchForWorkspaceRoot } from "vite";
|
|
8
9
|
// src/vite.ts
|
|
@@ -13,6 +14,9 @@ import { searchForWorkspaceRoot } from "vite";
|
|
|
13
14
|
* import { mantaqViz } from "@mantaq/inspect/vite";
|
|
14
15
|
* export default defineConfig({ plugins: [mantaqViz({ machines: "src/**\/*.machine.ts" })] });
|
|
15
16
|
*
|
|
17
|
+
* `vite build` with the plugin also writes the page as a static site
|
|
18
|
+
* (`index.html`, `viz.json`, a script) under `base`.
|
|
19
|
+
*
|
|
16
20
|
* Open `/__mantaq/` while the dev server runs. Machines load through your own
|
|
17
21
|
* Vite config (aliases, tsconfig paths), are inspected on the server, and the
|
|
18
22
|
* page redraws, keeping the walk, when you save a file.
|
|
@@ -73,6 +77,64 @@ async function documentText(server, context) {
|
|
|
73
77
|
warnings: results.flatMap((result) => result.warnings)
|
|
74
78
|
};
|
|
75
79
|
}
|
|
80
|
+
function stylesheets() {
|
|
81
|
+
return STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n");
|
|
82
|
+
}
|
|
83
|
+
/** Build mode: write the page, `viz.json` and the viewer script under `base`. */
|
|
84
|
+
function siteBuild(context) {
|
|
85
|
+
let standalone = false;
|
|
86
|
+
return {
|
|
87
|
+
name: "mantaq-viz-build",
|
|
88
|
+
apply: "build",
|
|
89
|
+
config: (config) => {
|
|
90
|
+
const input = config.build?.rollupOptions?.input;
|
|
91
|
+
const root = resolve(config.root ?? process.cwd());
|
|
92
|
+
standalone = input === void 0 && !existsSync(resolve(root, "index.html"));
|
|
93
|
+
return standalone ? { build: { rollupOptions: { input: { "mantaq-live": context.live } } } } : {};
|
|
94
|
+
},
|
|
95
|
+
buildStart() {
|
|
96
|
+
if (!standalone) this.emitFile({
|
|
97
|
+
type: "chunk",
|
|
98
|
+
id: context.live,
|
|
99
|
+
name: "mantaq-live"
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
async generateBundle(_options, bundle) {
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const file of machineFiles(context.root(), context.patterns)) {
|
|
105
|
+
const load = { config: context.project.configFile };
|
|
106
|
+
try {
|
|
107
|
+
results.push(await inspectFile(file, { fuzz: context.fuzz }, load));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.warn(`[mantaq-viz] ${file}: could not inspect (${String(error)})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const warning of results.flatMap((result) => result.warnings)) this.warn(`[mantaq-viz] ${warning}`);
|
|
113
|
+
const text = serializeViz(vizDocument(results.flatMap((result) => result.machines)));
|
|
114
|
+
const dir = context.base.replace(/^\//, "");
|
|
115
|
+
const entry = Object.values(bundle).find((chunk) => chunk.type === "chunk" && chunk.isEntry && chunk.name === "mantaq-live");
|
|
116
|
+
if (entry === void 0) {
|
|
117
|
+
this.error("[mantaq-viz] the live page entry was not bundled");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.emitFile({
|
|
121
|
+
type: "asset",
|
|
122
|
+
fileName: `${dir}viz.json`,
|
|
123
|
+
source: text
|
|
124
|
+
});
|
|
125
|
+
this.emitFile({
|
|
126
|
+
type: "asset",
|
|
127
|
+
fileName: `${dir}index.html`,
|
|
128
|
+
source: livePageHtml({
|
|
129
|
+
title: "mantaq viz",
|
|
130
|
+
source: "viz.json",
|
|
131
|
+
entry: posix.relative(dir === "" ? "." : dir, entry.fileName),
|
|
132
|
+
css: stylesheets()
|
|
133
|
+
})
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
76
138
|
function mantaqViz(options) {
|
|
77
139
|
const base = (options.base ?? "/__mantaq/").replace(/\/?$/, "/");
|
|
78
140
|
const patterns = Array.isArray(options.machines) ? options.machines : [options.machines];
|
|
@@ -80,80 +142,92 @@ function mantaqViz(options) {
|
|
|
80
142
|
const live = fileURLToPath(import.meta.resolve(import.meta.url.endsWith(".ts") ? "./live-entry.ts" : "./live-entry.mjs"));
|
|
81
143
|
const viz = dirname(dirname(fileURLToPath(import.meta.resolve("@mantaq/viz/live"))));
|
|
82
144
|
let root = process.cwd();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
root = config.root;
|
|
90
|
-
},
|
|
91
|
-
resolveId: (id) => id === "virtual:mantaq/targets" ? TARGETS_RESOLVED : null,
|
|
92
|
-
load: (id) => id === "\0virtual:mantaq/targets" ? targetsSource(machineFiles(root, patterns)) : null
|
|
145
|
+
const project = { configFile: void 0 };
|
|
146
|
+
const targets = {
|
|
147
|
+
name: "mantaq-viz-targets",
|
|
148
|
+
configResolved: (config) => {
|
|
149
|
+
root = config.root;
|
|
150
|
+
project.configFile = config.configFile;
|
|
93
151
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
112
|
-
server.watcher.on("all", (_event, path) => {
|
|
113
|
-
if (!/\.[cm]?[jt]sx?$/.test(path)) return;
|
|
114
|
-
cached = null;
|
|
115
|
-
const generated = server.moduleGraph.getModuleById(TARGETS_RESOLVED);
|
|
116
|
-
if (generated !== void 0) server.moduleGraph.invalidateModule(generated);
|
|
117
|
-
server.environments.ssr.moduleGraph.invalidateAll();
|
|
118
|
-
server.ws.send({
|
|
119
|
-
type: "custom",
|
|
120
|
-
event: UPDATE_EVENT
|
|
121
|
-
});
|
|
152
|
+
resolveId: (id) => id === "virtual:mantaq/targets" ? TARGETS_RESOLVED : null,
|
|
153
|
+
load: (id) => id === "\0virtual:mantaq/targets" ? targetsSource(machineFiles(root, patterns)) : null
|
|
154
|
+
};
|
|
155
|
+
const plugin = {
|
|
156
|
+
name: "mantaq-viz",
|
|
157
|
+
apply: "serve",
|
|
158
|
+
config: (config) => ({ server: { fs: { allow: [
|
|
159
|
+
searchForWorkspaceRoot(config.root ?? process.cwd()),
|
|
160
|
+
dirname(dirname(live)),
|
|
161
|
+
viz
|
|
162
|
+
] } } }),
|
|
163
|
+
configureServer(server) {
|
|
164
|
+
let cached = null;
|
|
165
|
+
const build = async () => {
|
|
166
|
+
const { text, warnings } = await documentText(server, {
|
|
167
|
+
files: machineFiles(server.config.root, patterns),
|
|
168
|
+
fuzz
|
|
122
169
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
response.setHeader("content-type", "text/html");
|
|
149
|
-
response.end(page);
|
|
170
|
+
for (const warning of warnings) server.config.logger.warn(`[mantaq-viz] ${warning}`);
|
|
171
|
+
return text;
|
|
172
|
+
};
|
|
173
|
+
server.watcher.on("all", (_event, path) => {
|
|
174
|
+
if (!/\.[cm]?[jt]sx?$/.test(path) && !/[\\/]__runs__[\\/][^\\/]+\.json$/.test(path)) return;
|
|
175
|
+
cached = null;
|
|
176
|
+
const generated = server.moduleGraph.getModuleById(TARGETS_RESOLVED);
|
|
177
|
+
if (generated !== void 0) server.moduleGraph.invalidateModule(generated);
|
|
178
|
+
server.environments.ssr.moduleGraph.invalidateAll();
|
|
179
|
+
server.ws.send({
|
|
180
|
+
type: "custom",
|
|
181
|
+
event: UPDATE_EVENT
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
server.middlewares.use((request, response, next) => {
|
|
185
|
+
const path = request.url?.split("?")[0];
|
|
186
|
+
if (path === `${base}viz.json`) {
|
|
187
|
+
cached ??= build();
|
|
188
|
+
cached.then((text) => {
|
|
189
|
+
response.setHeader("content-type", "application/json");
|
|
190
|
+
response.end(text);
|
|
191
|
+
}, (error) => {
|
|
192
|
+
cached = null;
|
|
193
|
+
response.statusCode = 500;
|
|
194
|
+
response.end(String(error));
|
|
150
195
|
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (path !== base && path !== base.slice(0, -1)) {
|
|
199
|
+
next();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const html = livePageHtml({
|
|
203
|
+
title: "mantaq viz",
|
|
204
|
+
source: `${base}viz.json`,
|
|
205
|
+
entry: `/@fs${live}`,
|
|
206
|
+
css: stylesheets()
|
|
151
207
|
});
|
|
152
|
-
server.
|
|
153
|
-
|
|
208
|
+
server.transformIndexHtml(request.url ?? base, html).then((page) => {
|
|
209
|
+
response.setHeader("content-type", "text/html");
|
|
210
|
+
response.end(page);
|
|
154
211
|
});
|
|
155
|
-
}
|
|
212
|
+
});
|
|
213
|
+
server.httpServer?.once("listening", () => {
|
|
214
|
+
server.config.logger.info(` mantaq viz: ${base}`);
|
|
215
|
+
});
|
|
156
216
|
}
|
|
217
|
+
};
|
|
218
|
+
const site = siteBuild({
|
|
219
|
+
base,
|
|
220
|
+
patterns,
|
|
221
|
+
fuzz,
|
|
222
|
+
live,
|
|
223
|
+
project,
|
|
224
|
+
root: () => root
|
|
225
|
+
});
|
|
226
|
+
return [
|
|
227
|
+
inspectEntry(),
|
|
228
|
+
targets,
|
|
229
|
+
site,
|
|
230
|
+
plugin
|
|
157
231
|
];
|
|
158
232
|
}
|
|
159
233
|
export { STYLESHEETS, TARGETS, TARGETS_RESOLVED, mantaqViz, targetsSource };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/inspect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Turn a Mantaq machine into a viz document: structure, handler explanations (AST), fuzz and test coverage. Vite plugin included.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
".": "./dist/index.mjs",
|
|
17
17
|
"./drive": "./dist/drive.mjs",
|
|
18
18
|
"./live-entry": "./dist/live-entry.mjs",
|
|
19
|
+
"./record": "./dist/record.mjs",
|
|
19
20
|
"./vite": "./dist/vite.mjs",
|
|
20
21
|
"./package.json": "./package.json"
|
|
21
22
|
},
|
|
@@ -26,9 +27,9 @@
|
|
|
26
27
|
"typescript": "6.0.3",
|
|
27
28
|
"@mantaq/core": "0.5.4",
|
|
28
29
|
"@mantaq/traversal": "0.3.4",
|
|
29
|
-
"@mantaq/viz": "0.3.0",
|
|
30
30
|
"@mantaq/utils": "0.0.5",
|
|
31
|
-
"@mantaq/pbt": "0.0.4"
|
|
31
|
+
"@mantaq/pbt": "0.0.4",
|
|
32
|
+
"@mantaq/viz": "0.4.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/node": "^26.2.0",
|
|
@@ -36,10 +37,20 @@
|
|
|
36
37
|
"happy-dom": "^21.0.4",
|
|
37
38
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
38
39
|
"vite-plus": "1.4.0",
|
|
40
|
+
"vitest": "5.0.5",
|
|
39
41
|
"@mantaq/sugar": "2.0.4"
|
|
40
42
|
},
|
|
41
43
|
"peerDependencies": {
|
|
42
|
-
"vite": "*"
|
|
44
|
+
"vite": "*",
|
|
45
|
+
"vitest": "*"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"vitest": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"inlinedDependencies": {
|
|
53
|
+
"vite-plus": "1.4.0"
|
|
43
54
|
},
|
|
44
55
|
"scripts": {
|
|
45
56
|
"build": "vp pack",
|