Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ jobs:
echo "Root version: $ROOT_VERSION"
MISMATCH=0
for pkg in packages/*/package.json; do
[ -f "$pkg" ] || continue
PKG_VERSION=$(jq -r .version "$pkg")
if [ "$PKG_VERSION" != "$ROOT_VERSION" ]; then
echo "::error file=$pkg::Version mismatch: $pkg has $PKG_VERSION, expected $ROOT_VERSION"
MISMATCH=1
fi
done
# Check optionalDependencies in wrapper
for dep_version in $(jq -r '.optionalDependencies // {} | values[]' packages/wrapper/package.json); do
for dep_version in $(jq -r '.optionalDependencies // {} | values[]' packages/wrapper/package.json 2>/dev/null || true); do
if [ "$dep_version" != "$ROOT_VERSION" ]; then
echo "::error file=packages/wrapper/package.json::Dependency version mismatch: $dep_version, expected $ROOT_VERSION"
MISMATCH=1
Expand Down Expand Up @@ -163,12 +164,8 @@ jobs:
timeout_minutes: 5
command: bun install --frozen-lockfile

- name: Build npm package (setup E2E env)
uses: nick-fields/retry@v4
with:
max_attempts: 2
timeout_minutes: 15
command: bun run test:npx
- name: Build E2E fixtures
run: bun build src/index.ts --outdir dist --target node --format cjs

- name: E2E Hook Tests
uses: nick-fields/retry@v4
Expand Down
24 changes: 8 additions & 16 deletions __tests__/e2e/helpers/hook-runner.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
/**
* Runs the failproofai binary as a subprocess for E2E hook tests.
*
* Invokes .test-npx/node_modules/@failproofai/<platform>/bin/failproofai --hook <event>
* exactly as Claude Code does — no Node.js bridge, no mocks.
* Invokes bin/failproofai.mjs --hook <event> via bun, exactly as Claude Code does —
* no Node.js bridge, no mocks.
*
* Run `bun run test:npx` once before running these tests.
* Run `bun build src/index.ts --outdir dist --target node --format cjs` once before
* running these tests (required for custom hook files that import from 'failproofai').
*/
import { expect } from "vitest";
import { spawnSync } from "node:child_process";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { platform, arch } from "node:os";

const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");

function getBinaryPath(): string {
const os = platform(); // linux | darwin | win32
const cpu = arch(); // x64 | arm64
const ext = os === "win32" ? ".exe" : "";
const pkgName = `${os}-${cpu}`;
return resolve(REPO_ROOT, `.test-npx/node_modules/@failproofai/${pkgName}/bin/failproofai${ext}`);
return resolve(REPO_ROOT, "bin/failproofai.mjs");
}

function getDistPath(): string {
return resolve(REPO_ROOT, ".test-npx/node_modules/failproofai/dist");
return resolve(REPO_ROOT, "dist");
}

export interface HookRunResult {
Expand All @@ -50,10 +45,7 @@ export function runHook(
const binaryPath = getBinaryPath();

if (!existsSync(binaryPath)) {
throw new Error(
`E2E binary not found: ${binaryPath}\n` +
`Run \`bun run test:npx\` first to build and install the npm package.`,
);
throw new Error(`E2E binary not found: ${binaryPath}`);
}

const env: NodeJS.ProcessEnv = {
Expand All @@ -63,7 +55,7 @@ export function runHook(
...(opts?.homeDir ? { HOME: opts.homeDir } : {}),
};

const result = spawnSync(binaryPath, ["--hook", event], {
const result = spawnSync("bun", [binaryPath, "--hook", event], {
input: JSON.stringify(payload),
env,
encoding: "utf8",
Expand Down
2 changes: 1 addition & 1 deletion __tests__/e2e/hooks/builtin-policies.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Each test invokes the real failproofai binary as a subprocess with an isolated
* fixture environment — no mocks, no Claude, just stdin/stdout.
*
* Run `bun run test:npx` once before running these tests.
* Run `bun build src/index.ts --outdir dist --target node --format cjs` once before running these tests.
*/
import { describe, it } from "vitest";
import { runHook, assertAllow, assertPreToolUseDeny, assertPostToolUseDeny, assertInstruct } from "../helpers/hook-runner";
Expand Down
13 changes: 4 additions & 9 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,19 @@ E2E tests invoke the real `failproofai` binary as a subprocess, pipe a JSON payl

### Setup

E2E tests require the npm package to be built and installed locally first:
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'`:

```bash
bun run test:npx
bun build src/index.ts --outdir dist --target node --format cjs
```

This script:
1. Builds the Next.js standalone
2. Compiles the native binary (`bun build --compile`)
3. Packs and installs both platform and wrapper packages into `.test-npx/`

Once setup, run the tests:
Then run the tests:

```bash
bun run test:e2e
```

The built package persists in `.test-npx/` between runs, so you only need to run `test:npx` again after making changes to the hook handler or binary entry point.
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

Expand Down
19 changes: 19 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Public API for failproofai custom hooks.
*
* Used as the bundle entry point for `dist/index.js` (CJS) and re-exported
* by the ESM shim that rewrites `from 'failproofai'` in user hook files.
*/
export {
customPolicies,
getCustomHooks,
clearCustomHooks,
} from "./hooks/custom-hooks-registry";
export { allow, deny, instruct } from "./hooks/policy-helpers";
export type {
PolicyContext,
PolicyResult,
CustomHook,
PolicyDecision,
PolicyFunction,
} from "./hooks/policy-types";