Skip to main content
failproofai has two test suites: unit tests (fast, mocked) and end-to-end tests (real subprocess invocations).

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 real failproofai 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':
Then run the tests:
Rebuild 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 use vitest.config.e2e.mts with:
  • environment: "node" - no browser globals needed
  • pool: "forks" - true process isolation (tests spawn subprocesses)
  • testTimeout: 20_000 - 20s per test (binary startup + hook eval)
The 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.