UNPKG

@firfi/quint-connect

Version:

Model-based testing framework connecting Quint specifications to TypeScript implementations

294 lines (213 loc) 8.56 kB
--- name: quint-connect-ts-debug description: > Diagnose quint-connect test failures. StateMismatchError (state diverged at step N, has traceIndex, stepIndex, expected, actual), TraceReplayError (handler threw, has action, cause), NoTracesError (wrong --main module or unsatisfiable spec), QuintError (quint run exited non-zero), QuintNotFoundError (quint CLI not on PATH). Reproduce failures deterministically with seed. Inspect raw ITF traces via traceDir. Use when a quint-connect test fails and you need to understand why. type: core library: quint-connect-ts library_version: "0.6.0 (Effect 3, @latest) / 1.0.0-effect4 (Effect 4, @effect4)" sources: - "dearlordylord/quint-connect-ts:README.md" - "dearlordylord/quint-connect-ts:src/runner/runner.ts" - "dearlordylord/quint-connect-ts:src/cli/quint.ts" - "dearlordylord/quint-connect-ts:BUG_REPORT.md" --- # quint-connect-ts -- Debug Failing Trace ## Effect 3 vs Effect 4 This only matters if the project uses `effect` as a dependency (Effect API path). The Simple API error handling is identical across both versions. Install the correct dist-tag: `@firfi/quint-connect@latest` for `effect@^3`, `@firfi/quint-connect@effect4` for `effect@^4`. See `quint-connect-ts-setup` skill for full details and API difference table. The error classes (`StateMismatchError`, `TraceReplayError`, etc.) work the same way in both versions. Internally they use `Schema.TaggedError` (Effect 3) or `Schema.TaggedErrorClass` (Effect 4), but the user-facing API (`Effect.catchTag`, error fields) is unchanged. ## Error Types | Error | When | Key fields | |---|---|---| | `StateMismatchError` | `compareState` returned false | `traceIndex`, `stepIndex`, `expected`, `actual` | | `TraceReplayError` | Handler threw, decode failed, unknown action | `traceIndex`, `stepIndex`, `action`, `cause` | | `NoTracesError` | `quint run --mbt` produced no trace files | -- | | `QuintError` | `quint run` exited non-zero | stderr output | | `QuintNotFoundError` | `quint` CLI not found on PATH | -- | ## Setup -- Error Handling (Simple API) ```ts import { run, StateMismatchError, TraceReplayError, } from "@firfi/quint-connect" try { await run(opts) } catch (e) { if (e instanceof StateMismatchError) { console.log("State diverged at trace", e.traceIndex, "step", e.stepIndex) console.log("Expected (spec):", e.expected) console.log("Actual (impl):", e.actual) } if (e instanceof TraceReplayError) { console.log("Action failed:", e.action, "at step", e.stepIndex) console.log("Cause:", e.cause) } } ``` ## Setup -- Error Handling (Effect API) ```ts import { quintRun } from "@firfi/quint-connect/effect" quintRun(opts).pipe( Effect.catchTag("StateMismatchError", (e) => Effect.log("Diverged at", e.traceIndex, e.stepIndex, e.expected, e.actual)), Effect.catchTag("TraceReplayError", (e) => Effect.log("Action", e.action, "failed:", e.cause)), ) ``` ## Core Patterns ### Reproduce a failure with seed Every `run`/`quintRun` result includes a `seed`. Pass it back to get the exact same traces: ```ts // First run — failure reported const result = await run({ spec: specPath, driver: myDriver, nTraces: 10, }) // result.seed = "0x138ff8c9" // Reproduce — same traces, same failure const result2 = await run({ spec: specPath, driver: myDriver, nTraces: 10, seed: "0x138ff8c9", }) ``` The seed can also be set via `QUINT_SEED` environment variable. ### Inspect raw ITF traces Use `traceDir` to persist trace files for manual inspection: ```ts await run({ spec: specPath, driver: myDriver, traceDir: "./debug-traces", }) ``` Then read the JSON files in `./debug-traces/` to see the raw ITF state at each step. Look for: - `mbt::actionTaken` — which action was dispatched - `mbt::nondetPicks` — what nondeterministic values were chosen - State variable keys — verify fully-qualified names ### Run quint manually to inspect output Before writing any driver code, run quint directly to see what it produces: ```bash npx @informalsystems/quint run --mbt \ --max-samples 1 --max-steps 3 \ specs/counter.qnt ``` This shows the exact ITF output: variable names, ITF encoding, action names. Inspect this before guessing at schema shapes. ### Debug NoTracesError `NoTracesError` means quint ran but produced zero trace files. Common causes: 1. **Wrong module name** — multi-module specs require `--main`: ```ts await run({ spec: specPath, main: "counter_test" }) ``` 2. **Unsatisfiable spec** — the init or step action's preconditions are never satisfied. Check the spec logic. 3. **Wrong spec path** — the file doesn't exist or isn't a valid `.qnt` file. ### Debug QuintNotFoundError Quint CLI must be on PATH. Install it: ```bash pnpm add -D @informalsystems/quint ``` Or ensure it's available globally. `npx @informalsystems/quint` works without global install. ## Common Mistakes ### CRITICAL Return live mutable object from getState Wrong: ```ts const state = { items: new Map() } return { getState: () => state, } ``` Correct: ```ts const state = { items: new Map() } return { getState: () => ({ items: new Map(state.items) }), } ``` The runner captures state after each step for comparison. Returning a live reference means the captured "expected" snapshot mutates when the next step runs. All comparisons pass vacuously. Source: maintainer interview ### HIGH Re-run without seed to reproduce failure Wrong: ```ts // Test failed, try again: await run({ spec: specPath, nTraces: 10 }) // Different random traces, different failure — or no failure at all ``` Correct: ```ts // Use the seed from the failed run's output: await run({ spec: specPath, nTraces: 10, seed: "0x138ff8c9" }) // Exact same traces, exact same failure ``` Without a seed, `quintRun` generates new random traces each time. The original failure may not reproduce. Always capture and reuse the seed. Source: README.md ### HIGH Hack the comparator instead of fixing the implementation Wrong: ```ts stateCheck(deserialize, (spec, impl) => { const { internalField: _, ...specRest } = spec const { internalField: __, ...implRest } = impl return deepEqual(specRest, implRest) }) ``` Correct: ```ts stateCheck(deserialize, (spec, impl) => spec.count === impl.count && spec.flag === impl.flag ) ``` When state diverges, the Quint spec is the source of truth. Ignoring fields, sorting maps, or adding exceptions to the comparator masks real implementation bugs. Fix the TS code to match the spec. Source: maintainer interview ### MEDIUM Use nTraces: 1 in CI for speed Wrong: ```ts await run({ spec: specPath, nTraces: 1, maxSteps: 50 }) ``` Correct: ```ts await run({ spec: specPath, nTraces: 10, maxSteps: 50 }) ``` A single trace covers one random execution path. Bugs on other paths pass CI and merge. 10 traces with 50 steps covers diverse paths without being excessively slow. Source: maintainer interview ### MEDIUM Hardcode relative spec path Wrong: ```ts await run({ spec: "../../specs/counter.qnt" }) ``` Correct: ```ts import * as path from "node:path" const specPath = path.join(import.meta.dirname, "specs", "counter.qnt") await run({ spec: specPath }) ``` Relative paths like `../../specs/counter.qnt` depend on the working directory. They work locally but break in CI where the test runner may use a different cwd. Source: examples/counter/counter.test.ts ### MEDIUM Use Effect.orDie in production state deserializer Wrong: ```ts stateCheck( (raw) => Schema.decodeUnknown(MyState)(raw).pipe(Effect.orDie), (spec, impl) => spec.count === impl.count, ) ``` Correct: ```ts stateCheck( (raw) => Schema.decodeUnknown(MyState)(raw), (spec, impl) => spec.count === impl.count, ) ``` `Effect.orDie` converts decode failures to untyped defects. This loses the structured `TraceReplayError` context (traceIndex, stepIndex, action) that helps diagnosis. Fine for prototyping; in CI, let errors propagate with full context. Source: maintainer interview ### HIGH Tension: setup simplicity vs production robustness Patterns that work during prototyping (`Effect.orDie`, `nTraces: 1`, partial state comparison) silently mask bugs in production. When optimizing for quick setup, agents omit the robustness that makes MBT testing valuable. Use `nTraces: 10+`, compare all state fields, and preserve structured error context. See also: quint-connect-ts-setup/SKILL.md See also: quint-connect-ts-itf-decoding/SKILL.md -- decode errors often surface as TraceReplayError