Running tests
Unit tests
Unit tests live in__tests__/ and use Vitest with jsdom.
Writing a policy unit test
End-to-end tests
E2E tests invoke the realfailproofai binary as a subprocess, pipe a JSON payload to stdin, and assert on the stdout output and exit code. This tests the complete integration path that Claude Code uses.
Setup
E2E tests run the binary directly from the repo source. Before the first run, build the CJS bundle that custom hook files use when they import from'failproofai':
dist/ whenever you change the public hook API (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts, or src/hooks/policy-types.ts).
E2E test structure
Using the E2E helpers
FixtureEnv - isolated per-test environment:
createFixtureEnv() registers afterEach cleanup automatically.
runHook - invoke the binary:
Payloads - ready-made payload factories:
Writing an E2E test
E2E response shapes
Vitest config
E2E tests usevitest.config.e2e.mts with:
environment: "node"- no browser globals neededpool: "forks"- true process isolation (tests spawn subprocesses)testTimeout: 20_000- 20s per test (binary startup + hook eval)
forks pool is important: thread-based workers share globalThis, which can interfere with subprocess-spawning tests. Process-based forks avoid this.
CI
The full CI run (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) is required to pass before merging. The E2E suite runs as a separate CI job in parallel.
See Contributing for the complete pre-merge checklist.
