Chuyển đến nội dung chính
failproofai có hai bộ kiểm thử: kiểm thử đơn vị (nhanh, được mock) và kiểm thử end-to-end (gọi subprocess thực tế).

Chạy kiểm thử

# Chạy tất cả kiểm thử đơn vị một lần
bun run test:run

# Chạy kiểm thử đơn vị ở chế độ watch
bun run test

# Chạy kiểm thử E2E (yêu cầu cài đặt - xem bên dưới)
bun run test:e2e

# Kiểm tra kiểu dữ liệu mà không xây dựng
bunx tsc --noEmit

# Lint
bun run lint

Kiểm thử đơn vị

Các kiểm thử đơn vị 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 config và gộp scope
    policy-evaluator.test.ts      # Tiêm param và thứ tự đánh giá
    custom-hooks-registry.test.ts # Thêm/lấy/xóa registry globalThis
    custom-hooks-loader.test.ts   # Trình tải ESM, import bắc cầu, xử lý lỗi
    manager.test.ts               # Hoạt động install/remove/list
  components/
    sessions-list.test.tsx        # Thành phần danh sách phiên
    project-list.test.tsx         # Thành phần danh sách dự án
    ...
  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 kiểm thử đơn vị cho chính sách

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());
  });
});

Kiểm thử end-to-end

Các kiểm thử E2E gọi binary failproofai thực tế dưới dạng subprocess, pipe payload JSON tới stdin, và kiểm tra kết quả stdout và mã thoát. Điều này kiểm thử đường dẫn tích hợp hoàn chỉnh mà Claude Code sử dụng.

Cài đặt

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

Cấu trúc kiểm thử E2E

__tests__/e2e/
  helpers/
    hook-runner.ts      # Sinh binary, pipe payload JSON, bắt mã thoát + stdout + stderr
    fixture-env.ts      # Thư mục tạm biệt lập cho mỗi kiểm thử với các tệp config
    payloads.ts         # Nhà máy payload chính xác theo Claude cho mỗi loại sự kiện
  hooks/
    builtin-policies.e2e.test.ts   # Mỗi chính sách builtin với subprocess thực
    custom-hooks.e2e.test.ts       # Tải và đánh giá custom hook
    config-scopes.e2e.test.ts      # Gộp config trên project/local/global
    policy-params.e2e.test.ts      # Tiêm tham số cho mỗi chính sách tham số hóa

Sử dụng trình hỗ trợ E2E

FixtureEnv - môi trường biệt lập cho mỗi kiểm thử:
import { createFixtureEnv } from "../helpers/fixture-env";

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

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() đăng ký dọn dẹp afterEach 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 - nhà máy payload 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 kiểm thử E2E

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 → stdout trống
  });
});

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 (không-Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Instruct Stop2stdout trống; lý do trong stderr
Allow0chuỗi trống

Cấu hình Vitest

Các kiểm thử E2E sử dụng vitest.config.e2e.mts với:
  • environment: "node" - không cần globals trình duyệt
  • pool: "forks" - cô lập process thực (kiểm thử sinh subprocess)
  • testTimeout: 20_000 - 20s cho mỗi kiểm thử (khởi động binary + đánh giá hook)
Pool forks rất quan trọng: worker dựa trên luồng chia sẻ globalThis, điều này có thể gây ảnh hưởng đến các kiểm thử sinh subprocess. Các forks dựa trên process tránh điều này.

CI

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ộ kiểm thử E2E chạy như một công việc CI riêng biệt song song. Xem Contributing để biết danh sách kiểm tra trước merge hoàn chỉnh.