Chuyển đến nội dung chính
failproofai có hai bộ test: unit tests (nhanh, mocked) và end-to-end tests (real subprocess invocations).

Chạy các bài 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 (yêu cầu setup - xem bên dưới)
bun run test:e2e

# Kiểm tra kiểu dữ liệu mà không build
bunx tsc --noEmit

# Lint
bun run lint

Unit tests

Unit tests nằm trong __tests__/ và sử dụng Vitest với happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Logic chính sách cho mỗi builtin
    hooks-config.test.ts          # Tải cấu hình và hợp nhất scope
    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 một policy unit test

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 binary failproofai thực tế như một subprocess, pipe một payload JSON tới stdin, và khẳng định đầu ra stdout và mã thoát. Điều này kiểm tra đường dẫn tích hợp hoàn chỉnh mà Claude Code sử dụng.

Setup

E2E tests chạy binary trực tiếp từ nguồn repo. Trước lần chạy đầu tiên, build CJS bundle mà các tệp custom hook sử dụng khi họ import từ 'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
Sau đó chạy các bài kiểm thử:
bun run test:e2e
Xây dựng lại 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 binary, pipe payload JSON, capture exit code + stdout + stderr
    fixture-env.ts      # Thư mục temp cô lập cho mỗi test với các tệp cấu hình
    payloads.ts         # Claude-accurate payload factories cho mỗi event type
  hooks/
    builtin-policies.e2e.test.ts   # Mỗi builtin policy với real subprocess
    custom-hooks.e2e.test.ts       # Custom hook loading và evaluation
    config-scopes.e2e.test.ts      # Config merging across project/local/global
    policy-params.e2e.test.ts      # Parameter injection cho mỗi parameterized policy

Sử dụng E2E helpers

FixtureEnv - môi trường cô lập cho mỗi test:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - thư mục temp; truyền vào payload.cwd để nhận .failproofai/policies-config.json
// env.home   - thư mục home cô lập; không có rò rỉ ~/.failproofai thực tế

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() đăng ký afterEach cleanup tự động. runHook - gọi binary:
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 - các payload factories có sẵn:
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 mộ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
  });
});

Các hình dạng phản hồi E2E

Quyết địnhMã thoátstdout
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

Cấu hình Vitest

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 cho mỗi test (binary startup + hook eval)
Pool forks rất quan trọng: các worker dựa trên thread chia sẻ globalThis, điều này có thể can thiệp vào các bài kiểm thử spawning subprocess. Forks dựa trên process tránh được điều này.

CI

Việc chạy CI đầy đủ (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) được yêu cầu phải vượt qua trước khi merge. Bộ E2E chạy như một công việc CI riêng biệt song song. Xem Contributing để xem danh sách kiểm tra trước merge đầy đủ.