machina-test 3.0.0 → 4.0.0

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 CHANGED
@@ -1,17 +1,17 @@
1
1
  # machina-test
2
2
 
3
- Testing tools for [machina](https://machina-js.org) FSMs — graph topology matchers and property-based runtime testing.
3
+ Testing tools for [machina](https://machina-js.org) FSMs: graph matchers for the wiring, and property-based testing at runtime with `walkAll`.
4
4
 
5
5
  ```ts
6
6
  import "machina-test";
7
7
  import { walkAll } from "machina-test";
8
8
 
9
- // Graph matchers — does the wiring look right?
9
+ // Graph matchers: does the wiring look right?
10
10
  expect(fsm).toHaveNoUnreachableStates();
11
11
  expect(fsm).toAlwaysReach("delivered", { from: "placed" });
12
12
  expect(fsm).toNeverReach("shipped", { from: "cancelled" });
13
13
 
14
- // Runtime testing — does it actually work?
14
+ // Runtime testing: does it work when it runs?
15
15
  walkAll(() => createMyFsm(), {
16
16
  invariant({ ctx }) {
17
17
  /* assert a rule that must always hold */
@@ -29,11 +29,11 @@ npm install --save-dev machina-test
29
29
  pnpm add -D machina-test
30
30
  ```
31
31
 
32
- `machina` >= 6.1.0 is a peer dependency. Either `jest` or `vitest` must be available as the host test runner.
32
+ `machina` >= 8.0.0 is a peer dependency, and the host test runner must be `jest` >= 29 or `vitest` >= 1.
33
33
 
34
34
  ## Setup
35
35
 
36
- Import `machina-test` in your test files (or in a setup file that runs after the framework's globals are available). The import registers the matchers via `expect.extend()` as a side effect.
36
+ Import `machina-test` in your test files, or in a setup file that runs after the framework's globals are available. The import registers the matchers via `expect.extend()` as a side effect.
37
37
 
38
38
  ```ts
39
39
  // in each test file
@@ -43,13 +43,13 @@ import "machina-test";
43
43
  import "machina-test";
44
44
  ```
45
45
 
46
- That's it. TypeScript users get autocomplete for all three matchers automatically.
46
+ TypeScript users get autocomplete for all three matchers automatically.
47
47
 
48
48
  ## Matchers
49
49
 
50
50
  ### `toHaveNoUnreachableStates()`
51
51
 
52
- Asserts that every state in the FSM is reachable from `initialState`. Delegates to machina-inspect's `inspectGraph()`, which recurses into child FSM graphs.
52
+ Asserts that each state in the FSM is reachable from `initialState`. The matcher delegates to machina-inspect's `inspectGraph()`, which recurses into child FSM graphs.
53
53
 
54
54
  ```ts
55
55
  expect(fsm).toHaveNoUnreachableStates();
@@ -57,17 +57,17 @@ expect(fsm).toHaveNoUnreachableStates();
57
57
 
58
58
  ### `toAlwaysReach(targetState, { from })`
59
59
 
60
- Asserts that a path exists from `from` to `targetState` in the FSM's top-level graph. BFS over all edges (both "definite" and "possible"). Top-level graph only — does not traverse into `_child` FSMs.
60
+ Asserts that a path exists from `from` to `targetState` in the FSM's top-level graph. The search is a BFS over all edges, both `"definite"` and `"possible"`.
61
61
 
62
62
  ```ts
63
63
  expect(fsm).toAlwaysReach("delivered", { from: "placed" });
64
64
  ```
65
65
 
66
- "Always" refers to graph topology — the plumbing exists — not runtime certainty. Whether the path executes at runtime depends on which handlers fire.
66
+ "Always" is a claim about the graph: the path exists in the wiring. Whether the path executes at runtime depends on which handlers fire.
67
67
 
68
68
  ### `toNeverReach(targetState, { from })`
69
69
 
70
- Asserts that no path exists from `from` to `targetState`. The logical inverse of `toAlwaysReach`.
70
+ Asserts that `targetState` is unreachable from `from`. It is the logical inverse of `toAlwaysReach`.
71
71
 
72
72
  ```ts
73
73
  expect(fsm).toNeverReach("shipped", { from: "cancelled" });
@@ -84,33 +84,33 @@ expect(fsm).not.toNeverReach("delivered", { from: "placed" });
84
84
 
85
85
  ### Invalid state names
86
86
 
87
- Typos produce clean test failures (not thrown exceptions) with actionable messages:
87
+ A typo in a state name produces a normal test failure, and the message lists the available states:
88
88
 
89
89
  ```
90
90
  State 'shiped' does not exist in FSM 'order-workflow'. Available states: placed, validating, processing, shipped, delivered, cancelled, refunded.
91
91
  ```
92
92
 
93
- ## Testing hierarchical FSMs
93
+ ### Hierarchical FSMs
94
94
 
95
- `toAlwaysReach` and `toNeverReach` operate on the **top-level graph only**. They do not traverse into `_child` FSMs. This is intentional — it avoids ambiguity around composite state names and keeps assertions explicit about which level you're testing.
95
+ `toAlwaysReach` and `toNeverReach` operate on the top-level graph only, and they do not traverse into `_child` FSMs. The limit is intentional: it avoids ambiguity around composite state names, and each assertion states which level it tests.
96
96
 
97
97
  To test a child FSM, pass it directly to `expect()`:
98
98
 
99
99
  ```ts
100
- // Test the parent — sees browsing, checkout, confirmation
100
+ // Test the parent: browsing, checkout, confirmation
101
101
  expect(checkout).toAlwaysReach("confirmation", { from: "browsing" });
102
102
 
103
- // Test the child — sees entering-details, processing, authorized, declined
103
+ // Test the child: entering-details, processing, authorized, declined
104
104
  const payment = createPaymentFsm();
105
105
  expect(payment).toAlwaysReach("authorized", { from: "entering-details" });
106
106
  expect(payment).toNeverReach("entering-details", { from: "authorized" });
107
107
  ```
108
108
 
109
- **The one exception**: `toHaveNoUnreachableStates()` _does_ recurse into children via `inspectGraph()`. An orphaned state in a child surfaces as a failure when called on the parent.
109
+ `toHaveNoUnreachableStates()` is the exception: it recurses into children via `inspectGraph()`. An orphaned state in a child fails the assertion on the parent.
110
110
 
111
- ## walkAll — Runtime Testing
111
+ ## Runtime Testing with `walkAll`
112
112
 
113
- The matchers check graph topology — does a path exist? `walkAll` checks runtime behavior — does the FSM actually work when you feed it random inputs?
113
+ The matchers check graph topology: does a path exist? `walkAll` checks runtime behavior: does the FSM work when you feed it random inputs?
114
114
 
115
115
  ```ts
116
116
  import { walkAll, WalkFailureError } from "machina-test";
@@ -120,12 +120,12 @@ const result = walkAll(
120
120
  {
121
121
  walks: 200, // 200 independent walks
122
122
  maxSteps: 20, // up to 20 handle() calls per walk
123
- seed: 42, // deterministic — same sequence every run
123
+ seed: 42, // deterministic: same sequence every run
124
124
  inputs: {
125
- begin: () => Math.floor(Math.random() * 200),
125
+ begin: () => 100, // payload for handle("begin", 100)
126
126
  },
127
127
  invariant({ ctx }) {
128
- // checked after every transition — throw to fail
128
+ // runs after each transition, throw to fail
129
129
  if ((ctx as any).balance < 0) {
130
130
  throw new Error("balance went negative");
131
131
  }
@@ -134,7 +134,13 @@ const result = walkAll(
134
134
  );
135
135
  ```
136
136
 
137
- On failure, `WalkFailureError` carries the seed, step number, and full input sequence. Pass the seed back to replay the exact walk that failed:
137
+ `inputs` maps an input name to a payload generator. When a walk fires `begin`, walkAll calls `inputs.begin()` and passes the return value to `handle()`. For an input without a generator, walkAll calls `handle()` with the input name alone.
138
+
139
+ The seed drives walkAll's choice of inputs. It does not seed your payload generators, so a generator that calls `Math.random()` breaks replay. When you rely on replay, use deterministic generators or give them their own seeded source.
140
+
141
+ On success, `walkAll` returns a `WalkResult` with the `seed` it used and `walksCompleted`, the number of walks that finished without an invariant violation.
142
+
143
+ On failure, `WalkFailureError` contains the `seed`, `step`, `state`, and `inputSequence` fields. Pass the seed back to replay the exact walk that failed:
138
144
 
139
145
  ```ts
140
146
  try {
@@ -147,13 +153,15 @@ try {
147
153
  }
148
154
  ```
149
155
 
150
- Both `Fsm` and `BehavioralFsm` are supported. See the [docs](https://machina-js.org/tools/machina-test/) for full configuration reference, payload generators, input filtering, and BehavioralFsm client factories.
156
+ If the factory returns an FSM built with an [input payload map](https://machina-js.org/guide/typed-payloads/), the config requires a `payloads` key in place of `inputs`. You supply one tuple-returning generator per payload-carrying input, and the compiler checks the set. A missing or misspelled generator is a compile error, with the input's name in the message.
157
+
158
+ Both `Fsm` and `BehavioralFsm` are supported. See the [docs](https://machina-js.org/tools/machina-test/) for the full configuration reference, payload generators (untyped and typed), input filtering, and BehavioralFsm client factories.
151
159
 
152
- ## See also
160
+ ## See Also
153
161
 
154
- - [machina-inspect](https://www.npmjs.com/package/machina-inspect) — the graph analysis engine the matchers are built on
155
- - [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina) — catch structural issues at lint time in your editor
156
- - [testing-with-machina-test example](../../examples/testing-with-machina-test/) — full working example with matchers and walkAll
162
+ - [machina-inspect](https://www.npmjs.com/package/machina-inspect), the graph analysis engine behind the matchers
163
+ - [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina), the same structural checks at lint time in your editor
164
+ - [testing-with-machina-test](https://github.com/ifandelse/machina.js/tree/master/examples/testing-with-machina-test), a working example with matchers and walkAll
157
165
 
158
166
  ## License
159
167
 
package/dist/index.cjs CHANGED
@@ -221,14 +221,15 @@ const validateConfig = (config, availableInputs) => {
221
221
  * input filters, payload generators, and optional client factory.
222
222
  */
223
223
  const walkAll = (factory, config) => {
224
- const walks = config.walks ?? 100;
225
- const maxSteps = config.maxSteps ?? 50;
224
+ const cfg = config;
225
+ const walks = cfg.walks ?? 100;
226
+ const maxSteps = cfg.maxSteps ?? 50;
226
227
  const probeFsm = factory();
227
- const inputs = validateConfig(config, extractInputs(probeFsm));
228
+ const inputs = validateConfig(cfg, extractInputs(probeFsm));
228
229
  if (typeof probeFsm.dispose === "function") probeFsm.dispose();
229
- const seed = config.seed ?? Math.floor(Math.random() * 2 ** 32);
230
+ const seed = cfg.seed ?? Math.floor(Math.random() * 2 ** 32);
230
231
  const prng = createPrng(seed);
231
- for (let walkIndex = 0; walkIndex < walks; walkIndex++) runSingleWalk(factory, config, inputs, prng, seed, maxSteps);
232
+ for (let walkIndex = 0; walkIndex < walks; walkIndex++) runSingleWalk(factory, cfg, inputs, prng, seed, maxSteps);
232
233
  return {
233
234
  seed,
234
235
  walksCompleted: walks
@@ -302,12 +303,22 @@ const runSingleWalk = (factory, config, inputs, prng, seed, maxSteps) => {
302
303
  for (let step = 1; step <= maxSteps; step++) {
303
304
  currentStep = step;
304
305
  const inputName = inputs[randomInt(prng, inputs.length)];
305
- const payload = config.inputs?.[inputName]?.();
306
+ let extraArgs;
307
+ let payload;
308
+ const tupleGen = config.payloads?.[inputName];
309
+ if (tupleGen) {
310
+ const tuple = tupleGen();
311
+ if (!Array.isArray(tuple)) throw new Error(`walkAll: payloads.${inputName} must return the input's argument tuple (an array)`);
312
+ extraArgs = tuple;
313
+ payload = tuple;
314
+ } else {
315
+ payload = config.inputs?.[inputName]?.();
316
+ extraArgs = payload !== void 0 ? [payload] : [];
317
+ }
306
318
  inputSequence.push({
307
319
  input: inputName,
308
320
  payload
309
321
  });
310
- const extraArgs = payload !== void 0 ? [payload] : [];
311
322
  if (isBehavioral && client !== void 0) fsm.handle(client, inputName, ...extraArgs);
312
323
  else fsm.handle(inputName, ...extraArgs);
313
324
  if (violation !== null) throw new WalkFailureError({
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["MACHINA_TYPE"],"sources":["../src/reachability.ts","../src/matchers.ts","../src/walk.ts","../src/index.ts"],"sourcesContent":["// =============================================================================\n// reachability.ts — BFS reachability check for StateGraph\n//\n// Top-level graph only. Child graphs are not traversed here — users who want\n// to assert reachability inside a child FSM pass the child instance directly\n// to expect().\n//\n// Both \"definite\" and \"possible\" edges are followed. The question is whether\n// the plumbing exists, not whether runtime conditions will always cause it to\n// execute. See design decisions in the build plan for rationale.\n// =============================================================================\n\nimport type { StateGraph } from \"machina-inspect\";\n\n/**\n * Returns true if `to` is reachable from `from` via BFS over the\n * top-level graph's edges. Returns false if either state is missing\n * from the graph, or if no path exists.\n *\n * Treats `from === to` as trivially reachable (every state reaches itself).\n */\nconst canReach = (graph: StateGraph, from: string, to: string): boolean => {\n // Trivially reachable — a state always reaches itself\n if (from === to) {\n return true;\n }\n\n // If the source state isn't in the graph, there's nothing to walk\n if (!(from in graph.nodes)) {\n return false;\n }\n\n const visited = new Set<string>();\n const queue: string[] = [from];\n visited.add(from);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = graph.nodes[current];\n\n // Missing node is a safety check — shouldn't happen in a well-formed graph\n if (!node) {\n continue;\n }\n\n for (const edge of node.edges) {\n if (edge.to === to) {\n return true;\n }\n if (!visited.has(edge.to) && edge.to in graph.nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n return false;\n};\n\nexport { canReach };\n","// =============================================================================\n// matchers.ts — Custom matcher implementations for machina FSMs\n//\n// Each matcher follows the expect.extend() contract:\n// - Receives the subject as `received`\n// - Returns { pass: boolean, message: () => string }\n// - `message` is always the FAILURE message — jest/vitest call it only\n// when the assertion fails (accounting for .not inversion)\n//\n// Invalid state names produce matcher failures, not throws, so they appear\n// as clean test failures in the runner rather than unhandled exceptions.\n// =============================================================================\n\nimport { buildStateGraph, inspectGraph, type StateGraph } from \"machina-inspect\";\nimport type { MachinaInstance } from \"machina\";\nimport { canReach } from \"./reachability\";\nimport type { ReachabilityOptions } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the StateGraph for the received FSM instance.\n * `buildStateGraph` accepts both config objects and live instances;\n * here we always receive live instances (users pass `expect(fsm)`).\n */\nconst getGraph = (received: MachinaInstance): StateGraph => {\n return buildStateGraph(received);\n};\n\n/**\n * Produce the \"invalid state\" failure message. Used when a state name\n * passed to a matcher does not exist in the graph.\n */\nconst invalidStateMessage = (stateName: string, fsmId: string, graph: StateGraph): string => {\n const available = Object.keys(graph.nodes).join(\", \");\n return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${available}.`;\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toHaveNoUnreachableStates\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when the FSM has no states that are unreachable from initialState.\n * Uses inspectGraph() to run all checks and filters for unreachable-state\n * findings, which includes recursive child graph analysis.\n */\nconst toHaveNoUnreachableStates = (received: MachinaInstance): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const findings = inspectGraph(graph).filter(f => f.type === \"unreachable-state\");\n\n const pass = findings.length === 0;\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toHaveNoUnreachableStates() but all states reachable\n return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;\n }\n const stateList = findings.map(f => `'${f.states.join(\", \")}'`).join(\", \");\n return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toAlwaysReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when a path exists from `options.from` to `targetState` in the\n * FSM's top-level graph (BFS over all edges, definite and possible).\n */\nconst toAlwaysReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n const pass = canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toAlwaysReach but path exists\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toNeverReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when NO path exists from `options.from` to `targetState`.\n * The logical inverse of toAlwaysReach.\n */\nconst toNeverReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n // pass: true means \"no path exists\" (the assertion that toNeverReach checks)\n const pass = !canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toNeverReach but no path exists\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n };\n\n return { pass, message };\n};\n\nexport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach };\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// walk.ts — Property-based walk testing for machina FSMs\n//\n// `walkAll` runs multiple randomized input sequences against a live FSM,\n// asserting a user-supplied invariant after every transition. Deterministic\n// replay is guaranteed via a seeded PRNG — on failure, the seed and full input\n// sequence are included in the thrown WalkFailureError so you can reproduce\n// the exact walk that uncovered the bug.\n//\n// Design choices:\n// - Factory function (not FSM instance) as first arg: each walk starts fresh\n// - Throws on failure, returns on success: natural test code, no result-type gymnastics\n// - step counts handle() calls, not transitions: maps 1:1 to input sequence index\n// - Inline mulberry32 PRNG: 5 lines, no dependency, deterministic across platforms\n// - Event-driven invariant: handles _onEnter bounces + deferred replays without\n// monkey-patching handle()\n// =============================================================================\n\nimport { MACHINA_TYPE } from \"machina\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Arguments passed to the invariant callback after every transition.\n * Use these to assert that the FSM is in a valid state after each step.\n */\nexport interface InvariantArgs {\n /** State the FSM just transitioned TO */\n state: string;\n /** State the FSM transitioned FROM */\n previousState: string;\n /** Dot-delimited composite state (includes child states if any) */\n compositeState: string;\n /** The FSM context object (Fsm) or client object (BehavioralFsm) */\n ctx: unknown;\n /** The input name that triggered this transition */\n input: string;\n /** The payload passed to handle(), or undefined if no generator was configured */\n payload: unknown;\n /** Which handle() call produced this transition (1-indexed) */\n step: number;\n}\n\n/**\n * Returned by walkAll on success.\n */\nexport interface WalkResult {\n /** The seed used for this run — pass back to walkAll to replay */\n seed: number;\n /** How many walks completed without an invariant violation */\n walksCompleted: number;\n}\n\n/**\n * Configuration for a walkAll run.\n *\n * @typeParam TClient - The client object type for BehavioralFsm walks.\n * Not relevant for Fsm walks — omit `client` in that case.\n */\nexport interface WalkConfig<TClient extends object = object> {\n /**\n * How many independent walks to run. Each walk creates a fresh FSM\n * via the factory and runs up to `maxSteps` random inputs.\n * Must be >= 1. Default: 100.\n */\n walks?: number;\n\n /**\n * Maximum number of handle() calls per walk.\n * Must be >= 1. Default: 50.\n */\n maxSteps?: number;\n\n /**\n * Seed for the PRNG. Providing the same seed guarantees the identical\n * input sequence — use the seed from WalkResult or WalkFailureError to\n * replay a specific walk. Defaults to a random integer.\n */\n seed?: number;\n\n /**\n * Whitelist of input names to use. Mutually exclusive with `exclude`.\n * If provided, only these inputs will be fired during walks.\n */\n include?: string[];\n\n /**\n * Blacklist of input names to exclude. Mutually exclusive with `include`.\n * These inputs are removed from the auto-extracted set before walking.\n */\n exclude?: string[];\n\n /**\n * Payload generators keyed by input name. When walkAll fires input \"X\"\n * and `inputs[\"X\"]` is defined, it calls that function and passes the\n * return value as the payload to handle(). Inputs without generators\n * are fired with no payload.\n */\n inputs?: Record<string, () => unknown>;\n\n /**\n * BehavioralFsm client factory. Required when the FSM produced by the\n * factory is a BehavioralFsm — each walk calls this to get a fresh client.\n * Omit for Fsm walks.\n */\n client?: () => TClient;\n\n /**\n * Called after every transition. Return `false` OR throw to signal a\n * violation — WalkFailureError is constructed automatically in both cases.\n * Return `true` or `undefined` (void) to indicate the invariant passed.\n */\n invariant: (args: InvariantArgs) => boolean | void;\n}\n\n/** A recorded step in the input sequence — used for replay and error reporting */\ninterface InputRecord {\n input: string;\n payload: unknown;\n}\n\n/**\n * Thrown when walkAll detects an invariant violation. Carries the seed and\n * full input sequence so you can replay the exact walk that failed.\n */\nexport class WalkFailureError extends Error {\n /** Seed that produced this walk — pass to walkAll({ seed }) to replay */\n readonly seed: number;\n /** Which step (1-indexed handle() call) caused the violation */\n readonly step: number;\n /** The inputs fired up to and including the failing step */\n readonly inputSequence: InputRecord[];\n /** State at the time of violation */\n readonly state: string;\n /** State before the violating transition */\n readonly previousState: string;\n /** Composite state at the time of violation */\n readonly compositeState: string;\n /** The FSM context or client at the time of violation */\n readonly ctx: unknown;\n\n constructor(args: {\n seed: number;\n step: number;\n inputSequence: InputRecord[];\n state: string;\n previousState: string;\n compositeState: string;\n ctx: unknown;\n cause: unknown;\n }) {\n const causeMessage = args.cause instanceof Error ? args.cause.message : String(args.cause);\n const sequenceSummary = args.inputSequence\n .map((r, i) => ` ${i + 1}. ${r.input}`)\n .join(\"\\n\");\n\n super(\n `walkAll invariant violation at step ${args.step} (seed: ${args.seed})\\n` +\n ` State: ${args.previousState} → ${args.state}\\n` +\n ` Composite: ${args.compositeState}\\n` +\n ` Cause: ${causeMessage}\\n` +\n `Input sequence:\\n${sequenceSummary}`\n );\n this.name = \"WalkFailureError\";\n this.seed = args.seed;\n this.step = args.step;\n this.inputSequence = args.inputSequence;\n this.state = args.state;\n this.previousState = args.previousState;\n this.compositeState = args.compositeState;\n this.ctx = args.ctx;\n }\n}\n\n// -----------------------------------------------------------------------------\n// Seeded PRNG — mulberry32\n//\n// Passes BigCrush and PractRand. 5 lines, no external dependency.\n// Returns floats in [0, 1) just like Math.random(), enabling drop-in\n// deterministic replacement wherever we'd otherwise use Math.random().\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a seeded PRNG using mulberry32.\n * Returns a function that yields deterministic floats in [0, 1).\n * Two PRNGs with the same seed produce identical sequences.\n */\nexport const createPrng = (seed: number): (() => number) => {\n let s = seed;\n return () => {\n s += 0x6d2b79f5;\n let t = s;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n};\n\n/**\n * Pick a random integer in [0, max) using the given PRNG.\n * Used to select a random index from the inputs array each step.\n */\nexport const randomInt = (prng: () => number, max: number): number => {\n return Math.floor(prng() * max);\n};\n\n// -----------------------------------------------------------------------------\n// Keys that are lifecycle hooks or special directives, not named inputs.\n// Mirrors SpecialStateKeys from machina core types.\n// -----------------------------------------------------------------------------\n\nconst RESERVED_KEYS = new Set([\"_onEnter\", \"_onExit\", \"_child\", \"*\"]);\n\n/**\n * Extract all distinct input names from an FSM's states object.\n * Skips reserved keys (_onEnter, _onExit, _child, *) — these are lifecycle\n * hooks and catch-alls, not named inputs you'd fire via handle().\n */\nexport const extractInputs = (fsm: {\n states: Record<string, Record<string, unknown>>;\n}): string[] => {\n const inputs = new Set<string>();\n for (const stateObj of Object.values(fsm.states)) {\n for (const key of Object.keys(stateObj)) {\n if (!RESERVED_KEYS.has(key)) {\n inputs.add(key);\n }\n }\n }\n return Array.from(inputs);\n};\n\n// -----------------------------------------------------------------------------\n// Config validation\n// -----------------------------------------------------------------------------\n\nconst validateConfig = (config: WalkConfig, availableInputs: string[]): string[] => {\n const walks = config.walks ?? 100;\n const maxSteps = config.maxSteps ?? 50;\n\n if (walks < 1) {\n throw new Error(`walkAll: walks must be >= 1, got ${walks}`);\n }\n\n if (maxSteps < 1) {\n throw new Error(`walkAll: maxSteps must be >= 1, got ${maxSteps}`);\n }\n\n if (config.include && config.exclude) {\n throw new Error(\n \"walkAll: include and exclude are mutually exclusive — use one or the other\"\n );\n }\n\n let inputs: string[];\n\n if (config.include) {\n // Catch typos and misconfigurations before walking — an include entry\n // that doesn't exist in the FSM is almost certainly a bug at the call site.\n const knownInputs = new Set(availableInputs);\n const unknown = config.include.filter(name => !knownInputs.has(name));\n if (unknown.length > 0) {\n throw new Error(\n `walkAll: include contains inputs not found in the FSM: ${unknown.join(\", \")}`\n );\n }\n inputs = config.include;\n } else if (config.exclude) {\n const excluded = new Set(config.exclude);\n inputs = availableInputs.filter(k => !excluded.has(k));\n } else {\n inputs = availableInputs;\n }\n\n if (inputs.length === 0) {\n throw new Error(\n \"walkAll: no inputs available after filtering — check include/exclude config\"\n );\n }\n\n return inputs;\n};\n\n// -----------------------------------------------------------------------------\n// Walk engine\n// -----------------------------------------------------------------------------\n\n/**\n * Run property-based walk tests against a live FSM.\n *\n * Throws WalkFailureError on invariant violation. Returns WalkResult on success.\n *\n * @param factory - Called once per walk to create a fresh FSM instance.\n * Each walk gets its own FSM so context mutations don't bleed between walks.\n * @param config - Walk configuration: invariant, walk count, step limit, seed,\n * input filters, payload generators, and optional client factory.\n */\nexport const walkAll = <TClient extends object = object>(\n factory: () => any,\n config: WalkConfig<TClient>\n): WalkResult => {\n const walks = config.walks ?? 100;\n const maxSteps = config.maxSteps ?? 50;\n\n // Build the input list from the first FSM instance — we only need this\n // for validation, not for actual walking (each walk creates its own FSM).\n const probeFsm = factory();\n const allInputs = extractInputs(probeFsm);\n const inputs = validateConfig(config, allInputs);\n\n // Dispose the probe FSM if possible — it was only needed for input extraction.\n if (typeof probeFsm.dispose === \"function\") {\n probeFsm.dispose();\n }\n\n const seed = config.seed ?? Math.floor(Math.random() * 2 ** 32);\n const prng = createPrng(seed);\n\n for (let walkIndex = 0; walkIndex < walks; walkIndex++) {\n runSingleWalk(factory, config, inputs, prng, seed, maxSteps);\n }\n\n return { seed, walksCompleted: walks };\n};\n\n/**\n * Run one walk: create a fresh FSM, subscribe to events, fire random inputs,\n * check invariant after every transition. Throws WalkFailureError on violation.\n */\nconst runSingleWalk = <TClient extends object>(\n factory: () => any,\n config: WalkConfig<TClient>,\n inputs: string[],\n prng: () => number,\n seed: number,\n maxSteps: number\n): void => {\n const fsm = factory();\n const isBehavioral = fsm[MACHINA_TYPE] === \"BehavioralFsm\";\n\n // For BehavioralFsm: create a client and track it separately.\n // For Fsm: the client IS the fsm (context lives on the fsm).\n const client: TClient | undefined = isBehavioral\n ? config.client\n ? config.client()\n : ({} as TClient)\n : undefined;\n\n // Initialize BehavioralFsm client by transitioning into initialState.\n // Fsm eagerly initializes in its constructor — no action needed.\n if (isBehavioral && client !== undefined) {\n fsm.transition(client, fsm.initialState);\n }\n\n const inputSequence: InputRecord[] = [];\n let currentStep = 0;\n let currentInput = \"\";\n let violation: unknown = null;\n let violationArgs: InvariantArgs | null = null;\n\n // Track \"current input\" via the handling event, which fires before dispatch.\n // This is the input that caused any subsequent transitioned events — including\n // those from _onEnter bounces, where handling does NOT re-fire.\n // Both Fsm and BehavioralFsm expose inputName on this event, so no branching needed.\n const handlingSub = fsm.on(\"handling\", (data: any) => {\n currentInput = data.inputName;\n });\n\n // The transitioned event fires after every state change, including _onEnter\n // bounces. We run the invariant here so we catch all transition types.\n const transitionedSub = isBehavioral\n ? fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n const ctx = data.client ?? client;\n const composite = fsm.compositeState(data.client ?? client);\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n })\n : fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n // For Fsm, read context from fsm.context (Task 1 made this public)\n const ctx = fsm.context;\n const composite = fsm.compositeState();\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n });\n\n try {\n for (let step = 1; step <= maxSteps; step++) {\n currentStep = step;\n const inputName = inputs[randomInt(prng, inputs.length)];\n const payload = config.inputs?.[inputName]?.();\n inputSequence.push({ input: inputName, payload });\n\n // Only spread the payload arg when a generator produced one — handlers\n // with no generator shouldn't receive a spurious undefined argument,\n // which could mask bugs in handlers that check arguments.length.\n const extraArgs = payload !== undefined ? [payload] : [];\n if (isBehavioral && client !== undefined) {\n fsm.handle(client, inputName, ...extraArgs);\n } else {\n fsm.handle(inputName, ...extraArgs);\n }\n\n if (violation !== null) {\n throw new WalkFailureError({\n seed,\n step,\n inputSequence: [...inputSequence],\n state: violationArgs!.state,\n previousState: violationArgs!.previousState,\n compositeState: violationArgs!.compositeState,\n ctx: violationArgs!.ctx,\n cause: violation,\n });\n }\n }\n } finally {\n // Always unsubscribe, even on failure — prevents listener leaks between walks\n handlingSub.off();\n transitionedSub.off();\n\n if (typeof fsm.dispose === \"function\") {\n fsm.dispose();\n }\n }\n};\n","// =============================================================================\n// index.ts — Side-effect entry point for machina-test\n//\n// Importing this module registers all three custom matchers with the global\n// `expect` provided by Jest or Vitest. This is the same pattern used by\n// @testing-library/jest-dom — the import IS the side effect.\n//\n// `expect` must be available as a global at the time this module is evaluated.\n// Put the import in a test file or in a setup file that runs after the test\n// framework's globals are injected.\n// =============================================================================\n\nimport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach } from \"./matchers\";\n\n// Register with the host test runner's global expect\nexpect.extend({ toHaveNoUnreachableStates, toAlwaysReach, toNeverReach });\n\n// Re-export the options type so consumers can import it alongside the package\nexport type { ReachabilityOptions } from \"./types\";\n\n// walkAll is a standalone function — no side-effect registration needed.\n// Import it directly: import { walkAll } from \"machina-test\"\nexport { walkAll } from \"./walk\";\nexport type { WalkConfig, InvariantArgs, WalkResult } from \"./walk\";\nexport { WalkFailureError } from \"./walk\";\n"],"mappings":";;;;;;;;;;;;AAqBA,MAAM,YAAY,OAAmB,MAAc,OAAwB;AAEvE,KAAI,SAAS,GACT,QAAO;AAIX,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;CAGX,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,QAAkB,CAAC,KAAK;AAC9B,SAAQ,IAAI,KAAK;AAEjB,QAAO,MAAM,SAAS,GAAG;EACrB,MAAM,UAAU,MAAM,OAAO;EAC7B,MAAM,OAAO,MAAM,MAAM;AAGzB,MAAI,CAAC,KACD;AAGJ,OAAK,MAAM,QAAQ,KAAK,OAAO;AAC3B,OAAI,KAAK,OAAO,GACZ,QAAO;AAEX,OAAI,CAAC,QAAQ,IAAI,KAAK,GAAG,IAAI,KAAK,MAAM,MAAM,OAAO;AACjD,YAAQ,IAAI,KAAK,GAAG;AACpB,UAAM,KAAK,KAAK,GAAG;;;;AAK/B,QAAO;;;;;;;;;;AC7BX,MAAM,YAAY,aAA0C;AACxD,6CAAuB,SAAS;;;;;;AAOpC,MAAM,uBAAuB,WAAmB,OAAe,UAA8B;AAEzF,QAAO,UAAU,UAAU,2BAA2B,MAAM,uBAD1C,OAAO,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,CACwC;;;;;;;AAYjG,MAAM,6BAA6B,aAAwD;CACvF,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,6CAAwB,MAAM,CAAC,QAAO,MAAK,EAAE,SAAS,oBAAoB;CAEhF,MAAM,OAAO,SAAS,WAAW;CAEjC,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM;EAExC,MAAM,YAAY,SAAS,KAAI,MAAK,IAAI,EAAE,OAAO,KAAK,KAAK,CAAC,GAAG,CAAC,KAAK,KAAK;AAC1E,SAAO,iBAAiB,MAAM,MAAM,8CAA8C,UAAU;;AAGhG,QAAO;EAAE;EAAM;EAAS;;;;;;AAW5B,MAAM,iBACF,UACA,aACA,YAC2B;CAC3B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,EAAE,SAAS;AAGjB,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,MAAM,MAAM,OAAO,MAAM;EAC/D;AAGL,KAAI,EAAE,eAAe,MAAM,OACvB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,aAAa,MAAM,OAAO,MAAM;EACtE;CAGL,MAAM,OAAO,SAAS,OAAO,MAAM,YAAY;CAE/C,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM,0BAA0B,KAAK,QAAQ,YAAY;AAE3F,SAAO,iBAAiB,MAAM,MAAM,yBAAyB,KAAK,QAAQ,YAAY;;AAG1F,QAAO;EAAE;EAAM;EAAS;;;;;;AAW5B,MAAM,gBACF,UACA,aACA,YAC2B;CAC3B,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,EAAE,SAAS;AAGjB,KAAI,EAAE,QAAQ,MAAM,OAChB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,MAAM,MAAM,OAAO,MAAM;EAC/D;AAGL,KAAI,EAAE,eAAe,MAAM,OACvB,QAAO;EACH,MAAM;EACN,eAAe,oBAAoB,aAAa,MAAM,OAAO,MAAM;EACtE;CAIL,MAAM,OAAO,CAAC,SAAS,OAAO,MAAM,YAAY;CAEhD,MAAM,gBAAwB;AAC1B,MAAI,KAEA,QAAO,iBAAiB,MAAM,MAAM,yBAAyB,KAAK,QAAQ,YAAY;AAE1F,SAAO,iBAAiB,MAAM,MAAM,0BAA0B,KAAK,QAAQ,YAAY;;AAG3F,QAAO;EAAE;EAAM;EAAS;;;;;;;;;ACzB5B,IAAa,mBAAb,cAAsC,MAAM;;CAExC,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;;CAET,AAAS;CAET,YAAY,MAST;EACC,MAAM,eAAe,KAAK,iBAAiB,QAAQ,KAAK,MAAM,UAAU,OAAO,KAAK,MAAM;EAC1F,MAAM,kBAAkB,KAAK,cACxB,KAAK,GAAG,MAAM,KAAK,IAAI,EAAE,IAAI,EAAE,QAAQ,CACvC,KAAK,KAAK;AAEf,QACI,uCAAuC,KAAK,KAAK,UAAU,KAAK,KAAK,cACrD,KAAK,cAAc,KAAK,KAAK,MAAM,iBAC/B,KAAK,eAAe,aACxB,aAAa,qBACL,kBAC3B;AACD,OAAK,OAAO;AACZ,OAAK,OAAO,KAAK;AACjB,OAAK,OAAO,KAAK;AACjB,OAAK,gBAAgB,KAAK;AAC1B,OAAK,QAAQ,KAAK;AAClB,OAAK,gBAAgB,KAAK;AAC1B,OAAK,iBAAiB,KAAK;AAC3B,OAAK,MAAM,KAAK;;;;;;;;AAiBxB,MAAa,cAAc,SAAiC;CACxD,IAAI,IAAI;AACR,cAAa;AACT,OAAK;EACL,IAAI,IAAI;AACR,MAAI,KAAK,KAAK,IAAK,MAAM,IAAK,IAAI,EAAE;AACpC,OAAK,IAAI,KAAK,KAAK,IAAK,MAAM,GAAI,IAAI,GAAG;AACzC,WAAS,IAAK,MAAM,QAAS,KAAK;;;;;;;AAQ1C,MAAa,aAAa,MAAoB,QAAwB;AAClE,QAAO,KAAK,MAAM,MAAM,GAAG,IAAI;;AAQnC,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAY;CAAW;CAAU;CAAI,CAAC;;;;;;AAOrE,MAAa,iBAAiB,QAEd;CACZ,MAAM,yBAAS,IAAI,KAAa;AAChC,MAAK,MAAM,YAAY,OAAO,OAAO,IAAI,OAAO,CAC5C,MAAK,MAAM,OAAO,OAAO,KAAK,SAAS,CACnC,KAAI,CAAC,cAAc,IAAI,IAAI,CACvB,QAAO,IAAI,IAAI;AAI3B,QAAO,MAAM,KAAK,OAAO;;AAO7B,MAAM,kBAAkB,QAAoB,oBAAwC;CAChF,MAAM,QAAQ,OAAO,SAAS;CAC9B,MAAM,WAAW,OAAO,YAAY;AAEpC,KAAI,QAAQ,EACR,OAAM,IAAI,MAAM,oCAAoC,QAAQ;AAGhE,KAAI,WAAW,EACX,OAAM,IAAI,MAAM,uCAAuC,WAAW;AAGtE,KAAI,OAAO,WAAW,OAAO,QACzB,OAAM,IAAI,MACN,6EACH;CAGL,IAAI;AAEJ,KAAI,OAAO,SAAS;EAGhB,MAAM,cAAc,IAAI,IAAI,gBAAgB;EAC5C,MAAM,UAAU,OAAO,QAAQ,QAAO,SAAQ,CAAC,YAAY,IAAI,KAAK,CAAC;AACrE,MAAI,QAAQ,SAAS,EACjB,OAAM,IAAI,MACN,0DAA0D,QAAQ,KAAK,KAAK,GAC/E;AAEL,WAAS,OAAO;YACT,OAAO,SAAS;EACvB,MAAM,WAAW,IAAI,IAAI,OAAO,QAAQ;AACxC,WAAS,gBAAgB,QAAO,MAAK,CAAC,SAAS,IAAI,EAAE,CAAC;OAEtD,UAAS;AAGb,KAAI,OAAO,WAAW,EAClB,OAAM,IAAI,MACN,8EACH;AAGL,QAAO;;;;;;;;;;;;AAiBX,MAAa,WACT,SACA,WACa;CACb,MAAM,QAAQ,OAAO,SAAS;CAC9B,MAAM,WAAW,OAAO,YAAY;CAIpC,MAAM,WAAW,SAAS;CAE1B,MAAM,SAAS,eAAe,QADZ,cAAc,SAAS,CACO;AAGhD,KAAI,OAAO,SAAS,YAAY,WAC5B,UAAS,SAAS;CAGtB,MAAM,OAAO,OAAO,QAAQ,KAAK,MAAM,KAAK,QAAQ,GAAG,KAAK,GAAG;CAC/D,MAAM,OAAO,WAAW,KAAK;AAE7B,MAAK,IAAI,YAAY,GAAG,YAAY,OAAO,YACvC,eAAc,SAAS,QAAQ,QAAQ,MAAM,MAAM,SAAS;AAGhE,QAAO;EAAE;EAAM,gBAAgB;EAAO;;;;;;AAO1C,MAAM,iBACF,SACA,QACA,QACA,MACA,MACA,aACO;CACP,MAAM,MAAM,SAAS;CACrB,MAAM,eAAe,IAAIA,0BAAkB;CAI3C,MAAM,SAA8B,eAC9B,OAAO,SACH,OAAO,QAAQ,GACd,EAAE,GACP;AAIN,KAAI,gBAAgB,WAAW,OAC3B,KAAI,WAAW,QAAQ,IAAI,aAAa;CAG5C,MAAM,gBAA+B,EAAE;CACvC,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI,YAAqB;CACzB,IAAI,gBAAsC;CAM1C,MAAM,cAAc,IAAI,GAAG,aAAa,SAAc;AAClD,iBAAe,KAAK;GACtB;CAIF,MAAM,kBAAkB,eAClB,IAAI,GAAG,iBAAiB,SAAc;AAClC,MAAI,cAAc,KACd;EAEJ,MAAM,MAAM,KAAK,UAAU;EAC3B,MAAM,YAAY,IAAI,eAAe,KAAK,UAAU,OAAO;EAC3D,MAAM,SAAS,cAAc,cAAc,SAAS;EACpD,MAAM,OAAsB;GACxB,OAAO,KAAK;GACZ,eAAe,KAAK;GACpB,gBAAgB;GAChB;GACA,OAAO;GACP,SAAS,QAAQ;GACjB,MAAM;GACT;AACD,MAAI;AAIA,OAHe,OAAO,UAAU,KAAK,KAGtB,OAAO;AAClB,gCAAY,IAAI,MAAM,2BAA2B;AACjD,oBAAgB;;WAEf,KAAK;AACV,eAAY;AACZ,mBAAgB;;GAEtB,GACF,IAAI,GAAG,iBAAiB,SAAc;AAClC,MAAI,cAAc,KACd;EAGJ,MAAM,MAAM,IAAI;EAChB,MAAM,YAAY,IAAI,gBAAgB;EACtC,MAAM,SAAS,cAAc,cAAc,SAAS;EACpD,MAAM,OAAsB;GACxB,OAAO,KAAK;GACZ,eAAe,KAAK;GACpB,gBAAgB;GAChB;GACA,OAAO;GACP,SAAS,QAAQ;GACjB,MAAM;GACT;AACD,MAAI;AAIA,OAHe,OAAO,UAAU,KAAK,KAGtB,OAAO;AAClB,gCAAY,IAAI,MAAM,2BAA2B;AACjD,oBAAgB;;WAEf,KAAK;AACV,eAAY;AACZ,mBAAgB;;GAEtB;AAER,KAAI;AACA,OAAK,IAAI,OAAO,GAAG,QAAQ,UAAU,QAAQ;AACzC,iBAAc;GACd,MAAM,YAAY,OAAO,UAAU,MAAM,OAAO,OAAO;GACvD,MAAM,UAAU,OAAO,SAAS,cAAc;AAC9C,iBAAc,KAAK;IAAE,OAAO;IAAW;IAAS,CAAC;GAKjD,MAAM,YAAY,YAAY,SAAY,CAAC,QAAQ,GAAG,EAAE;AACxD,OAAI,gBAAgB,WAAW,OAC3B,KAAI,OAAO,QAAQ,WAAW,GAAG,UAAU;OAE3C,KAAI,OAAO,WAAW,GAAG,UAAU;AAGvC,OAAI,cAAc,KACd,OAAM,IAAI,iBAAiB;IACvB;IACA;IACA,eAAe,CAAC,GAAG,cAAc;IACjC,OAAO,cAAe;IACtB,eAAe,cAAe;IAC9B,gBAAgB,cAAe;IAC/B,KAAK,cAAe;IACpB,OAAO;IACV,CAAC;;WAGJ;AAEN,cAAY,KAAK;AACjB,kBAAgB,KAAK;AAErB,MAAI,OAAO,IAAI,YAAY,WACvB,KAAI,SAAS;;;;;;ACvczB,OAAO,OAAO;CAAE;CAA2B;CAAe;CAAc,CAAC"}
1
+ {"version":3,"file":"index.cjs","names":["MACHINA_TYPE"],"sources":["../src/reachability.ts","../src/matchers.ts","../src/walk.ts","../src/index.ts"],"sourcesContent":["// =============================================================================\n// reachability.ts — BFS reachability check for StateGraph\n//\n// Top-level graph only. Child graphs are not traversed here — users who want\n// to assert reachability inside a child FSM pass the child instance directly\n// to expect().\n//\n// Both \"definite\" and \"possible\" edges are followed. The question is whether\n// the plumbing exists, not whether runtime conditions will always cause it to\n// execute. See design decisions in the build plan for rationale.\n// =============================================================================\n\nimport type { StateGraph } from \"machina-inspect\";\n\n/**\n * Returns true if `to` is reachable from `from` via BFS over the\n * top-level graph's edges. Returns false if either state is missing\n * from the graph, or if no path exists.\n *\n * Treats `from === to` as trivially reachable (every state reaches itself).\n */\nconst canReach = (graph: StateGraph, from: string, to: string): boolean => {\n // Trivially reachable — a state always reaches itself\n if (from === to) {\n return true;\n }\n\n // If the source state isn't in the graph, there's nothing to walk\n if (!(from in graph.nodes)) {\n return false;\n }\n\n const visited = new Set<string>();\n const queue: string[] = [from];\n visited.add(from);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = graph.nodes[current];\n\n // Missing node is a safety check — shouldn't happen in a well-formed graph\n if (!node) {\n continue;\n }\n\n for (const edge of node.edges) {\n if (edge.to === to) {\n return true;\n }\n if (!visited.has(edge.to) && edge.to in graph.nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n return false;\n};\n\nexport { canReach };\n","// =============================================================================\n// matchers.ts — Custom matcher implementations for machina FSMs\n//\n// Each matcher follows the expect.extend() contract:\n// - Receives the subject as `received`\n// - Returns { pass: boolean, message: () => string }\n// - `message` is always the FAILURE message — jest/vitest call it only\n// when the assertion fails (accounting for .not inversion)\n//\n// Invalid state names produce matcher failures, not throws, so they appear\n// as clean test failures in the runner rather than unhandled exceptions.\n// =============================================================================\n\nimport { buildStateGraph, inspectGraph, type StateGraph } from \"machina-inspect\";\nimport type { MachinaInstance } from \"machina\";\nimport { canReach } from \"./reachability\";\nimport type { ReachabilityOptions } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the StateGraph for the received FSM instance.\n * `buildStateGraph` accepts both config objects and live instances;\n * here we always receive live instances (users pass `expect(fsm)`).\n */\nconst getGraph = (received: MachinaInstance): StateGraph => {\n return buildStateGraph(received);\n};\n\n/**\n * Produce the \"invalid state\" failure message. Used when a state name\n * passed to a matcher does not exist in the graph.\n */\nconst invalidStateMessage = (stateName: string, fsmId: string, graph: StateGraph): string => {\n const available = Object.keys(graph.nodes).join(\", \");\n return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${available}.`;\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toHaveNoUnreachableStates\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when the FSM has no states that are unreachable from initialState.\n * Uses inspectGraph() to run all checks and filters for unreachable-state\n * findings, which includes recursive child graph analysis.\n */\nconst toHaveNoUnreachableStates = (received: MachinaInstance): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const findings = inspectGraph(graph).filter(f => f.type === \"unreachable-state\");\n\n const pass = findings.length === 0;\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toHaveNoUnreachableStates() but all states reachable\n return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;\n }\n const stateList = findings.map(f => `'${f.states.join(\", \")}'`).join(\", \");\n return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toAlwaysReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when a path exists from `options.from` to `targetState` in the\n * FSM's top-level graph (BFS over all edges, definite and possible).\n */\nconst toAlwaysReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n const pass = canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toAlwaysReach but path exists\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toNeverReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when NO path exists from `options.from` to `targetState`.\n * The logical inverse of toAlwaysReach.\n */\nconst toNeverReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n // pass: true means \"no path exists\" (the assertion that toNeverReach checks)\n const pass = !canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toNeverReach but no path exists\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n };\n\n return { pass, message };\n};\n\nexport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach };\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// walk.ts — Property-based walk testing for machina FSMs\n//\n// `walkAll` runs multiple randomized input sequences against a live FSM,\n// asserting a user-supplied invariant after every transition. Deterministic\n// replay is guaranteed via a seeded PRNG — on failure, the seed and full input\n// sequence are included in the thrown WalkFailureError so you can reproduce\n// the exact walk that uncovered the bug.\n//\n// Design choices:\n// - Factory function (not FSM instance) as first arg: each walk starts fresh\n// - Throws on failure, returns on success: natural test code, no result-type gymnastics\n// - step counts handle() calls, not transitions: maps 1:1 to input sequence index\n// - Inline mulberry32 PRNG: 5 lines, no dependency, deterministic across platforms\n// - Event-driven invariant: handles _onEnter bounces + deferred replays without\n// monkey-patching handle()\n// =============================================================================\n\nimport { MACHINA_TYPE, type InputMap, type InputMapOfInstance } from \"machina\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Arguments passed to the invariant callback after every transition.\n * Use these to assert that the FSM is in a valid state after each step.\n */\nexport interface InvariantArgs {\n /** State the FSM just transitioned TO */\n state: string;\n /** State the FSM transitioned FROM */\n previousState: string;\n /** Dot-delimited composite state (includes child states if any) */\n compositeState: string;\n /** The FSM context object (Fsm) or client object (BehavioralFsm) */\n ctx: unknown;\n /** The input name that triggered this transition */\n input: string;\n /**\n * What the generator produced for this step, or undefined if none was\n * configured. Legacy `inputs` generators: the single value passed to\n * handle(). Typed `payloads` generators: the full argument tuple.\n */\n payload: unknown;\n /** Which handle() call produced this transition (1-indexed) */\n step: number;\n}\n\n/**\n * Returned by walkAll on success.\n */\nexport interface WalkResult {\n /** The seed used for this run — pass back to walkAll to replay */\n seed: number;\n /** How many walks completed without an invariant violation */\n walksCompleted: number;\n}\n\n/**\n * Configuration for a walkAll run.\n *\n * @typeParam TClient - The client object type for BehavioralFsm walks.\n * Not relevant for Fsm walks — omit `client` in that case.\n * @typeParam TInputs - The FSM's input payload map (#195), extracted from\n * the factory's return type. Under the wide default (untyped FSMs), the\n * config carries the legacy `inputs` generators. Under a real map, the\n * config requires `payloads` instead — one typed, tuple-returning\n * generator per payload-carrying input.\n */\nexport type WalkConfig<\n TClient extends object = object,\n TInputs = InputMap,\n> = WalkConfigBase<TClient> & PayloadGeneratorsFor<TInputs>;\n\n/** Input names in the map whose tuples are non-empty — they carry payloads. */\ntype PayloadCarryingInputs<TInputs> = {\n [K in keyof TInputs & string]: TInputs[K] extends [] ? never : K;\n}[keyof TInputs & string];\n\n/**\n * The generator half of WalkConfig, selected by the map.\n *\n * Wide map (untyped FSM): the legacy `inputs` shape — string-keyed, one\n * value per call, passed as one argument.\n *\n * Real map with payload-carrying inputs: `payloads` is REQUIRED, with one\n * key per payload-carrying input. Each generator returns that input's full\n * argument tuple and walkAll spreads it. This is what turns a forgotten\n * generator from a mid-walk TypeError into a compile error naming the\n * input. The legacy `inputs` key is absent on this path on purpose — one\n * way to do it.\n *\n * Real map with no payload-carrying inputs: nothing to generate, so the\n * config carries neither key.\n */\ntype PayloadGeneratorsFor<TInputs> = string extends keyof TInputs\n ? {\n /**\n * Payload generators keyed by input name. When walkAll fires input\n * \"X\" and `inputs[\"X\"]` is defined, it calls that function and\n * passes the return value as the payload to handle(). Inputs\n * without generators are fired with no payload.\n *\n * If the FSM fixes an input payload map (machina #195), walkAll's\n * config switches to the typed `payloads` key instead, and the\n * compiler requires a generator for every payload-carrying input.\n */\n inputs?: Record<string, () => unknown>;\n }\n : [PayloadCarryingInputs<TInputs>] extends [never]\n ? unknown\n : {\n /**\n * Typed payload generators, one per payload-carrying input in\n * the FSM's map. Each returns the input's full argument tuple;\n * walkAll spreads it into handle(). Required — handlers written\n * against a map trust their payloads, so every payload-carrying\n * input a walk can fire must have a generator.\n */\n payloads: {\n [K in PayloadCarryingInputs<TInputs>]: () => TInputs[K];\n };\n };\n\n/**\n * The generator-independent half of WalkConfig. Exported only through\n * `WalkConfig` — consumers never name this directly.\n */\ninterface WalkConfigBase<TClient extends object = object> {\n /**\n * How many independent walks to run. Each walk creates a fresh FSM\n * via the factory and runs up to `maxSteps` random inputs.\n * Must be >= 1. Default: 100.\n */\n walks?: number;\n\n /**\n * Maximum number of handle() calls per walk.\n * Must be >= 1. Default: 50.\n */\n maxSteps?: number;\n\n /**\n * Seed for the PRNG. Providing the same seed guarantees the identical\n * input sequence — use the seed from WalkResult or WalkFailureError to\n * replay a specific walk. Defaults to a random integer.\n */\n seed?: number;\n\n /**\n * Whitelist of input names to use. Mutually exclusive with `exclude`.\n * If provided, only these inputs will be fired during walks.\n */\n include?: string[];\n\n /**\n * Blacklist of input names to exclude. Mutually exclusive with `include`.\n * These inputs are removed from the auto-extracted set before walking.\n */\n exclude?: string[];\n\n /**\n * BehavioralFsm client factory. Required when the FSM produced by the\n * factory is a BehavioralFsm — each walk calls this to get a fresh client.\n * Omit for Fsm walks.\n */\n client?: () => TClient;\n\n /**\n * Called after every transition. Return `false` OR throw to signal a\n * violation — WalkFailureError is constructed automatically in both cases.\n * Return `true` or `undefined` (void) to indicate the invariant passed.\n */\n invariant: (args: InvariantArgs) => boolean | void;\n}\n\n/**\n * Runtime view of the config: both generator keys optional, element types\n * erased. The conditional half of WalkConfig exists for callers; the walk\n * loop treats the two keys uniformly, with `payloads` taking precedence.\n */\ntype RuntimeWalkConfig<TClient extends object> = WalkConfigBase<TClient> & {\n inputs?: Record<string, () => unknown>;\n payloads?: Record<string, () => unknown[]>;\n};\n\n/** A recorded step in the input sequence — used for replay and error reporting */\ninterface InputRecord {\n input: string;\n payload: unknown;\n}\n\n/**\n * Thrown when walkAll detects an invariant violation. Carries the seed and\n * full input sequence so you can replay the exact walk that failed.\n */\nexport class WalkFailureError extends Error {\n /** Seed that produced this walk — pass to walkAll({ seed }) to replay */\n readonly seed: number;\n /** Which step (1-indexed handle() call) caused the violation */\n readonly step: number;\n /** The inputs fired up to and including the failing step */\n readonly inputSequence: InputRecord[];\n /** State at the time of violation */\n readonly state: string;\n /** State before the violating transition */\n readonly previousState: string;\n /** Composite state at the time of violation */\n readonly compositeState: string;\n /** The FSM context or client at the time of violation */\n readonly ctx: unknown;\n\n constructor(args: {\n seed: number;\n step: number;\n inputSequence: InputRecord[];\n state: string;\n previousState: string;\n compositeState: string;\n ctx: unknown;\n cause: unknown;\n }) {\n const causeMessage = args.cause instanceof Error ? args.cause.message : String(args.cause);\n const sequenceSummary = args.inputSequence\n .map((r, i) => ` ${i + 1}. ${r.input}`)\n .join(\"\\n\");\n\n super(\n `walkAll invariant violation at step ${args.step} (seed: ${args.seed})\\n` +\n ` State: ${args.previousState} → ${args.state}\\n` +\n ` Composite: ${args.compositeState}\\n` +\n ` Cause: ${causeMessage}\\n` +\n `Input sequence:\\n${sequenceSummary}`\n );\n this.name = \"WalkFailureError\";\n this.seed = args.seed;\n this.step = args.step;\n this.inputSequence = args.inputSequence;\n this.state = args.state;\n this.previousState = args.previousState;\n this.compositeState = args.compositeState;\n this.ctx = args.ctx;\n }\n}\n\n// -----------------------------------------------------------------------------\n// Seeded PRNG — mulberry32\n//\n// Passes BigCrush and PractRand. 5 lines, no external dependency.\n// Returns floats in [0, 1) just like Math.random(), enabling drop-in\n// deterministic replacement wherever we'd otherwise use Math.random().\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a seeded PRNG using mulberry32.\n * Returns a function that yields deterministic floats in [0, 1).\n * Two PRNGs with the same seed produce identical sequences.\n */\nexport const createPrng = (seed: number): (() => number) => {\n let s = seed;\n return () => {\n s += 0x6d2b79f5;\n let t = s;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n};\n\n/**\n * Pick a random integer in [0, max) using the given PRNG.\n * Used to select a random index from the inputs array each step.\n */\nexport const randomInt = (prng: () => number, max: number): number => {\n return Math.floor(prng() * max);\n};\n\n// -----------------------------------------------------------------------------\n// Keys that are lifecycle hooks or special directives, not named inputs.\n// Mirrors SpecialStateKeys from machina core types.\n// -----------------------------------------------------------------------------\n\nconst RESERVED_KEYS = new Set([\"_onEnter\", \"_onExit\", \"_child\", \"*\"]);\n\n/**\n * Extract all distinct input names from an FSM's states object.\n * Skips reserved keys (_onEnter, _onExit, _child, *) — these are lifecycle\n * hooks and catch-alls, not named inputs you'd fire via handle().\n */\nexport const extractInputs = (fsm: {\n states: Record<string, Record<string, unknown>>;\n}): string[] => {\n const inputs = new Set<string>();\n for (const stateObj of Object.values(fsm.states)) {\n for (const key of Object.keys(stateObj)) {\n if (!RESERVED_KEYS.has(key)) {\n inputs.add(key);\n }\n }\n }\n return Array.from(inputs);\n};\n\n// -----------------------------------------------------------------------------\n// Config validation\n// -----------------------------------------------------------------------------\n\nconst validateConfig = (config: WalkConfigBase, availableInputs: string[]): string[] => {\n const walks = config.walks ?? 100;\n const maxSteps = config.maxSteps ?? 50;\n\n if (walks < 1) {\n throw new Error(`walkAll: walks must be >= 1, got ${walks}`);\n }\n\n if (maxSteps < 1) {\n throw new Error(`walkAll: maxSteps must be >= 1, got ${maxSteps}`);\n }\n\n if (config.include && config.exclude) {\n throw new Error(\n \"walkAll: include and exclude are mutually exclusive — use one or the other\"\n );\n }\n\n let inputs: string[];\n\n if (config.include) {\n // Catch typos and misconfigurations before walking — an include entry\n // that doesn't exist in the FSM is almost certainly a bug at the call site.\n const knownInputs = new Set(availableInputs);\n const unknown = config.include.filter(name => !knownInputs.has(name));\n if (unknown.length > 0) {\n throw new Error(\n `walkAll: include contains inputs not found in the FSM: ${unknown.join(\", \")}`\n );\n }\n inputs = config.include;\n } else if (config.exclude) {\n const excluded = new Set(config.exclude);\n inputs = availableInputs.filter(k => !excluded.has(k));\n } else {\n inputs = availableInputs;\n }\n\n if (inputs.length === 0) {\n throw new Error(\n \"walkAll: no inputs available after filtering — check include/exclude config\"\n );\n }\n\n return inputs;\n};\n\n// -----------------------------------------------------------------------------\n// Walk engine\n// -----------------------------------------------------------------------------\n\n/**\n * Run property-based walk tests against a live FSM.\n *\n * Throws WalkFailureError on invariant violation. Returns WalkResult on success.\n *\n * @param factory - Called once per walk to create a fresh FSM instance.\n * Each walk gets its own FSM so context mutations don't bleed between walks.\n * @param config - Walk configuration: invariant, walk count, step limit, seed,\n * input filters, payload generators, and optional client factory.\n */\nexport const walkAll = <TFsm = unknown, TClient extends object = object>(\n factory: () => TFsm,\n config: WalkConfig<TClient, InputMapOfInstance<TFsm>>\n): WalkResult => {\n // The conditional generator half of WalkConfig exists for callers; the\n // runtime treats both generator keys uniformly through this view.\n const cfg = config as RuntimeWalkConfig<TClient>;\n const walks = cfg.walks ?? 100;\n const maxSteps = cfg.maxSteps ?? 50;\n\n // Build the input list from the first FSM instance — we only need this\n // for validation, not for actual walking (each walk creates its own FSM).\n const probeFsm = factory() as { states: Record<string, Record<string, unknown>> } & {\n dispose?: () => void;\n };\n const allInputs = extractInputs(probeFsm);\n const inputs = validateConfig(cfg, allInputs);\n\n // Dispose the probe FSM if possible — it was only needed for input extraction.\n if (typeof probeFsm.dispose === \"function\") {\n probeFsm.dispose();\n }\n\n const seed = cfg.seed ?? Math.floor(Math.random() * 2 ** 32);\n const prng = createPrng(seed);\n\n for (let walkIndex = 0; walkIndex < walks; walkIndex++) {\n runSingleWalk(factory, cfg, inputs, prng, seed, maxSteps);\n }\n\n return { seed, walksCompleted: walks };\n};\n\n/**\n * Run one walk: create a fresh FSM, subscribe to events, fire random inputs,\n * check invariant after every transition. Throws WalkFailureError on violation.\n */\nconst runSingleWalk = <TClient extends object>(\n factory: () => unknown,\n config: RuntimeWalkConfig<TClient>,\n inputs: string[],\n prng: () => number,\n seed: number,\n maxSteps: number\n): void => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the walk loop drives any FSM shape; the public walkAll signature owns the typing\n const fsm = factory() as any;\n const isBehavioral = fsm[MACHINA_TYPE] === \"BehavioralFsm\";\n\n // For BehavioralFsm: create a client and track it separately.\n // For Fsm: the client IS the fsm (context lives on the fsm).\n const client: TClient | undefined = isBehavioral\n ? config.client\n ? config.client()\n : ({} as TClient)\n : undefined;\n\n // Initialize BehavioralFsm client by transitioning into initialState.\n // Fsm eagerly initializes in its constructor — no action needed.\n if (isBehavioral && client !== undefined) {\n fsm.transition(client, fsm.initialState);\n }\n\n const inputSequence: InputRecord[] = [];\n let currentStep = 0;\n let currentInput = \"\";\n let violation: unknown = null;\n let violationArgs: InvariantArgs | null = null;\n\n // Track \"current input\" via the handling event, which fires before dispatch.\n // This is the input that caused any subsequent transitioned events — including\n // those from _onEnter bounces, where handling does NOT re-fire.\n // Both Fsm and BehavioralFsm expose inputName on this event, so no branching needed.\n const handlingSub = fsm.on(\"handling\", (data: any) => {\n currentInput = data.inputName;\n });\n\n // The transitioned event fires after every state change, including _onEnter\n // bounces. We run the invariant here so we catch all transition types.\n const transitionedSub = isBehavioral\n ? fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n const ctx = data.client ?? client;\n const composite = fsm.compositeState(data.client ?? client);\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n })\n : fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n // For Fsm, read context from fsm.context (Task 1 made this public)\n const ctx = fsm.context;\n const composite = fsm.compositeState();\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n });\n\n try {\n for (let step = 1; step <= maxSteps; step++) {\n currentStep = step;\n const inputName = inputs[randomInt(prng, inputs.length)];\n let extraArgs: unknown[];\n let payload: unknown;\n const tupleGen = config.payloads?.[inputName];\n if (tupleGen) {\n // Typed generators return the input's full argument tuple;\n // walkAll spreads it. An untyped caller reaching this path\n // from JavaScript could return anything — fail loudly rather\n // than guess whether a non-array was meant as one argument.\n const tuple = tupleGen();\n if (!Array.isArray(tuple)) {\n throw new Error(\n `walkAll: payloads.${inputName} must return the input's argument tuple (an array)`\n );\n }\n extraArgs = tuple;\n payload = tuple;\n } else {\n // Legacy generators return one value, passed as one argument.\n // Only spread when a generator produced one — handlers with no\n // generator shouldn't receive a spurious undefined argument,\n // which could mask bugs in handlers that check arguments.length.\n payload = config.inputs?.[inputName]?.();\n extraArgs = payload !== undefined ? [payload] : [];\n }\n inputSequence.push({ input: inputName, payload });\n if (isBehavioral && client !== undefined) {\n fsm.handle(client, inputName, ...extraArgs);\n } else {\n fsm.handle(inputName, ...extraArgs);\n }\n\n if (violation !== null) {\n throw new WalkFailureError({\n seed,\n step,\n inputSequence: [...inputSequence],\n state: violationArgs!.state,\n previousState: violationArgs!.previousState,\n compositeState: violationArgs!.compositeState,\n ctx: violationArgs!.ctx,\n cause: violation,\n });\n }\n }\n } finally {\n // Always unsubscribe, even on failure — prevents listener leaks between walks\n handlingSub.off();\n transitionedSub.off();\n\n if (typeof fsm.dispose === \"function\") {\n fsm.dispose();\n }\n }\n};\n","// =============================================================================\n// index.ts — Side-effect entry point for machina-test\n//\n// Importing this module registers all three custom matchers with the global\n// `expect` provided by Jest or Vitest. This is the same pattern used by\n// @testing-library/jest-dom — the import IS the side effect.\n//\n// `expect` must be available as a global at the time this module is evaluated.\n// Put the import in a test file or in a setup file that runs after the test\n// framework's globals are injected.\n// =============================================================================\n\nimport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach } from \"./matchers\";\n\n// Register with the host test runner's global expect\nexpect.extend({ toHaveNoUnreachableStates, toAlwaysReach, toNeverReach });\n\n// Re-export the options type so consumers can import it alongside the package\nexport type { ReachabilityOptions } from \"./types\";\n\n// walkAll is a standalone function — no side-effect registration needed.\n// Import it directly: import { walkAll } from \"machina-test\"\nexport { walkAll } from \"./walk\";\nexport type { WalkConfig, InvariantArgs, WalkResult } from \"./walk\";\nexport { WalkFailureError } from \"./walk\";\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,5 @@
1
+ import { InputMap, InputMapOfInstance } from "machina";
2
+
1
3
  //#region src/types.d.ts
2
4
  interface ReachabilityOptions {
3
5
  /** The state to start the BFS from */
@@ -62,7 +64,11 @@ interface InvariantArgs {
62
64
  ctx: unknown;
63
65
  /** The input name that triggered this transition */
64
66
  input: string;
65
- /** The payload passed to handle(), or undefined if no generator was configured */
67
+ /**
68
+ * What the generator produced for this step, or undefined if none was
69
+ * configured. Legacy `inputs` generators: the single value passed to
70
+ * handle(). Typed `payloads` generators: the full argument tuple.
71
+ */
66
72
  payload: unknown;
67
73
  /** Which handle() call produced this transition (1-indexed) */
68
74
  step: number;
@@ -81,8 +87,58 @@ interface WalkResult {
81
87
  *
82
88
  * @typeParam TClient - The client object type for BehavioralFsm walks.
83
89
  * Not relevant for Fsm walks — omit `client` in that case.
90
+ * @typeParam TInputs - The FSM's input payload map (#195), extracted from
91
+ * the factory's return type. Under the wide default (untyped FSMs), the
92
+ * config carries the legacy `inputs` generators. Under a real map, the
93
+ * config requires `payloads` instead — one typed, tuple-returning
94
+ * generator per payload-carrying input.
84
95
  */
85
- interface WalkConfig<TClient extends object = object> {
96
+ type WalkConfig<TClient extends object = object, TInputs = InputMap> = WalkConfigBase<TClient> & PayloadGeneratorsFor<TInputs>;
97
+ /** Input names in the map whose tuples are non-empty — they carry payloads. */
98
+ type PayloadCarryingInputs<TInputs> = { [K in keyof TInputs & string]: TInputs[K] extends [] ? never : K }[keyof TInputs & string];
99
+ /**
100
+ * The generator half of WalkConfig, selected by the map.
101
+ *
102
+ * Wide map (untyped FSM): the legacy `inputs` shape — string-keyed, one
103
+ * value per call, passed as one argument.
104
+ *
105
+ * Real map with payload-carrying inputs: `payloads` is REQUIRED, with one
106
+ * key per payload-carrying input. Each generator returns that input's full
107
+ * argument tuple and walkAll spreads it. This is what turns a forgotten
108
+ * generator from a mid-walk TypeError into a compile error naming the
109
+ * input. The legacy `inputs` key is absent on this path on purpose — one
110
+ * way to do it.
111
+ *
112
+ * Real map with no payload-carrying inputs: nothing to generate, so the
113
+ * config carries neither key.
114
+ */
115
+ type PayloadGeneratorsFor<TInputs> = string extends keyof TInputs ? {
116
+ /**
117
+ * Payload generators keyed by input name. When walkAll fires input
118
+ * "X" and `inputs["X"]` is defined, it calls that function and
119
+ * passes the return value as the payload to handle(). Inputs
120
+ * without generators are fired with no payload.
121
+ *
122
+ * If the FSM fixes an input payload map (machina #195), walkAll's
123
+ * config switches to the typed `payloads` key instead, and the
124
+ * compiler requires a generator for every payload-carrying input.
125
+ */
126
+ inputs?: Record<string, () => unknown>;
127
+ } : [PayloadCarryingInputs<TInputs>] extends [never] ? unknown : {
128
+ /**
129
+ * Typed payload generators, one per payload-carrying input in
130
+ * the FSM's map. Each returns the input's full argument tuple;
131
+ * walkAll spreads it into handle(). Required — handlers written
132
+ * against a map trust their payloads, so every payload-carrying
133
+ * input a walk can fire must have a generator.
134
+ */
135
+ payloads: { [K in PayloadCarryingInputs<TInputs>]: () => TInputs[K] };
136
+ };
137
+ /**
138
+ * The generator-independent half of WalkConfig. Exported only through
139
+ * `WalkConfig` — consumers never name this directly.
140
+ */
141
+ interface WalkConfigBase<TClient extends object = object> {
86
142
  /**
87
143
  * How many independent walks to run. Each walk creates a fresh FSM
88
144
  * via the factory and runs up to `maxSteps` random inputs.
@@ -110,13 +166,6 @@ interface WalkConfig<TClient extends object = object> {
110
166
  * These inputs are removed from the auto-extracted set before walking.
111
167
  */
112
168
  exclude?: string[];
113
- /**
114
- * Payload generators keyed by input name. When walkAll fires input "X"
115
- * and `inputs["X"]` is defined, it calls that function and passes the
116
- * return value as the payload to handle(). Inputs without generators
117
- * are fired with no payload.
118
- */
119
- inputs?: Record<string, () => unknown>;
120
169
  /**
121
170
  * BehavioralFsm client factory. Required when the FSM produced by the
122
171
  * factory is a BehavioralFsm — each walk calls this to get a fresh client.
@@ -175,7 +224,7 @@ declare class WalkFailureError extends Error {
175
224
  * @param config - Walk configuration: invariant, walk count, step limit, seed,
176
225
  * input filters, payload generators, and optional client factory.
177
226
  */
178
- declare const walkAll: <TClient extends object = object>(factory: () => any, config: WalkConfig<TClient>) => WalkResult;
227
+ declare const walkAll: <TFsm = unknown, TClient extends object = object>(factory: () => TFsm, config: WalkConfig<TClient, InputMapOfInstance<TFsm>>) => WalkResult;
179
228
  //#endregion
180
229
  export { type InvariantArgs, type ReachabilityOptions, type WalkConfig, WalkFailureError, type WalkResult, walkAll };
181
230
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/walk.ts"],"mappings":";UAWiB,mBAAA;EAAA;EAEb,IAAA;AAAA;AAAA,QAII,MAAA;EAAA,UAEM,IAAA;IAAA,UACI,QAAA;MANjB;;;;;MAYW,yBAAA,IAA6B,CAAA;MAYoC;;;;MANjE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;MAZ5D;;;;MAkBN,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAA;EAAA;AAAA;AAAA;EAAA,UAQ/D,SAAA;IARW;;;;;IAcjB,yBAAA,IAA6B,CAAA;IAdyC;AAAA;;;IAoBtE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAtB;;;;IAM5C,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;EAAA;AAAA;;;;AArDzE;;;UCkBiB,aAAA;EDhBT;ECkBJ,KAAA;EDjBH;ECmBG,aAAA;EDPqC;ECSrC,cAAA;EDH0E;ECK1E,GAAA;EDCyE;ECCzE,KAAA;EDD0E;ECG1E,OAAA;EDtBU;ECwBV,IAAA;AAAA;;;;UAMa,UAAA;EDjBuC;ECmBpD,IAAA;EDnB0E;ECqB1E,cAAA;AAAA;;;;;;;UASa,UAAA;EDxB6D;;;;;EC8B1E,KAAA;EDJqE;;;;ECUrE,QAAA;EDtBI;;;;;EC6BJ,IAAA;EDvBsE;;;;EC6BtE,OAAA;EDvBqE;;;;EC6BrE,OAAA;;;AAhEJ;;;;EAwEI,MAAA,GAAS,MAAA;EApET;;;;;EA2EA,MAAA,SAAe,OAAA;EAjEX;;AAMR;;;EAkEI,SAAA,GAAY,IAAA,EAAM,aAAA;AAAA;AArDtB;AAAA,UAyDU,WAAA;EACN,KAAA;EACA,OAAA;AAAA;;;;;cAOS,gBAAA,SAAyB,KAAA;EAtDlC;EAAA,SAwDS,IAAA;EA3CT;EAAA,SA6CS,IAAA;EA/BT;EAAA,SAiCS,aAAA,EAAe,WAAA;EA1BxB;EAAA,SA4BS,KAAA;EArBT;EAAA,SAuBS,aAAA;EAvBG;EAAA,SAyBH,cAAA;EAzBsB;EAAA,SA2BtB,GAAA;cAEG,IAAA;IACR,IAAA;IACA,IAAA;IACA,aAAA,EAAe,WAAA;IACf,KAAA;IACA,aAAA;IACA,cAAA;IACA,GAAA;IACA,KAAA;EAAA;AAAA;;;;;AAoJR;;;;;;cAAa,OAAA,oCACT,OAAA,aACA,MAAA,EAAQ,UAAA,CAAW,OAAA,MACpB,UAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/walk.ts"],"mappings":";;;UAWiB,mBAAA;;EAEb,IAAA;AAAA;AAAA,QAII,MAAA;EAAA,UAEM,IAAA;IAAA,UACI,QAAA;MAPV;AACP;;;;MAYW,yBAAA,IAA6B,CAAA;MAMqC;;;;MAAlE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;MAftE;;;;MAqBI,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAA;EAAA;AAAA;AAAA;EAAA,UAQ/D,SAAA;IAdgE;;;;;IAoBtE,yBAAA,IAA6B,CAAA;IAdyC;;;AAAA;IAoBtE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IApBI;;;;IA0BtE,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;EAAA;AAAA;;;;;AArDzE;;UCkBiB,aAAA;EDhBb;ECkBA,KAAA;EDjBH;ECmBG,aAAA;EDnBH;ECqBG,cAAA;EDHoD;ECKpD,GAAA;EDCmD;ECCnD,KAAA;EDD0E;;;;;ECO1E,OAAA;EDnBQ;ECqBR,IAAA;AAAA;;;;UAMa,UAAA;EDfL;ECiBR,IAAA;EDjBmD;ECmBnD,cAAA;AAAA;;;;ADnB0E;;;;;;;;KCiClE,UAAA,4CAEE,QAAA,IACV,cAAA,CAAe,OAAA,IAAW,oBAAA,CAAqB,OAAA;;KAG9C,qBAAA,0BACW,OAAA,YAAmB,OAAA,CAAQ,CAAA,uBAAwB,CAAA,SAC3D,OAAA;;;;;;;;;;;;;;;;;KAkBH,oBAAA,iCAAqD,OAAA;EApEzC;;;;;;;;;;EAgFP,MAAA,GAAS,MAAA;AAAA,KAEZ,qBAAA,CAAsB,OAAA;EAhErB;AAMR;;;;;AAkBA;EAkDY,QAAA,UACU,qBAAA,CAAsB,OAAA,UAAiB,OAAA,CAAQ,CAAA;AAAA;;;;;UAQ3D,cAAA;EA1DN;;;;;EAgEA,KAAA;EA9D+C;;;AAAS;EAoExD,QAAA;EAjEsB;;;;;EAwEtB,IAAA;EAtEW;;;;EA4EX,OAAA;EA7E+B;;;;EAmF/B,OAAA;EAlFW;AAAA;;;;EAyFX,MAAA,SAAe,OAAA;EAzDU;;;;;EAgEzB,SAAA,GAAY,IAAA,EAAM,aAAA;AAAA;;UAcZ,WAAA;EACN,KAAA;EACA,OAAA;AAAA;;;;;cAOS,gBAAA,SAAyB,KAAA;EA5EM;EAAA,SA8E/B,IAAA;EA9EwD;EAAA,SAgFxD,IAAA;EAhFyD;EAAA,SAkFzD,aAAA,EAAe,WAAA;EA1EJ;EAAA,SA4EX,KAAA;EA/BsB;EAAA,SAiCtB,aAAA;EAxET;EAAA,SA0ES,cAAA;EA7DT;EAAA,SA+DS,GAAA;cAEG,IAAA;IACR,IAAA;IACA,IAAA;IACA,aAAA,EAAe,WAAA;IACf,KAAA;IACA,aAAA;IACA,cAAA;IACA,GAAA;IACA,KAAA;EAAA;AAAA;;;;;;;;;;;cAoJK,OAAA,oDACT,OAAA,QAAe,IAAA,EACf,MAAA,EAAQ,UAAA,CAAW,OAAA,EAAS,kBAAA,CAAmB,IAAA,OAChD,UAAA"}
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import { InputMap, InputMapOfInstance } from "machina";
2
+
1
3
  //#region src/types.d.ts
2
4
  interface ReachabilityOptions {
3
5
  /** The state to start the BFS from */
@@ -62,7 +64,11 @@ interface InvariantArgs {
62
64
  ctx: unknown;
63
65
  /** The input name that triggered this transition */
64
66
  input: string;
65
- /** The payload passed to handle(), or undefined if no generator was configured */
67
+ /**
68
+ * What the generator produced for this step, or undefined if none was
69
+ * configured. Legacy `inputs` generators: the single value passed to
70
+ * handle(). Typed `payloads` generators: the full argument tuple.
71
+ */
66
72
  payload: unknown;
67
73
  /** Which handle() call produced this transition (1-indexed) */
68
74
  step: number;
@@ -81,8 +87,58 @@ interface WalkResult {
81
87
  *
82
88
  * @typeParam TClient - The client object type for BehavioralFsm walks.
83
89
  * Not relevant for Fsm walks — omit `client` in that case.
90
+ * @typeParam TInputs - The FSM's input payload map (#195), extracted from
91
+ * the factory's return type. Under the wide default (untyped FSMs), the
92
+ * config carries the legacy `inputs` generators. Under a real map, the
93
+ * config requires `payloads` instead — one typed, tuple-returning
94
+ * generator per payload-carrying input.
84
95
  */
85
- interface WalkConfig<TClient extends object = object> {
96
+ type WalkConfig<TClient extends object = object, TInputs = InputMap> = WalkConfigBase<TClient> & PayloadGeneratorsFor<TInputs>;
97
+ /** Input names in the map whose tuples are non-empty — they carry payloads. */
98
+ type PayloadCarryingInputs<TInputs> = { [K in keyof TInputs & string]: TInputs[K] extends [] ? never : K }[keyof TInputs & string];
99
+ /**
100
+ * The generator half of WalkConfig, selected by the map.
101
+ *
102
+ * Wide map (untyped FSM): the legacy `inputs` shape — string-keyed, one
103
+ * value per call, passed as one argument.
104
+ *
105
+ * Real map with payload-carrying inputs: `payloads` is REQUIRED, with one
106
+ * key per payload-carrying input. Each generator returns that input's full
107
+ * argument tuple and walkAll spreads it. This is what turns a forgotten
108
+ * generator from a mid-walk TypeError into a compile error naming the
109
+ * input. The legacy `inputs` key is absent on this path on purpose — one
110
+ * way to do it.
111
+ *
112
+ * Real map with no payload-carrying inputs: nothing to generate, so the
113
+ * config carries neither key.
114
+ */
115
+ type PayloadGeneratorsFor<TInputs> = string extends keyof TInputs ? {
116
+ /**
117
+ * Payload generators keyed by input name. When walkAll fires input
118
+ * "X" and `inputs["X"]` is defined, it calls that function and
119
+ * passes the return value as the payload to handle(). Inputs
120
+ * without generators are fired with no payload.
121
+ *
122
+ * If the FSM fixes an input payload map (machina #195), walkAll's
123
+ * config switches to the typed `payloads` key instead, and the
124
+ * compiler requires a generator for every payload-carrying input.
125
+ */
126
+ inputs?: Record<string, () => unknown>;
127
+ } : [PayloadCarryingInputs<TInputs>] extends [never] ? unknown : {
128
+ /**
129
+ * Typed payload generators, one per payload-carrying input in
130
+ * the FSM's map. Each returns the input's full argument tuple;
131
+ * walkAll spreads it into handle(). Required — handlers written
132
+ * against a map trust their payloads, so every payload-carrying
133
+ * input a walk can fire must have a generator.
134
+ */
135
+ payloads: { [K in PayloadCarryingInputs<TInputs>]: () => TInputs[K] };
136
+ };
137
+ /**
138
+ * The generator-independent half of WalkConfig. Exported only through
139
+ * `WalkConfig` — consumers never name this directly.
140
+ */
141
+ interface WalkConfigBase<TClient extends object = object> {
86
142
  /**
87
143
  * How many independent walks to run. Each walk creates a fresh FSM
88
144
  * via the factory and runs up to `maxSteps` random inputs.
@@ -110,13 +166,6 @@ interface WalkConfig<TClient extends object = object> {
110
166
  * These inputs are removed from the auto-extracted set before walking.
111
167
  */
112
168
  exclude?: string[];
113
- /**
114
- * Payload generators keyed by input name. When walkAll fires input "X"
115
- * and `inputs["X"]` is defined, it calls that function and passes the
116
- * return value as the payload to handle(). Inputs without generators
117
- * are fired with no payload.
118
- */
119
- inputs?: Record<string, () => unknown>;
120
169
  /**
121
170
  * BehavioralFsm client factory. Required when the FSM produced by the
122
171
  * factory is a BehavioralFsm — each walk calls this to get a fresh client.
@@ -175,7 +224,7 @@ declare class WalkFailureError extends Error {
175
224
  * @param config - Walk configuration: invariant, walk count, step limit, seed,
176
225
  * input filters, payload generators, and optional client factory.
177
226
  */
178
- declare const walkAll: <TClient extends object = object>(factory: () => any, config: WalkConfig<TClient>) => WalkResult;
227
+ declare const walkAll: <TFsm = unknown, TClient extends object = object>(factory: () => TFsm, config: WalkConfig<TClient, InputMapOfInstance<TFsm>>) => WalkResult;
179
228
  //#endregion
180
229
  export { type InvariantArgs, type ReachabilityOptions, type WalkConfig, WalkFailureError, type WalkResult, walkAll };
181
230
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/walk.ts"],"mappings":";UAWiB,mBAAA;EAAA;EAEb,IAAA;AAAA;AAAA,QAII,MAAA;EAAA,UAEM,IAAA;IAAA,UACI,QAAA;MANjB;;;;;MAYW,yBAAA,IAA6B,CAAA;MAYoC;;;;MANjE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;MAZ5D;;;;MAkBN,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAA;EAAA;AAAA;AAAA;EAAA,UAQ/D,SAAA;IARW;;;;;IAcjB,yBAAA,IAA6B,CAAA;IAdyC;AAAA;;;IAoBtE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAtB;;;;IAM5C,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;EAAA;AAAA;;;;AArDzE;;;UCkBiB,aAAA;EDhBT;ECkBJ,KAAA;EDjBH;ECmBG,aAAA;EDPqC;ECSrC,cAAA;EDH0E;ECK1E,GAAA;EDCyE;ECCzE,KAAA;EDD0E;ECG1E,OAAA;EDtBU;ECwBV,IAAA;AAAA;;;;UAMa,UAAA;EDjBuC;ECmBpD,IAAA;EDnB0E;ECqB1E,cAAA;AAAA;;;;;;;UASa,UAAA;EDxB6D;;;;;EC8B1E,KAAA;EDJqE;;;;ECUrE,QAAA;EDtBI;;;;;EC6BJ,IAAA;EDvBsE;;;;EC6BtE,OAAA;EDvBqE;;;;EC6BrE,OAAA;;;AAhEJ;;;;EAwEI,MAAA,GAAS,MAAA;EApET;;;;;EA2EA,MAAA,SAAe,OAAA;EAjEX;;AAMR;;;EAkEI,SAAA,GAAY,IAAA,EAAM,aAAA;AAAA;AArDtB;AAAA,UAyDU,WAAA;EACN,KAAA;EACA,OAAA;AAAA;;;;;cAOS,gBAAA,SAAyB,KAAA;EAtDlC;EAAA,SAwDS,IAAA;EA3CT;EAAA,SA6CS,IAAA;EA/BT;EAAA,SAiCS,aAAA,EAAe,WAAA;EA1BxB;EAAA,SA4BS,KAAA;EArBT;EAAA,SAuBS,aAAA;EAvBG;EAAA,SAyBH,cAAA;EAzBsB;EAAA,SA2BtB,GAAA;cAEG,IAAA;IACR,IAAA;IACA,IAAA;IACA,aAAA,EAAe,WAAA;IACf,KAAA;IACA,aAAA;IACA,cAAA;IACA,GAAA;IACA,KAAA;EAAA;AAAA;;;;;AAoJR;;;;;;cAAa,OAAA,oCACT,OAAA,aACA,MAAA,EAAQ,UAAA,CAAW,OAAA,MACpB,UAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/walk.ts"],"mappings":";;;UAWiB,mBAAA;;EAEb,IAAA;AAAA;AAAA,QAII,MAAA;EAAA,UAEM,IAAA;IAAA,UACI,QAAA;MAPV;AACP;;;;MAYW,yBAAA,IAA6B,CAAA;MAMqC;;;;MAAlE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;MAftE;;;;MAqBI,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IAAA;EAAA;AAAA;AAAA;EAAA,UAQ/D,SAAA;IAdgE;;;;;IAoBtE,yBAAA,IAA6B,CAAA;IAdyC;;;AAAA;IAoBtE,aAAA,CAAc,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;IApBI;;;;IA0BtE,YAAA,CAAa,WAAA,UAAqB,OAAA,EAAS,mBAAA,GAAsB,CAAA;EAAA;AAAA;;;;;AArDzE;;UCkBiB,aAAA;EDhBb;ECkBA,KAAA;EDjBH;ECmBG,aAAA;EDnBH;ECqBG,cAAA;EDHoD;ECKpD,GAAA;EDCmD;ECCnD,KAAA;EDD0E;;;;;ECO1E,OAAA;EDnBQ;ECqBR,IAAA;AAAA;;;;UAMa,UAAA;EDfL;ECiBR,IAAA;EDjBmD;ECmBnD,cAAA;AAAA;;;;ADnB0E;;;;;;;;KCiClE,UAAA,4CAEE,QAAA,IACV,cAAA,CAAe,OAAA,IAAW,oBAAA,CAAqB,OAAA;;KAG9C,qBAAA,0BACW,OAAA,YAAmB,OAAA,CAAQ,CAAA,uBAAwB,CAAA,SAC3D,OAAA;;;;;;;;;;;;;;;;;KAkBH,oBAAA,iCAAqD,OAAA;EApEzC;;;;;;;;;;EAgFP,MAAA,GAAS,MAAA;AAAA,KAEZ,qBAAA,CAAsB,OAAA;EAhErB;AAMR;;;;;AAkBA;EAkDY,QAAA,UACU,qBAAA,CAAsB,OAAA,UAAiB,OAAA,CAAQ,CAAA;AAAA;;;;;UAQ3D,cAAA;EA1DN;;;;;EAgEA,KAAA;EA9D+C;;;AAAS;EAoExD,QAAA;EAjEsB;;;;;EAwEtB,IAAA;EAtEW;;;;EA4EX,OAAA;EA7E+B;;;;EAmF/B,OAAA;EAlFW;AAAA;;;;EAyFX,MAAA,SAAe,OAAA;EAzDU;;;;;EAgEzB,SAAA,GAAY,IAAA,EAAM,aAAA;AAAA;;UAcZ,WAAA;EACN,KAAA;EACA,OAAA;AAAA;;;;;cAOS,gBAAA,SAAyB,KAAA;EA5EM;EAAA,SA8E/B,IAAA;EA9EwD;EAAA,SAgFxD,IAAA;EAhFyD;EAAA,SAkFzD,aAAA,EAAe,WAAA;EA1EJ;EAAA,SA4EX,KAAA;EA/BsB;EAAA,SAiCtB,aAAA;EAxET;EAAA,SA0ES,cAAA;EA7DT;EAAA,SA+DS,GAAA;cAEG,IAAA;IACR,IAAA;IACA,IAAA;IACA,aAAA,EAAe,WAAA;IACf,KAAA;IACA,aAAA;IACA,cAAA;IACA,GAAA;IACA,KAAA;EAAA;AAAA;;;;;;;;;;;cAoJK,OAAA,oDACT,OAAA,QAAe,IAAA,EACf,MAAA,EAAQ,UAAA,CAAW,OAAA,EAAS,kBAAA,CAAmB,IAAA,OAChD,UAAA"}
package/dist/index.mjs CHANGED
@@ -220,14 +220,15 @@ const validateConfig = (config, availableInputs) => {
220
220
  * input filters, payload generators, and optional client factory.
221
221
  */
222
222
  const walkAll = (factory, config) => {
223
- const walks = config.walks ?? 100;
224
- const maxSteps = config.maxSteps ?? 50;
223
+ const cfg = config;
224
+ const walks = cfg.walks ?? 100;
225
+ const maxSteps = cfg.maxSteps ?? 50;
225
226
  const probeFsm = factory();
226
- const inputs = validateConfig(config, extractInputs(probeFsm));
227
+ const inputs = validateConfig(cfg, extractInputs(probeFsm));
227
228
  if (typeof probeFsm.dispose === "function") probeFsm.dispose();
228
- const seed = config.seed ?? Math.floor(Math.random() * 2 ** 32);
229
+ const seed = cfg.seed ?? Math.floor(Math.random() * 2 ** 32);
229
230
  const prng = createPrng(seed);
230
- for (let walkIndex = 0; walkIndex < walks; walkIndex++) runSingleWalk(factory, config, inputs, prng, seed, maxSteps);
231
+ for (let walkIndex = 0; walkIndex < walks; walkIndex++) runSingleWalk(factory, cfg, inputs, prng, seed, maxSteps);
231
232
  return {
232
233
  seed,
233
234
  walksCompleted: walks
@@ -301,12 +302,22 @@ const runSingleWalk = (factory, config, inputs, prng, seed, maxSteps) => {
301
302
  for (let step = 1; step <= maxSteps; step++) {
302
303
  currentStep = step;
303
304
  const inputName = inputs[randomInt(prng, inputs.length)];
304
- const payload = config.inputs?.[inputName]?.();
305
+ let extraArgs;
306
+ let payload;
307
+ const tupleGen = config.payloads?.[inputName];
308
+ if (tupleGen) {
309
+ const tuple = tupleGen();
310
+ if (!Array.isArray(tuple)) throw new Error(`walkAll: payloads.${inputName} must return the input's argument tuple (an array)`);
311
+ extraArgs = tuple;
312
+ payload = tuple;
313
+ } else {
314
+ payload = config.inputs?.[inputName]?.();
315
+ extraArgs = payload !== void 0 ? [payload] : [];
316
+ }
305
317
  inputSequence.push({
306
318
  input: inputName,
307
319
  payload
308
320
  });
309
- const extraArgs = payload !== void 0 ? [payload] : [];
310
321
  if (isBehavioral && client !== void 0) fsm.handle(client, inputName, ...extraArgs);
311
322
  else fsm.handle(inputName, ...extraArgs);
312
323
  if (violation !== null) throw new WalkFailureError({
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/reachability.ts","../src/matchers.ts","../src/walk.ts","../src/index.ts"],"sourcesContent":["// =============================================================================\n// reachability.ts — BFS reachability check for StateGraph\n//\n// Top-level graph only. Child graphs are not traversed here — users who want\n// to assert reachability inside a child FSM pass the child instance directly\n// to expect().\n//\n// Both \"definite\" and \"possible\" edges are followed. The question is whether\n// the plumbing exists, not whether runtime conditions will always cause it to\n// execute. See design decisions in the build plan for rationale.\n// =============================================================================\n\nimport type { StateGraph } from \"machina-inspect\";\n\n/**\n * Returns true if `to` is reachable from `from` via BFS over the\n * top-level graph's edges. Returns false if either state is missing\n * from the graph, or if no path exists.\n *\n * Treats `from === to` as trivially reachable (every state reaches itself).\n */\nconst canReach = (graph: StateGraph, from: string, to: string): boolean => {\n // Trivially reachable — a state always reaches itself\n if (from === to) {\n return true;\n }\n\n // If the source state isn't in the graph, there's nothing to walk\n if (!(from in graph.nodes)) {\n return false;\n }\n\n const visited = new Set<string>();\n const queue: string[] = [from];\n visited.add(from);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = graph.nodes[current];\n\n // Missing node is a safety check — shouldn't happen in a well-formed graph\n if (!node) {\n continue;\n }\n\n for (const edge of node.edges) {\n if (edge.to === to) {\n return true;\n }\n if (!visited.has(edge.to) && edge.to in graph.nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n return false;\n};\n\nexport { canReach };\n","// =============================================================================\n// matchers.ts — Custom matcher implementations for machina FSMs\n//\n// Each matcher follows the expect.extend() contract:\n// - Receives the subject as `received`\n// - Returns { pass: boolean, message: () => string }\n// - `message` is always the FAILURE message — jest/vitest call it only\n// when the assertion fails (accounting for .not inversion)\n//\n// Invalid state names produce matcher failures, not throws, so they appear\n// as clean test failures in the runner rather than unhandled exceptions.\n// =============================================================================\n\nimport { buildStateGraph, inspectGraph, type StateGraph } from \"machina-inspect\";\nimport type { MachinaInstance } from \"machina\";\nimport { canReach } from \"./reachability\";\nimport type { ReachabilityOptions } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the StateGraph for the received FSM instance.\n * `buildStateGraph` accepts both config objects and live instances;\n * here we always receive live instances (users pass `expect(fsm)`).\n */\nconst getGraph = (received: MachinaInstance): StateGraph => {\n return buildStateGraph(received);\n};\n\n/**\n * Produce the \"invalid state\" failure message. Used when a state name\n * passed to a matcher does not exist in the graph.\n */\nconst invalidStateMessage = (stateName: string, fsmId: string, graph: StateGraph): string => {\n const available = Object.keys(graph.nodes).join(\", \");\n return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${available}.`;\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toHaveNoUnreachableStates\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when the FSM has no states that are unreachable from initialState.\n * Uses inspectGraph() to run all checks and filters for unreachable-state\n * findings, which includes recursive child graph analysis.\n */\nconst toHaveNoUnreachableStates = (received: MachinaInstance): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const findings = inspectGraph(graph).filter(f => f.type === \"unreachable-state\");\n\n const pass = findings.length === 0;\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toHaveNoUnreachableStates() but all states reachable\n return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;\n }\n const stateList = findings.map(f => `'${f.states.join(\", \")}'`).join(\", \");\n return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toAlwaysReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when a path exists from `options.from` to `targetState` in the\n * FSM's top-level graph (BFS over all edges, definite and possible).\n */\nconst toAlwaysReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n const pass = canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toAlwaysReach but path exists\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toNeverReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when NO path exists from `options.from` to `targetState`.\n * The logical inverse of toAlwaysReach.\n */\nconst toNeverReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n // pass: true means \"no path exists\" (the assertion that toNeverReach checks)\n const pass = !canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toNeverReach but no path exists\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n };\n\n return { pass, message };\n};\n\nexport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach };\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// walk.ts — Property-based walk testing for machina FSMs\n//\n// `walkAll` runs multiple randomized input sequences against a live FSM,\n// asserting a user-supplied invariant after every transition. Deterministic\n// replay is guaranteed via a seeded PRNG — on failure, the seed and full input\n// sequence are included in the thrown WalkFailureError so you can reproduce\n// the exact walk that uncovered the bug.\n//\n// Design choices:\n// - Factory function (not FSM instance) as first arg: each walk starts fresh\n// - Throws on failure, returns on success: natural test code, no result-type gymnastics\n// - step counts handle() calls, not transitions: maps 1:1 to input sequence index\n// - Inline mulberry32 PRNG: 5 lines, no dependency, deterministic across platforms\n// - Event-driven invariant: handles _onEnter bounces + deferred replays without\n// monkey-patching handle()\n// =============================================================================\n\nimport { MACHINA_TYPE } from \"machina\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Arguments passed to the invariant callback after every transition.\n * Use these to assert that the FSM is in a valid state after each step.\n */\nexport interface InvariantArgs {\n /** State the FSM just transitioned TO */\n state: string;\n /** State the FSM transitioned FROM */\n previousState: string;\n /** Dot-delimited composite state (includes child states if any) */\n compositeState: string;\n /** The FSM context object (Fsm) or client object (BehavioralFsm) */\n ctx: unknown;\n /** The input name that triggered this transition */\n input: string;\n /** The payload passed to handle(), or undefined if no generator was configured */\n payload: unknown;\n /** Which handle() call produced this transition (1-indexed) */\n step: number;\n}\n\n/**\n * Returned by walkAll on success.\n */\nexport interface WalkResult {\n /** The seed used for this run — pass back to walkAll to replay */\n seed: number;\n /** How many walks completed without an invariant violation */\n walksCompleted: number;\n}\n\n/**\n * Configuration for a walkAll run.\n *\n * @typeParam TClient - The client object type for BehavioralFsm walks.\n * Not relevant for Fsm walks — omit `client` in that case.\n */\nexport interface WalkConfig<TClient extends object = object> {\n /**\n * How many independent walks to run. Each walk creates a fresh FSM\n * via the factory and runs up to `maxSteps` random inputs.\n * Must be >= 1. Default: 100.\n */\n walks?: number;\n\n /**\n * Maximum number of handle() calls per walk.\n * Must be >= 1. Default: 50.\n */\n maxSteps?: number;\n\n /**\n * Seed for the PRNG. Providing the same seed guarantees the identical\n * input sequence — use the seed from WalkResult or WalkFailureError to\n * replay a specific walk. Defaults to a random integer.\n */\n seed?: number;\n\n /**\n * Whitelist of input names to use. Mutually exclusive with `exclude`.\n * If provided, only these inputs will be fired during walks.\n */\n include?: string[];\n\n /**\n * Blacklist of input names to exclude. Mutually exclusive with `include`.\n * These inputs are removed from the auto-extracted set before walking.\n */\n exclude?: string[];\n\n /**\n * Payload generators keyed by input name. When walkAll fires input \"X\"\n * and `inputs[\"X\"]` is defined, it calls that function and passes the\n * return value as the payload to handle(). Inputs without generators\n * are fired with no payload.\n */\n inputs?: Record<string, () => unknown>;\n\n /**\n * BehavioralFsm client factory. Required when the FSM produced by the\n * factory is a BehavioralFsm — each walk calls this to get a fresh client.\n * Omit for Fsm walks.\n */\n client?: () => TClient;\n\n /**\n * Called after every transition. Return `false` OR throw to signal a\n * violation — WalkFailureError is constructed automatically in both cases.\n * Return `true` or `undefined` (void) to indicate the invariant passed.\n */\n invariant: (args: InvariantArgs) => boolean | void;\n}\n\n/** A recorded step in the input sequence — used for replay and error reporting */\ninterface InputRecord {\n input: string;\n payload: unknown;\n}\n\n/**\n * Thrown when walkAll detects an invariant violation. Carries the seed and\n * full input sequence so you can replay the exact walk that failed.\n */\nexport class WalkFailureError extends Error {\n /** Seed that produced this walk — pass to walkAll({ seed }) to replay */\n readonly seed: number;\n /** Which step (1-indexed handle() call) caused the violation */\n readonly step: number;\n /** The inputs fired up to and including the failing step */\n readonly inputSequence: InputRecord[];\n /** State at the time of violation */\n readonly state: string;\n /** State before the violating transition */\n readonly previousState: string;\n /** Composite state at the time of violation */\n readonly compositeState: string;\n /** The FSM context or client at the time of violation */\n readonly ctx: unknown;\n\n constructor(args: {\n seed: number;\n step: number;\n inputSequence: InputRecord[];\n state: string;\n previousState: string;\n compositeState: string;\n ctx: unknown;\n cause: unknown;\n }) {\n const causeMessage = args.cause instanceof Error ? args.cause.message : String(args.cause);\n const sequenceSummary = args.inputSequence\n .map((r, i) => ` ${i + 1}. ${r.input}`)\n .join(\"\\n\");\n\n super(\n `walkAll invariant violation at step ${args.step} (seed: ${args.seed})\\n` +\n ` State: ${args.previousState} → ${args.state}\\n` +\n ` Composite: ${args.compositeState}\\n` +\n ` Cause: ${causeMessage}\\n` +\n `Input sequence:\\n${sequenceSummary}`\n );\n this.name = \"WalkFailureError\";\n this.seed = args.seed;\n this.step = args.step;\n this.inputSequence = args.inputSequence;\n this.state = args.state;\n this.previousState = args.previousState;\n this.compositeState = args.compositeState;\n this.ctx = args.ctx;\n }\n}\n\n// -----------------------------------------------------------------------------\n// Seeded PRNG — mulberry32\n//\n// Passes BigCrush and PractRand. 5 lines, no external dependency.\n// Returns floats in [0, 1) just like Math.random(), enabling drop-in\n// deterministic replacement wherever we'd otherwise use Math.random().\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a seeded PRNG using mulberry32.\n * Returns a function that yields deterministic floats in [0, 1).\n * Two PRNGs with the same seed produce identical sequences.\n */\nexport const createPrng = (seed: number): (() => number) => {\n let s = seed;\n return () => {\n s += 0x6d2b79f5;\n let t = s;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n};\n\n/**\n * Pick a random integer in [0, max) using the given PRNG.\n * Used to select a random index from the inputs array each step.\n */\nexport const randomInt = (prng: () => number, max: number): number => {\n return Math.floor(prng() * max);\n};\n\n// -----------------------------------------------------------------------------\n// Keys that are lifecycle hooks or special directives, not named inputs.\n// Mirrors SpecialStateKeys from machina core types.\n// -----------------------------------------------------------------------------\n\nconst RESERVED_KEYS = new Set([\"_onEnter\", \"_onExit\", \"_child\", \"*\"]);\n\n/**\n * Extract all distinct input names from an FSM's states object.\n * Skips reserved keys (_onEnter, _onExit, _child, *) — these are lifecycle\n * hooks and catch-alls, not named inputs you'd fire via handle().\n */\nexport const extractInputs = (fsm: {\n states: Record<string, Record<string, unknown>>;\n}): string[] => {\n const inputs = new Set<string>();\n for (const stateObj of Object.values(fsm.states)) {\n for (const key of Object.keys(stateObj)) {\n if (!RESERVED_KEYS.has(key)) {\n inputs.add(key);\n }\n }\n }\n return Array.from(inputs);\n};\n\n// -----------------------------------------------------------------------------\n// Config validation\n// -----------------------------------------------------------------------------\n\nconst validateConfig = (config: WalkConfig, availableInputs: string[]): string[] => {\n const walks = config.walks ?? 100;\n const maxSteps = config.maxSteps ?? 50;\n\n if (walks < 1) {\n throw new Error(`walkAll: walks must be >= 1, got ${walks}`);\n }\n\n if (maxSteps < 1) {\n throw new Error(`walkAll: maxSteps must be >= 1, got ${maxSteps}`);\n }\n\n if (config.include && config.exclude) {\n throw new Error(\n \"walkAll: include and exclude are mutually exclusive — use one or the other\"\n );\n }\n\n let inputs: string[];\n\n if (config.include) {\n // Catch typos and misconfigurations before walking — an include entry\n // that doesn't exist in the FSM is almost certainly a bug at the call site.\n const knownInputs = new Set(availableInputs);\n const unknown = config.include.filter(name => !knownInputs.has(name));\n if (unknown.length > 0) {\n throw new Error(\n `walkAll: include contains inputs not found in the FSM: ${unknown.join(\", \")}`\n );\n }\n inputs = config.include;\n } else if (config.exclude) {\n const excluded = new Set(config.exclude);\n inputs = availableInputs.filter(k => !excluded.has(k));\n } else {\n inputs = availableInputs;\n }\n\n if (inputs.length === 0) {\n throw new Error(\n \"walkAll: no inputs available after filtering — check include/exclude config\"\n );\n }\n\n return inputs;\n};\n\n// -----------------------------------------------------------------------------\n// Walk engine\n// -----------------------------------------------------------------------------\n\n/**\n * Run property-based walk tests against a live FSM.\n *\n * Throws WalkFailureError on invariant violation. Returns WalkResult on success.\n *\n * @param factory - Called once per walk to create a fresh FSM instance.\n * Each walk gets its own FSM so context mutations don't bleed between walks.\n * @param config - Walk configuration: invariant, walk count, step limit, seed,\n * input filters, payload generators, and optional client factory.\n */\nexport const walkAll = <TClient extends object = object>(\n factory: () => any,\n config: WalkConfig<TClient>\n): WalkResult => {\n const walks = config.walks ?? 100;\n const maxSteps = config.maxSteps ?? 50;\n\n // Build the input list from the first FSM instance — we only need this\n // for validation, not for actual walking (each walk creates its own FSM).\n const probeFsm = factory();\n const allInputs = extractInputs(probeFsm);\n const inputs = validateConfig(config, allInputs);\n\n // Dispose the probe FSM if possible — it was only needed for input extraction.\n if (typeof probeFsm.dispose === \"function\") {\n probeFsm.dispose();\n }\n\n const seed = config.seed ?? Math.floor(Math.random() * 2 ** 32);\n const prng = createPrng(seed);\n\n for (let walkIndex = 0; walkIndex < walks; walkIndex++) {\n runSingleWalk(factory, config, inputs, prng, seed, maxSteps);\n }\n\n return { seed, walksCompleted: walks };\n};\n\n/**\n * Run one walk: create a fresh FSM, subscribe to events, fire random inputs,\n * check invariant after every transition. Throws WalkFailureError on violation.\n */\nconst runSingleWalk = <TClient extends object>(\n factory: () => any,\n config: WalkConfig<TClient>,\n inputs: string[],\n prng: () => number,\n seed: number,\n maxSteps: number\n): void => {\n const fsm = factory();\n const isBehavioral = fsm[MACHINA_TYPE] === \"BehavioralFsm\";\n\n // For BehavioralFsm: create a client and track it separately.\n // For Fsm: the client IS the fsm (context lives on the fsm).\n const client: TClient | undefined = isBehavioral\n ? config.client\n ? config.client()\n : ({} as TClient)\n : undefined;\n\n // Initialize BehavioralFsm client by transitioning into initialState.\n // Fsm eagerly initializes in its constructor — no action needed.\n if (isBehavioral && client !== undefined) {\n fsm.transition(client, fsm.initialState);\n }\n\n const inputSequence: InputRecord[] = [];\n let currentStep = 0;\n let currentInput = \"\";\n let violation: unknown = null;\n let violationArgs: InvariantArgs | null = null;\n\n // Track \"current input\" via the handling event, which fires before dispatch.\n // This is the input that caused any subsequent transitioned events — including\n // those from _onEnter bounces, where handling does NOT re-fire.\n // Both Fsm and BehavioralFsm expose inputName on this event, so no branching needed.\n const handlingSub = fsm.on(\"handling\", (data: any) => {\n currentInput = data.inputName;\n });\n\n // The transitioned event fires after every state change, including _onEnter\n // bounces. We run the invariant here so we catch all transition types.\n const transitionedSub = isBehavioral\n ? fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n const ctx = data.client ?? client;\n const composite = fsm.compositeState(data.client ?? client);\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n })\n : fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n // For Fsm, read context from fsm.context (Task 1 made this public)\n const ctx = fsm.context;\n const composite = fsm.compositeState();\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n });\n\n try {\n for (let step = 1; step <= maxSteps; step++) {\n currentStep = step;\n const inputName = inputs[randomInt(prng, inputs.length)];\n const payload = config.inputs?.[inputName]?.();\n inputSequence.push({ input: inputName, payload });\n\n // Only spread the payload arg when a generator produced one — handlers\n // with no generator shouldn't receive a spurious undefined argument,\n // which could mask bugs in handlers that check arguments.length.\n const extraArgs = payload !== undefined ? [payload] : [];\n if (isBehavioral && client !== undefined) {\n fsm.handle(client, inputName, ...extraArgs);\n } else {\n fsm.handle(inputName, ...extraArgs);\n }\n\n if (violation !== null) {\n throw new WalkFailureError({\n seed,\n step,\n inputSequence: [...inputSequence],\n state: violationArgs!.state,\n previousState: violationArgs!.previousState,\n compositeState: violationArgs!.compositeState,\n ctx: violationArgs!.ctx,\n cause: violation,\n });\n }\n }\n } finally {\n // Always unsubscribe, even on failure — prevents listener leaks between walks\n handlingSub.off();\n transitionedSub.off();\n\n if (typeof fsm.dispose === \"function\") {\n fsm.dispose();\n }\n }\n};\n","// =============================================================================\n// index.ts — Side-effect entry point for machina-test\n//\n// Importing this module registers all three custom matchers with the global\n// `expect` provided by Jest or Vitest. This is the same pattern used by\n// @testing-library/jest-dom — the import IS the side effect.\n//\n// `expect` must be available as a global at the time this module is evaluated.\n// Put the import in a test file or in a setup file that runs after the test\n// framework's globals are injected.\n// =============================================================================\n\nimport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach } from \"./matchers\";\n\n// Register with the host test runner's global expect\nexpect.extend({ toHaveNoUnreachableStates, toAlwaysReach, toNeverReach });\n\n// Re-export the options type so consumers can import it alongside the package\nexport type { ReachabilityOptions } from \"./types\";\n\n// walkAll is a standalone function — no side-effect registration needed.\n// Import it directly: import { walkAll } from \"machina-test\"\nexport { walkAll } from \"./walk\";\nexport type { WalkConfig, InvariantArgs, WalkResult } from \"./walk\";\nexport { WalkFailureError } from \"./walk\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/reachability.ts","../src/matchers.ts","../src/walk.ts","../src/index.ts"],"sourcesContent":["// =============================================================================\n// reachability.ts — BFS reachability check for StateGraph\n//\n// Top-level graph only. Child graphs are not traversed here — users who want\n// to assert reachability inside a child FSM pass the child instance directly\n// to expect().\n//\n// Both \"definite\" and \"possible\" edges are followed. The question is whether\n// the plumbing exists, not whether runtime conditions will always cause it to\n// execute. See design decisions in the build plan for rationale.\n// =============================================================================\n\nimport type { StateGraph } from \"machina-inspect\";\n\n/**\n * Returns true if `to` is reachable from `from` via BFS over the\n * top-level graph's edges. Returns false if either state is missing\n * from the graph, or if no path exists.\n *\n * Treats `from === to` as trivially reachable (every state reaches itself).\n */\nconst canReach = (graph: StateGraph, from: string, to: string): boolean => {\n // Trivially reachable — a state always reaches itself\n if (from === to) {\n return true;\n }\n\n // If the source state isn't in the graph, there's nothing to walk\n if (!(from in graph.nodes)) {\n return false;\n }\n\n const visited = new Set<string>();\n const queue: string[] = [from];\n visited.add(from);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = graph.nodes[current];\n\n // Missing node is a safety check — shouldn't happen in a well-formed graph\n if (!node) {\n continue;\n }\n\n for (const edge of node.edges) {\n if (edge.to === to) {\n return true;\n }\n if (!visited.has(edge.to) && edge.to in graph.nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n return false;\n};\n\nexport { canReach };\n","// =============================================================================\n// matchers.ts — Custom matcher implementations for machina FSMs\n//\n// Each matcher follows the expect.extend() contract:\n// - Receives the subject as `received`\n// - Returns { pass: boolean, message: () => string }\n// - `message` is always the FAILURE message — jest/vitest call it only\n// when the assertion fails (accounting for .not inversion)\n//\n// Invalid state names produce matcher failures, not throws, so they appear\n// as clean test failures in the runner rather than unhandled exceptions.\n// =============================================================================\n\nimport { buildStateGraph, inspectGraph, type StateGraph } from \"machina-inspect\";\nimport type { MachinaInstance } from \"machina\";\nimport { canReach } from \"./reachability\";\nimport type { ReachabilityOptions } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the StateGraph for the received FSM instance.\n * `buildStateGraph` accepts both config objects and live instances;\n * here we always receive live instances (users pass `expect(fsm)`).\n */\nconst getGraph = (received: MachinaInstance): StateGraph => {\n return buildStateGraph(received);\n};\n\n/**\n * Produce the \"invalid state\" failure message. Used when a state name\n * passed to a matcher does not exist in the graph.\n */\nconst invalidStateMessage = (stateName: string, fsmId: string, graph: StateGraph): string => {\n const available = Object.keys(graph.nodes).join(\", \");\n return `State '${stateName}' does not exist in FSM '${fsmId}'. Available states: ${available}.`;\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toHaveNoUnreachableStates\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when the FSM has no states that are unreachable from initialState.\n * Uses inspectGraph() to run all checks and filters for unreachable-state\n * findings, which includes recursive child graph analysis.\n */\nconst toHaveNoUnreachableStates = (received: MachinaInstance): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const findings = inspectGraph(graph).filter(f => f.type === \"unreachable-state\");\n\n const pass = findings.length === 0;\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toHaveNoUnreachableStates() but all states reachable\n return `Expected FSM '${graph.fsmId}' to have unreachable states, but all states are reachable.`;\n }\n const stateList = findings.map(f => `'${f.states.join(\", \")}'`).join(\", \");\n return `Expected FSM '${graph.fsmId}' to have no unreachable states, but found: ${stateList}.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toAlwaysReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when a path exists from `options.from` to `targetState` in the\n * FSM's top-level graph (BFS over all edges, definite and possible).\n */\nconst toAlwaysReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n const pass = canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toAlwaysReach but path exists\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n };\n\n return { pass, message };\n};\n\n// ---------------------------------------------------------------------------\n// Matcher: toNeverReach\n// ---------------------------------------------------------------------------\n\n/**\n * Passes when NO path exists from `options.from` to `targetState`.\n * The logical inverse of toAlwaysReach.\n */\nconst toNeverReach = (\n received: MachinaInstance,\n targetState: string,\n options: ReachabilityOptions\n): jest.CustomMatcherResult => {\n const graph = getGraph(received);\n const { from } = options;\n\n // Invalid state names are reported as matcher failures for clean output\n if (!(from in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(from, graph.fsmId, graph),\n };\n }\n\n if (!(targetState in graph.nodes)) {\n return {\n pass: false,\n message: () => invalidStateMessage(targetState, graph.fsmId, graph),\n };\n }\n\n // pass: true means \"no path exists\" (the assertion that toNeverReach checks)\n const pass = !canReach(graph, from, targetState);\n\n const message = (): string => {\n if (pass) {\n // Negated assertion failed: .not.toNeverReach but no path exists\n return `Expected FSM '${graph.fsmId}' to have a path from '${from}' to '${targetState}', but no path exists.`;\n }\n return `Expected FSM '${graph.fsmId}' to have no path from '${from}' to '${targetState}', but a path exists.`;\n };\n\n return { pass, message };\n};\n\nexport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach };\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// =============================================================================\n// walk.ts — Property-based walk testing for machina FSMs\n//\n// `walkAll` runs multiple randomized input sequences against a live FSM,\n// asserting a user-supplied invariant after every transition. Deterministic\n// replay is guaranteed via a seeded PRNG — on failure, the seed and full input\n// sequence are included in the thrown WalkFailureError so you can reproduce\n// the exact walk that uncovered the bug.\n//\n// Design choices:\n// - Factory function (not FSM instance) as first arg: each walk starts fresh\n// - Throws on failure, returns on success: natural test code, no result-type gymnastics\n// - step counts handle() calls, not transitions: maps 1:1 to input sequence index\n// - Inline mulberry32 PRNG: 5 lines, no dependency, deterministic across platforms\n// - Event-driven invariant: handles _onEnter bounces + deferred replays without\n// monkey-patching handle()\n// =============================================================================\n\nimport { MACHINA_TYPE, type InputMap, type InputMapOfInstance } from \"machina\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Arguments passed to the invariant callback after every transition.\n * Use these to assert that the FSM is in a valid state after each step.\n */\nexport interface InvariantArgs {\n /** State the FSM just transitioned TO */\n state: string;\n /** State the FSM transitioned FROM */\n previousState: string;\n /** Dot-delimited composite state (includes child states if any) */\n compositeState: string;\n /** The FSM context object (Fsm) or client object (BehavioralFsm) */\n ctx: unknown;\n /** The input name that triggered this transition */\n input: string;\n /**\n * What the generator produced for this step, or undefined if none was\n * configured. Legacy `inputs` generators: the single value passed to\n * handle(). Typed `payloads` generators: the full argument tuple.\n */\n payload: unknown;\n /** Which handle() call produced this transition (1-indexed) */\n step: number;\n}\n\n/**\n * Returned by walkAll on success.\n */\nexport interface WalkResult {\n /** The seed used for this run — pass back to walkAll to replay */\n seed: number;\n /** How many walks completed without an invariant violation */\n walksCompleted: number;\n}\n\n/**\n * Configuration for a walkAll run.\n *\n * @typeParam TClient - The client object type for BehavioralFsm walks.\n * Not relevant for Fsm walks — omit `client` in that case.\n * @typeParam TInputs - The FSM's input payload map (#195), extracted from\n * the factory's return type. Under the wide default (untyped FSMs), the\n * config carries the legacy `inputs` generators. Under a real map, the\n * config requires `payloads` instead — one typed, tuple-returning\n * generator per payload-carrying input.\n */\nexport type WalkConfig<\n TClient extends object = object,\n TInputs = InputMap,\n> = WalkConfigBase<TClient> & PayloadGeneratorsFor<TInputs>;\n\n/** Input names in the map whose tuples are non-empty — they carry payloads. */\ntype PayloadCarryingInputs<TInputs> = {\n [K in keyof TInputs & string]: TInputs[K] extends [] ? never : K;\n}[keyof TInputs & string];\n\n/**\n * The generator half of WalkConfig, selected by the map.\n *\n * Wide map (untyped FSM): the legacy `inputs` shape — string-keyed, one\n * value per call, passed as one argument.\n *\n * Real map with payload-carrying inputs: `payloads` is REQUIRED, with one\n * key per payload-carrying input. Each generator returns that input's full\n * argument tuple and walkAll spreads it. This is what turns a forgotten\n * generator from a mid-walk TypeError into a compile error naming the\n * input. The legacy `inputs` key is absent on this path on purpose — one\n * way to do it.\n *\n * Real map with no payload-carrying inputs: nothing to generate, so the\n * config carries neither key.\n */\ntype PayloadGeneratorsFor<TInputs> = string extends keyof TInputs\n ? {\n /**\n * Payload generators keyed by input name. When walkAll fires input\n * \"X\" and `inputs[\"X\"]` is defined, it calls that function and\n * passes the return value as the payload to handle(). Inputs\n * without generators are fired with no payload.\n *\n * If the FSM fixes an input payload map (machina #195), walkAll's\n * config switches to the typed `payloads` key instead, and the\n * compiler requires a generator for every payload-carrying input.\n */\n inputs?: Record<string, () => unknown>;\n }\n : [PayloadCarryingInputs<TInputs>] extends [never]\n ? unknown\n : {\n /**\n * Typed payload generators, one per payload-carrying input in\n * the FSM's map. Each returns the input's full argument tuple;\n * walkAll spreads it into handle(). Required — handlers written\n * against a map trust their payloads, so every payload-carrying\n * input a walk can fire must have a generator.\n */\n payloads: {\n [K in PayloadCarryingInputs<TInputs>]: () => TInputs[K];\n };\n };\n\n/**\n * The generator-independent half of WalkConfig. Exported only through\n * `WalkConfig` — consumers never name this directly.\n */\ninterface WalkConfigBase<TClient extends object = object> {\n /**\n * How many independent walks to run. Each walk creates a fresh FSM\n * via the factory and runs up to `maxSteps` random inputs.\n * Must be >= 1. Default: 100.\n */\n walks?: number;\n\n /**\n * Maximum number of handle() calls per walk.\n * Must be >= 1. Default: 50.\n */\n maxSteps?: number;\n\n /**\n * Seed for the PRNG. Providing the same seed guarantees the identical\n * input sequence — use the seed from WalkResult or WalkFailureError to\n * replay a specific walk. Defaults to a random integer.\n */\n seed?: number;\n\n /**\n * Whitelist of input names to use. Mutually exclusive with `exclude`.\n * If provided, only these inputs will be fired during walks.\n */\n include?: string[];\n\n /**\n * Blacklist of input names to exclude. Mutually exclusive with `include`.\n * These inputs are removed from the auto-extracted set before walking.\n */\n exclude?: string[];\n\n /**\n * BehavioralFsm client factory. Required when the FSM produced by the\n * factory is a BehavioralFsm — each walk calls this to get a fresh client.\n * Omit for Fsm walks.\n */\n client?: () => TClient;\n\n /**\n * Called after every transition. Return `false` OR throw to signal a\n * violation — WalkFailureError is constructed automatically in both cases.\n * Return `true` or `undefined` (void) to indicate the invariant passed.\n */\n invariant: (args: InvariantArgs) => boolean | void;\n}\n\n/**\n * Runtime view of the config: both generator keys optional, element types\n * erased. The conditional half of WalkConfig exists for callers; the walk\n * loop treats the two keys uniformly, with `payloads` taking precedence.\n */\ntype RuntimeWalkConfig<TClient extends object> = WalkConfigBase<TClient> & {\n inputs?: Record<string, () => unknown>;\n payloads?: Record<string, () => unknown[]>;\n};\n\n/** A recorded step in the input sequence — used for replay and error reporting */\ninterface InputRecord {\n input: string;\n payload: unknown;\n}\n\n/**\n * Thrown when walkAll detects an invariant violation. Carries the seed and\n * full input sequence so you can replay the exact walk that failed.\n */\nexport class WalkFailureError extends Error {\n /** Seed that produced this walk — pass to walkAll({ seed }) to replay */\n readonly seed: number;\n /** Which step (1-indexed handle() call) caused the violation */\n readonly step: number;\n /** The inputs fired up to and including the failing step */\n readonly inputSequence: InputRecord[];\n /** State at the time of violation */\n readonly state: string;\n /** State before the violating transition */\n readonly previousState: string;\n /** Composite state at the time of violation */\n readonly compositeState: string;\n /** The FSM context or client at the time of violation */\n readonly ctx: unknown;\n\n constructor(args: {\n seed: number;\n step: number;\n inputSequence: InputRecord[];\n state: string;\n previousState: string;\n compositeState: string;\n ctx: unknown;\n cause: unknown;\n }) {\n const causeMessage = args.cause instanceof Error ? args.cause.message : String(args.cause);\n const sequenceSummary = args.inputSequence\n .map((r, i) => ` ${i + 1}. ${r.input}`)\n .join(\"\\n\");\n\n super(\n `walkAll invariant violation at step ${args.step} (seed: ${args.seed})\\n` +\n ` State: ${args.previousState} → ${args.state}\\n` +\n ` Composite: ${args.compositeState}\\n` +\n ` Cause: ${causeMessage}\\n` +\n `Input sequence:\\n${sequenceSummary}`\n );\n this.name = \"WalkFailureError\";\n this.seed = args.seed;\n this.step = args.step;\n this.inputSequence = args.inputSequence;\n this.state = args.state;\n this.previousState = args.previousState;\n this.compositeState = args.compositeState;\n this.ctx = args.ctx;\n }\n}\n\n// -----------------------------------------------------------------------------\n// Seeded PRNG — mulberry32\n//\n// Passes BigCrush and PractRand. 5 lines, no external dependency.\n// Returns floats in [0, 1) just like Math.random(), enabling drop-in\n// deterministic replacement wherever we'd otherwise use Math.random().\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a seeded PRNG using mulberry32.\n * Returns a function that yields deterministic floats in [0, 1).\n * Two PRNGs with the same seed produce identical sequences.\n */\nexport const createPrng = (seed: number): (() => number) => {\n let s = seed;\n return () => {\n s += 0x6d2b79f5;\n let t = s;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n};\n\n/**\n * Pick a random integer in [0, max) using the given PRNG.\n * Used to select a random index from the inputs array each step.\n */\nexport const randomInt = (prng: () => number, max: number): number => {\n return Math.floor(prng() * max);\n};\n\n// -----------------------------------------------------------------------------\n// Keys that are lifecycle hooks or special directives, not named inputs.\n// Mirrors SpecialStateKeys from machina core types.\n// -----------------------------------------------------------------------------\n\nconst RESERVED_KEYS = new Set([\"_onEnter\", \"_onExit\", \"_child\", \"*\"]);\n\n/**\n * Extract all distinct input names from an FSM's states object.\n * Skips reserved keys (_onEnter, _onExit, _child, *) — these are lifecycle\n * hooks and catch-alls, not named inputs you'd fire via handle().\n */\nexport const extractInputs = (fsm: {\n states: Record<string, Record<string, unknown>>;\n}): string[] => {\n const inputs = new Set<string>();\n for (const stateObj of Object.values(fsm.states)) {\n for (const key of Object.keys(stateObj)) {\n if (!RESERVED_KEYS.has(key)) {\n inputs.add(key);\n }\n }\n }\n return Array.from(inputs);\n};\n\n// -----------------------------------------------------------------------------\n// Config validation\n// -----------------------------------------------------------------------------\n\nconst validateConfig = (config: WalkConfigBase, availableInputs: string[]): string[] => {\n const walks = config.walks ?? 100;\n const maxSteps = config.maxSteps ?? 50;\n\n if (walks < 1) {\n throw new Error(`walkAll: walks must be >= 1, got ${walks}`);\n }\n\n if (maxSteps < 1) {\n throw new Error(`walkAll: maxSteps must be >= 1, got ${maxSteps}`);\n }\n\n if (config.include && config.exclude) {\n throw new Error(\n \"walkAll: include and exclude are mutually exclusive — use one or the other\"\n );\n }\n\n let inputs: string[];\n\n if (config.include) {\n // Catch typos and misconfigurations before walking — an include entry\n // that doesn't exist in the FSM is almost certainly a bug at the call site.\n const knownInputs = new Set(availableInputs);\n const unknown = config.include.filter(name => !knownInputs.has(name));\n if (unknown.length > 0) {\n throw new Error(\n `walkAll: include contains inputs not found in the FSM: ${unknown.join(\", \")}`\n );\n }\n inputs = config.include;\n } else if (config.exclude) {\n const excluded = new Set(config.exclude);\n inputs = availableInputs.filter(k => !excluded.has(k));\n } else {\n inputs = availableInputs;\n }\n\n if (inputs.length === 0) {\n throw new Error(\n \"walkAll: no inputs available after filtering — check include/exclude config\"\n );\n }\n\n return inputs;\n};\n\n// -----------------------------------------------------------------------------\n// Walk engine\n// -----------------------------------------------------------------------------\n\n/**\n * Run property-based walk tests against a live FSM.\n *\n * Throws WalkFailureError on invariant violation. Returns WalkResult on success.\n *\n * @param factory - Called once per walk to create a fresh FSM instance.\n * Each walk gets its own FSM so context mutations don't bleed between walks.\n * @param config - Walk configuration: invariant, walk count, step limit, seed,\n * input filters, payload generators, and optional client factory.\n */\nexport const walkAll = <TFsm = unknown, TClient extends object = object>(\n factory: () => TFsm,\n config: WalkConfig<TClient, InputMapOfInstance<TFsm>>\n): WalkResult => {\n // The conditional generator half of WalkConfig exists for callers; the\n // runtime treats both generator keys uniformly through this view.\n const cfg = config as RuntimeWalkConfig<TClient>;\n const walks = cfg.walks ?? 100;\n const maxSteps = cfg.maxSteps ?? 50;\n\n // Build the input list from the first FSM instance — we only need this\n // for validation, not for actual walking (each walk creates its own FSM).\n const probeFsm = factory() as { states: Record<string, Record<string, unknown>> } & {\n dispose?: () => void;\n };\n const allInputs = extractInputs(probeFsm);\n const inputs = validateConfig(cfg, allInputs);\n\n // Dispose the probe FSM if possible — it was only needed for input extraction.\n if (typeof probeFsm.dispose === \"function\") {\n probeFsm.dispose();\n }\n\n const seed = cfg.seed ?? Math.floor(Math.random() * 2 ** 32);\n const prng = createPrng(seed);\n\n for (let walkIndex = 0; walkIndex < walks; walkIndex++) {\n runSingleWalk(factory, cfg, inputs, prng, seed, maxSteps);\n }\n\n return { seed, walksCompleted: walks };\n};\n\n/**\n * Run one walk: create a fresh FSM, subscribe to events, fire random inputs,\n * check invariant after every transition. Throws WalkFailureError on violation.\n */\nconst runSingleWalk = <TClient extends object>(\n factory: () => unknown,\n config: RuntimeWalkConfig<TClient>,\n inputs: string[],\n prng: () => number,\n seed: number,\n maxSteps: number\n): void => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the walk loop drives any FSM shape; the public walkAll signature owns the typing\n const fsm = factory() as any;\n const isBehavioral = fsm[MACHINA_TYPE] === \"BehavioralFsm\";\n\n // For BehavioralFsm: create a client and track it separately.\n // For Fsm: the client IS the fsm (context lives on the fsm).\n const client: TClient | undefined = isBehavioral\n ? config.client\n ? config.client()\n : ({} as TClient)\n : undefined;\n\n // Initialize BehavioralFsm client by transitioning into initialState.\n // Fsm eagerly initializes in its constructor — no action needed.\n if (isBehavioral && client !== undefined) {\n fsm.transition(client, fsm.initialState);\n }\n\n const inputSequence: InputRecord[] = [];\n let currentStep = 0;\n let currentInput = \"\";\n let violation: unknown = null;\n let violationArgs: InvariantArgs | null = null;\n\n // Track \"current input\" via the handling event, which fires before dispatch.\n // This is the input that caused any subsequent transitioned events — including\n // those from _onEnter bounces, where handling does NOT re-fire.\n // Both Fsm and BehavioralFsm expose inputName on this event, so no branching needed.\n const handlingSub = fsm.on(\"handling\", (data: any) => {\n currentInput = data.inputName;\n });\n\n // The transitioned event fires after every state change, including _onEnter\n // bounces. We run the invariant here so we catch all transition types.\n const transitionedSub = isBehavioral\n ? fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n const ctx = data.client ?? client;\n const composite = fsm.compositeState(data.client ?? client);\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n })\n : fsm.on(\"transitioned\", (data: any) => {\n if (violation !== null) {\n return;\n }\n // For Fsm, read context from fsm.context (Task 1 made this public)\n const ctx = fsm.context;\n const composite = fsm.compositeState();\n const record = inputSequence[inputSequence.length - 1];\n const args: InvariantArgs = {\n state: data.toState,\n previousState: data.fromState,\n compositeState: composite,\n ctx,\n input: currentInput,\n payload: record?.payload,\n step: currentStep,\n };\n try {\n const result = config.invariant(args);\n // Returning false is an explicit violation signal (same as throwing).\n // undefined/void and true both mean \"invariant holds\".\n if (result === false) {\n violation = new Error(\"invariant returned false\");\n violationArgs = args;\n }\n } catch (err) {\n violation = err;\n violationArgs = args;\n }\n });\n\n try {\n for (let step = 1; step <= maxSteps; step++) {\n currentStep = step;\n const inputName = inputs[randomInt(prng, inputs.length)];\n let extraArgs: unknown[];\n let payload: unknown;\n const tupleGen = config.payloads?.[inputName];\n if (tupleGen) {\n // Typed generators return the input's full argument tuple;\n // walkAll spreads it. An untyped caller reaching this path\n // from JavaScript could return anything — fail loudly rather\n // than guess whether a non-array was meant as one argument.\n const tuple = tupleGen();\n if (!Array.isArray(tuple)) {\n throw new Error(\n `walkAll: payloads.${inputName} must return the input's argument tuple (an array)`\n );\n }\n extraArgs = tuple;\n payload = tuple;\n } else {\n // Legacy generators return one value, passed as one argument.\n // Only spread when a generator produced one — handlers with no\n // generator shouldn't receive a spurious undefined argument,\n // which could mask bugs in handlers that check arguments.length.\n payload = config.inputs?.[inputName]?.();\n extraArgs = payload !== undefined ? [payload] : [];\n }\n inputSequence.push({ input: inputName, payload });\n if (isBehavioral && client !== undefined) {\n fsm.handle(client, inputName, ...extraArgs);\n } else {\n fsm.handle(inputName, ...extraArgs);\n }\n\n if (violation !== null) {\n throw new WalkFailureError({\n seed,\n step,\n inputSequence: [...inputSequence],\n state: violationArgs!.state,\n previousState: violationArgs!.previousState,\n compositeState: violationArgs!.compositeState,\n ctx: violationArgs!.ctx,\n cause: violation,\n });\n }\n }\n } finally {\n // Always unsubscribe, even on failure — prevents listener leaks between walks\n handlingSub.off();\n transitionedSub.off();\n\n if (typeof fsm.dispose === \"function\") {\n fsm.dispose();\n }\n }\n};\n","// =============================================================================\n// index.ts — Side-effect entry point for machina-test\n//\n// Importing this module registers all three custom matchers with the global\n// `expect` provided by Jest or Vitest. This is the same pattern used by\n// @testing-library/jest-dom — the import IS the side effect.\n//\n// `expect` must be available as a global at the time this module is evaluated.\n// Put the import in a test file or in a setup file that runs after the test\n// framework's globals are injected.\n// =============================================================================\n\nimport { toHaveNoUnreachableStates, toAlwaysReach, toNeverReach } from \"./matchers\";\n\n// Register with the host test runner's global expect\nexpect.extend({ toHaveNoUnreachableStates, toAlwaysReach, toNeverReach });\n\n// Re-export the options type so consumers can import it alongside the package\nexport type { ReachabilityOptions } from \"./types\";\n\n// walkAll is a standalone function — no side-effect registration needed.\n// Import it directly: import { walkAll } from \"machina-test\"\nexport { walkAll } from \"./walk\";\nexport type { WalkConfig, InvariantArgs, WalkResult } from \"./walk\";\nexport { WalkFailureError } from \"./walk\";\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machina-test",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Jest/Vitest custom matchers for testing machina FSMs.",
5
5
  "author": "Jim Cowart (http://ifandelse.com)",
6
6
  "license": "MIT",
@@ -44,11 +44,11 @@
44
44
  },
45
45
  "sideEffects": true,
46
46
  "dependencies": {
47
- "machina-inspect": "4.0.0"
47
+ "machina-inspect": "4.0.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "jest": ">=29",
51
- "machina": ">=7.0.0",
51
+ "machina": ">=8.0.0",
52
52
  "vitest": ">=1"
53
53
  },
54
54
  "peerDependenciesMeta": {
@@ -61,12 +61,12 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/jest": "^30.0.0",
64
- "eslint": "^9.39.4",
65
- "jest": "^30.3.0",
66
- "ts-jest": "^29.4.6",
64
+ "eslint": "^9.39.5",
65
+ "jest": "^30.5.2",
66
+ "ts-jest": "^29.4.14",
67
67
  "tsdown": "^0.20.3",
68
68
  "typescript": "^5.9.3",
69
- "machina": "7.0.0"
69
+ "machina": "8.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "dev": "tsdown --watch",