Chuyển đến nội dung chính
failproofai có hai bộ kiểm thử: unit tests (nhanh, mocked) và end-to-end tests (subprocess thực).

Chạy kiểm thử

# Chạy tất cả unit tests một lần
bun run test:run

# Chạy unit tests ở chế độ watch
bun run test

# Chạy E2E tests (cần setup - xem bên dưới)
bun run test:e2e

# Kiểm tra kiểu mà không cần build
bunx tsc --noEmit

# Lint
bun run lint

Unit tests

Unit tests nằm trong __tests__/ và sử dụng Vitest với jsdom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Logic policy cho mỗi builtin
    hooks-config.test.ts          # Config loading và scope merging
    policy-evaluator.test.ts      # Param injection và evaluation order
    custom-hooks-registry.test.ts # globalThis registry add/get/clear
    custom-hooks-loader.test.ts   # ESM loader, transitive imports, error handling
    manager.test.ts               # install/remove/list operations
  components/
    sessions-list.test.tsx        # Session list component
    project-list.test.tsx         # Project list component
    ...
  lib/
    logger.test.ts
    paths.test.ts
    date-filters.test.ts
    telemetry.test.ts
    ...
  actions/
    get-hooks-config.test.ts
    get-hook-activity.test.ts
    ...
  contexts/
    ThemeContext.test.tsx
    AutoRefreshContext.test.tsx

Viết unit test cho policy

import { describe, it, expect, beforeEach } from "vitest";
import { getBuiltinPolicies } from "../../src/hooks/builtin-policies";
import { allow, deny } from "../../src/hooks/policy-types";

describe("block-sudo", () => {
  const policy = getBuiltinPolicies().find((p) => p.name === "block-sudo")!;

  it("denies sudo commands", () => {
    const ctx = {
      eventType: "PreToolUse" as const,
      payload: {},
      toolName: "Bash",
      toolInput: { command: "sudo apt install nodejs" },
      params: { allowPatterns: [] },
    };
    expect(policy.fn(ctx)).toEqual(deny("sudo command blocked by failproofai"));
  });

  it("allows non-sudo commands", () => {
    const ctx = {
      eventType: "PreToolUse" as const,
      payload: {},
      toolName: "Bash",
      toolInput: { command: "ls -la" },
      params: { allowPatterns: [] },
    };
    expect(policy.fn(ctx)).toEqual(allow());
  });

  it("allows patterns in allowPatterns", () => {
    const ctx = {
      eventType: "PreToolUse" as const,
      payload: {},
      toolName: "Bash",
      toolInput: { command: "sudo systemctl status nginx" },
      params: { allowPatterns: ["sudo systemctl status"] },
    };
    expect(policy.fn(ctx)).toEqual(allow());
  });
});

End-to-end tests

E2E tests gọi nhị phân failproofai thực làm subprocess, pipe payload JSON tới stdin, và assert stdout output cũng như exit code. Điều này kiểm thử đường dẫn tích hợp hoàn chỉnh mà Claude Code sử dụng.

Setup

E2E tests chạy nhị phân trực tiếp từ nguồn repo. Trước lần chạy đầu tiên, build bundle CJS mà các file custom hook sử dụng khi import từ 'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
Sau đó chạy các kiểm thử:
bun run test:e2e
Rebuild dist/ bất cứ khi nào bạn thay đổi public hook API (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts, hoặc src/hooks/policy-types.ts).

Cấu trúc E2E test

__tests__/e2e/
  helpers/
    hook-runner.ts      # Spawn the binary, pipe payload JSON, capture exit code + stdout + stderr
    fixture-env.ts      # Per-test isolated temp directories with config files
    payloads.ts         # Claude-accurate payload factories for each event type
  hooks/
    builtin-policies.e2e.test.ts   # Each builtin policy with real subprocess
    custom-hooks.e2e.test.ts       # Custom hook loading and evaluation
    config-scopes.e2e.test.ts      # Config merging across project/local/global
    policy-params.e2e.test.ts      # Parameter injection for each parameterized policy

Sử dụng E2E helpers

FixtureEnv - isolated per-test environment:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - temp dir; pass as payload.cwd to pick up .failproofai/policies-config.json
// env.home   - isolated home dir; no real ~/.failproofai leaks in

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() đăng ký cleanup afterEach tự động. runHook - gọi nhị phân:
import { runHook } from "../helpers/hook-runner";
import { Payloads } from "../helpers/payloads";

const result = await runHook(
  "PreToolUse",
  Payloads.preToolUse.bash("sudo apt install nodejs", env.cwd),
  { homeDir: env.home }
);

expect(result.exitCode).toBe(0);
expect(result.parsed?.hookSpecificOutput?.permissionDecision).toBe("deny");
Payloads - ready-made payload factories:
Payloads.preToolUse.bash(command, cwd)
Payloads.preToolUse.write(filePath, content, cwd)
Payloads.preToolUse.read(filePath, cwd)
Payloads.postToolUse.bash(command, output, cwd)
Payloads.postToolUse.read(filePath, content, cwd)
Payloads.notification(message, cwd)
Payloads.stop(cwd)

Viết E2E test

import { describe, it, expect } from "vitest";
import { createFixtureEnv } from "../helpers/fixture-env";
import { runHook } from "../helpers/hook-runner";
import { Payloads } from "../helpers/payloads";

describe("block-rm-rf (E2E)", () => {
  it("denies rm -rf", async () => {
    const env = createFixtureEnv();
    env.writeConfig({ enabledPolicies: ["block-rm-rf"] });

    const result = await runHook(
      "PreToolUse",
      Payloads.preToolUse.bash("rm -rf /", env.cwd),
      { homeDir: env.home }
    );

    expect(result.exitCode).toBe(0);
    expect(result.parsed?.hookSpecificOutput?.permissionDecision).toBe("deny");
  });

  it("allows non-recursive rm", async () => {
    const env = createFixtureEnv();
    env.writeConfig({ enabledPolicies: ["block-rm-rf"] });

    const result = await runHook(
      "PreToolUse",
      Payloads.preToolUse.bash("rm /tmp/file.txt", env.cwd),
      { homeDir: env.home }
    );

    expect(result.exitCode).toBe(0);
    expect(result.stdout).toBe("");  // allow → empty stdout
  });
});

Hình dạng E2E response

DecisionExit codestdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (non-Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2empty stdout; reason in stderr
Allow0empty string

Vitest config

E2E tests sử dụng vitest.config.e2e.mts với:
  • environment: "node" - không cần browser globals
  • pool: "forks" - true process isolation (tests spawn subprocesses)
  • testTimeout: 20_000 - 20s per test (binary startup + hook eval)
Pool forks rất quan trọng: thread-based workers chia sẻ globalThis, điều này có thể gây ảnh hưởng tới các tests spawning subprocess. Process-based forks tránh điều này.

CI

Full CI run (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) được yêu cầu pass trước khi merge. E2E suite chạy như một separate CI job song song. Xem Contributing để biết complete pre-merge checklist.