메인 콘텐츠로 건너뛰기
failproofai는 두 가지 테스트 스위트를 제공합니다: 단위 테스트 (빠르고 모킹 기반)와 엔드-투-엔드 테스트 (실제 서브프로세스 호출 기반).

테스트 실행

# 단위 테스트 한 번 실행
bun run test:run

# 단위 테스트 워치 모드로 실행
bun run test

# E2E 테스트 실행 (사전 설정 필요 - 아래 참조)
bun run test:e2e

# 빌드 없이 타입 검사
bunx tsc --noEmit

# 린트
bun run lint

단위 테스트

단위 테스트는 __tests__/ 디렉토리에 위치하며, happy-dom을 사용하는 Vitest로 작성됩니다.
__tests__/
  hooks/
    builtin-policies.test.ts      # 각 빌트인 정책 로직
    hooks-config.test.ts          # 설정 로딩 및 스코프 병합
    policy-evaluator.test.ts      # 파라미터 주입 및 평가 순서
    custom-hooks-registry.test.ts # globalThis 레지스트리 add/get/clear
    custom-hooks-loader.test.ts   # ESM 로더, 전이적 임포트, 오류 처리
    manager.test.ts               # install/remove/list 동작
  components/
    sessions-list.test.tsx        # 세션 목록 컴포넌트
    project-list.test.tsx         # 프로젝트 목록 컴포넌트
    ...
  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

정책 단위 테스트 작성하기

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

엔드-투-엔드 테스트

E2E 테스트는 실제 failproofai 바이너리를 서브프로세스로 실행하고, JSON 페이로드를 stdin으로 파이프한 뒤 stdout 출력과 종료 코드를 검증합니다. 이를 통해 Claude Code가 실제로 사용하는 전체 통합 경로를 테스트합니다.

설정

E2E 테스트는 저장소 소스에서 직접 바이너리를 실행합니다. 첫 실행 전에, 커스텀 훅 파일이 'failproofai'에서 임포트할 때 사용하는 CJS 번들을 빌드해야 합니다:
bun build src/index.ts --outdir dist --target node --format cjs
그런 다음 테스트를 실행합니다:
bun run test:e2e
공개 훅 API(src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts, src/hooks/policy-types.ts)를 변경할 때마다 dist/를 다시 빌드하세요.

E2E 테스트 구조

__tests__/e2e/
  helpers/
    hook-runner.ts      # 바이너리 실행, 페이로드 JSON 파이프, 종료 코드 + stdout + stderr 캡처
    fixture-env.ts      # 테스트별 격리된 임시 디렉토리와 설정 파일
    payloads.ts         # 각 이벤트 타입에 대한 Claude 호환 페이로드 팩토리
  hooks/
    builtin-policies.e2e.test.ts   # 실제 서브프로세스를 사용한 각 빌트인 정책
    custom-hooks.e2e.test.ts       # 커스텀 훅 로딩 및 평가
    config-scopes.e2e.test.ts      # 프로젝트/로컬/글로벌 간 설정 병합
    policy-params.e2e.test.ts      # 파라미터화된 정책에 대한 파라미터 주입

E2E 헬퍼 사용하기

FixtureEnv - 테스트별 격리 환경:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - 임시 디렉토리; .failproofai/policies-config.json을 불러오려면 payload.cwd로 전달
// env.home   - 격리된 홈 디렉토리; 실제 ~/.failproofai가 유입되지 않음

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv()afterEach 정리를 자동으로 등록합니다. runHook - 바이너리 호출:
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 - 미리 준비된 페이로드 팩토리:
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)

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

E2E 응답 형태

결정종료 코드stdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (Stop 제외)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2빈 stdout; 이유는 stderr에 출력
Allow0빈 문자열

Vitest 설정

E2E 테스트는 vitest.config.e2e.mts를 사용하며 다음과 같이 구성됩니다:
  • environment: "node" - 브라우저 전역 변수 불필요
  • pool: "forks" - 진정한 프로세스 격리 (테스트가 서브프로세스를 생성)
  • testTimeout: 20_000 - 테스트당 20초 (바이너리 시작 + 훅 평가)
forks 풀은 중요합니다: 스레드 기반 워커는 globalThis를 공유하므로 서브프로세스를 생성하는 테스트에 영향을 줄 수 있습니다. 프로세스 기반의 포크는 이 문제를 방지합니다.

CI

머지 전에 전체 CI 실행(bun run lint && bunx tsc --noEmit && bun run test:run && bun run build)이 통과되어야 합니다. E2E 스위트는 별도의 CI 작업으로 병렬 실행됩니다. 머지 전 전체 체크리스트는 Contributing을 참조하세요.