Testing & gates#
There is no CI. Every gate in this project is local, run from package.json
scripts, and the repository says so deliberately —
dev/local-quality-gates.md:3 states it outright. This page is therefore also
the CI documentation: if you do not run these, nobody does.
The three rungs#
pnpm preflight format:check → lint:src → test → build
│
pnpm check preflight → e2e → sidecar:check → check:tauri-schema
│ → rust:check → rust:test
│
pnpm release:readiness executes those nine, plus the distribution report
| Command | What it is for |
|---|---|
pnpm preflight |
The everyday pre-commit pass |
pnpm check |
Everything. Genuinely long — Playwright plus 13 sequential sidecar smokes plus ~970 Rust tests |
pnpm release:readiness |
The nine gates plus signing, updater and artifact inspection |
pnpm check is slow enough that release-readiness.mjs defaults to
a 45-minute per-gate timeout (PACKETBENCH_RELEASE_GATE_TIMEOUT_MS). That
is a fair indicator of the magnitude.
What was green at the last release#
Recorded in CHANGELOG.md for 0.13.1 at commit 8dc13780:
| Gate | Result |
|---|---|
cargo check |
clean |
| Rust lib tests | 976 passing, 2 ignored |
acp_stream integration |
31 passing |
tsc --noEmit |
0 errors |
pnpm lint |
0 errors (9 warnings, all pre-existing) |
vitest run |
2765 / 2765 across 286 files |
check:tauri-schema |
clean |
A static count of Rust test attributes comes out a little higher
(856 inline plus 33 in tests/). Seven inline tests are #[cfg(unix)]-gated
and nine are #[ignore]d, so the number that actually runs on a Windows box
is lower than the number you can grep. Trust the run, not the grep.
Frontend: vitest#
vitest.config.ts is 18 lines and every one matters:
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}", "scripts/**/*.{test,spec}.{mjs,ts}"],
globals: true,
266 files: 262 under src/ (168 .test.ts + 94 .test.tsx) and 4 under
scripts/. Tests concentrate heavily — src/lib/__tests__ and
src/stores/__tests__ are about 59% of the frontend suite.
src/test/setup.ts imports @testing-library/jest-dom and polyfills an
in-memory localStorage on both window and globalThis when jsdom's is
missing.
There is no coverage tooling. vitest.config.ts has no
coverage block and no @vitest/coverage-* package is installed, so
--coverage fails. Do not promise a coverage number.
The scripts/** include glob is the only reason the four
repository fences run. Running vitest src — scoping to a directory, as you
would when iterating — silently skips all four. Run the bare
pnpm test before you push.
The repository fences#
Four files under scripts/ are not unit tests of application code. They scan
the source tree for banned idioms and fail the build when one appears. They are
written as vitest suites (none use node --test), so a violation turns
pnpm test, preflight and check red.
Three of the four share the same preamble: a read-once FILE_CONTENTS cache
and FENCE_TIMEOUT_MS = 30_000. The reason is recorded at
scripts/confirm-idiom.test.mjs:14 — re-reading the tree per pattern took 9-25 s
and timed out against vitest's 5 s default whenever the disk was busy, and "a
fence that fails for reasons unrelated to what it guards is worse than no
fence."
scripts/confirm-idiom.test.mjs#
Bans the native confirm dialog. The regex at :52 is
/(?<![\w$.])(?:(?:window|globalThis|self)\??\.)?confirm\s*\(/
The lookbehind is what keeps showConfirm(, onConfirm( and dialog.confirm(
legal. It sanitizes in a specific order — strip block comments, blank quoted
spans per line, then strip line comments — because doing it any other way
produced false positives; there is a recorded incident where a test title
containing the words failed a build.
It also asserts positively: ConfirmDeleteModal is still exported, and 14 named
files still import it, so a silent revert to a bare delete shows up.
scripts/attempt-provider-mapping.test.mjs#
Bans deriving a backend provider id by stripping the api- prefix:
/(?:replace(?:All)?\s*\(\s*\/\^api-\/|slice\s*\(\s*["'`]api-["'`]\.length)/
This needs a source fence rather than a unit test because the derivation is
right for seven of eight executors and wrong only for the default
api-claude → "claude", which get_provider rejects — "a shape that survives
review easily". One allowlist entry: the test that demonstrates the broken
derivation on purpose.
It also asserts attemptProviderFor is exported from
src/lib/attemptRouting.ts and referenced by both
src/components/flights/pickedToSpec.ts and src/stores/asyncFlightStore.ts.
scripts/workspace-agents-boundaries.test.mjs#
The broadest fence, and the only one that scans both src/ (.ts/.tsx)
and src-tauri/src/ (.rs). Three invariants:
- Workspace/Agents separation. No
addDraft(anywhere.WorkspaceView,WorkspaceCreationModalandAddSessionPickermay not mentionlaunchConversation,API_PROVIDERS,addConversationPaneoruseAgentTaskStore.AgentsViewmust containlaunchConversation({and must not haveonLaunched:. - Retired identifiers. Six names banned repo-wide (
openSession(,addConversationPane,ensureConversationWorkspace,attachConversationToWorkspace,openConversationAlongsideWorkspace,DraftTile), withsrc/stores/sessionGlue.tsrequired to exportopenConversationInAgentsand containremoveConversationPanes. - The Monitor window boundary.
WebviewWindowBuilderis banned in every Rust source exceptsrc-tauri/src/commands/monitor_windows.rs;new WebviewWindowis banned in all frontend sources. It then JSON-parsessrc-tauri/capabilities/monitor.jsonand assertswindows === ["monitor-*"]and thatpermissionsexcludesshell:default,fs:defaultandprocess:default.
Unlike the other two, this fence skips __tests__ directories and *.test.*
files.
scripts/target-triple.test.mjs#
Mostly a genuine unit test of scripts/target-triple.js — the five supported
triples, tripleToSupportedArchitectures, sidecarPlatformPackage,
detectHostTarget, resolveTarget. The notable assertion is that
resolveTarget throws when TAURI_TARGET and TAURI_ENV_TARGET_TRIPLE
disagree, because a stale export used to silently redirect fetch-node,
prune-sidecar and release-gate all to the same wrong target — so they agreed
with each other and the gate could not see it.
Its last describe block is a fence, over scripts/release-gate.mjs: no
hardcoded node-x86_64-pc-windows-msvc.exe; nodeBinaryRelPath(triple),
verifyStagedNodeRuntime(releaseTarget), nodeArchiveSha256(triple) and
createHash("sha256") must all be present; and it regex-extracts the
authenticodeEnv and appleEnv arrays to assert neither contains
TAURI_SIGNING_PRIVATE_KEY.
That last assertion is not pedantry. Counting the updater minisign key as code-signing evidence once produced a false "Signing credentials present" PASS on a build with zero Authenticode configuration.
ESLint carries a fence too#
eslint.config.js uses no-restricted-imports to forbid agentTaskStore.ts
importing workspaceStore and vice versa. sessionGlue is the only permitted
bridge.
Rust tests#
Mostly inline #[cfg(test)] modules — 93 files carrying them, densest in
src/core/worktree.rs (42), src/commands/provider_auth.rs (41),
src/commands/dictation/analytics.rs (28), src/core/git.rs (26) and
src/commands/project_memory.rs (26).
Three integration files under src-tauri/tests/:
| File | Shape |
|---|---|
acp_stream.rs |
1207 lines, 31 #[tokio::test]. End-to-end ACP bridge streaming against src-tauri/testdata/mock-engine.mjs, spawned as node … acp. No packetcode binary required, so it runs in a plain cargo test |
api_schema.rs |
11 lines, one #[ignore]d test — the TS-binding exporter |
ollama_e2e.rs |
312 lines, one #[ignore]d #[tokio::test]. Needs a live Ollama daemon; preconditions skip with a message rather than failing |
acp_stream.rs bounds every await with STEP = Duration::from_secs(10) so a
wedged reader fails fast instead of hanging the suite.
Hermetic storage tests#
src-tauri/src/core/storage.rs carries a #[cfg(test)] thread-local
data-dir override (TEST_DATA_DIR, :243). It is thread-local rather than a
global or an env-var mutation so parallel tests each get an isolated temp
directory and never race. The whole facility is #[cfg(test)], so production
data_dir() compiles byte-identically in release.
Do not replace that with a HOME rewrite. The historic approach
was #[ignore] plus a HOME mutation, which clobbered real user state.
Sidecar smokes#
Thirteen standalone .mjs scripts in agent-sidecar/test/, chained by
pnpm sidecar:check:
sidecar:install → sidecar:build →
smoke, remote-project-smoke, registry-smoke, protocol-smoke,
session-ordering-smoke, gating-smoke, anthropic-edit-correlation-smoke,
anthropic-multi-turn-smoke, mcp-config-smoke, mcp-trust-smoke,
mcp-trust-enforcement-smoke, remote-mcp-fromfs-smoke, anthropic-apikey-smoke
There is no test framework and no test script in agent-sidecar/package.json
— that file only has build and dev. Each smoke is invoked from the root
package.json as a bare node <path>.
The pattern is the same throughout: spawn node agent-sidecar/dist/index.js,
write an NDJSON request to stdin, parse the NDJSON event stream from stdout with
readline, assert against explicit pass criteria written as a comment block at
the top of the file, then exit 0 with "OK" or print diagnostics and exit 1.
They run against dist/, so sidecar:install && sidecar:build must precede
them — echo-smoke.mjs fails with an explicit instruction if dist/index.js
is missing.
After any pnpm tauri build, the sidecar's devDependencies have
been pruned away and sidecar:build will fail. Run pnpm sidecar:install
first. See Build & release.
Schema staleness check#
pnpm check:tauri-schema runs scripts/check-tauri-schema.mjs, which is a
generate-diff-restore:
- Read the current
src/generated/tauri-schema.tsinto memory. - Run
cargo test --test api_schema export_api_bindings -- --ignored --nocapture, which overwrites the file. - Read the regenerated content, then write the original back — so the check never leaves the tree dirty.
- Exit 1 with "Run pnpm generate:tauri-schema" if they differ.
It resolves paths from import.meta.url, not cwd, and it runs cargo with
cwd: src-tauri because Cargo discovers .cargo/config.toml from its working
directory, not from --manifest-path — running from the repo root would miss
this repo's target-dir redirect.
End-to-end (Playwright)#
playwright.config.ts: testDir: "./e2e", chromium only, 30 s timeout, web
server pnpm exec vite --host 127.0.0.1 --port 1420 --strictPort. Nine test()
calls across six spec files.
These are web-mode only. Tauri IPC is mocked via
e2e/setup/mock-tauri.ts. Nothing here exercises the native shell, the PTY
layer, the sidecar or the keyring. An e2e pass is not evidence that a packaged
install works.
Known gaps#
Be honest about these rather than discovering them at release time.
format:checkdoes not coversrc/. It checkspackage.json,eslint.config.js,dev/local-quality-gates.mdande2e/**only.pnpm formatwritessrc/, but no gate verifies it. Intentional perdev/local-quality-gates.md:31, but surprising. A corollary: editingdev/local-quality-gates.mdwith the wrong Prettier formatting breakspreflight.lint:srcallows warnings.preflightandcheckuselint:src, notlint:strict.react-refresh/only-export-componentsandno-unused-varsare warnings and never block a gate.- The four fences are invisible in the script list. They are not a named
script; they exist only because of the
scripts/**include glob. - CI-aware code exists with no CI.
playwright.config.ts:17branches onprocess.env.CIforforbidOnly,retriesand the"github"reporter, andeslint.config.jscomments talk about turning CI red. Accurate in spirit — a store-isolation regression does faillint:srcinsidepreflight— but there is no CI to turn red. - No packaged install has been through the full acceptance matrix. The
0.12.1 changelog entry says so explicitly.
pnpm checkbeing green is not the same as an installer working.
Adding a test#
- Frontend unit:
src/**/__tests__/*.test.ts(x). Follow the neighbours; the store tests insrc/stores/__tests__are the richest examples. - Rust unit: an inline
#[cfg(test)] mod testsbeside the code. If it touches the data dir, use the thread-local override instorage.rs. - A fence rather than a unit test when the wrong shape is one that survives
review — a plausible-looking idiom that is correct in most instances and
quietly wrong in one. That is the stated criterion in
scripts/attempt-provider-mapping.test.mjs:5. - A sidecar smoke when the behaviour is on the wire. Copy
echo-smoke.mjs, write the pass criteria as a header comment, and add both a script entry and a link in thesidecar:checkchain.
Related#
- Invariants & tripwires — what the fences protect
- Build & release — the gates that run at bundle time
- Contributing — conventions the linters do not catch