메인 콘텐츠로 건너뛰기
failproofai에는 두 가지 테스트 스위트가 있습니다: 유닛 테스트 (빠른 실행, 모킹 사용)와 엔드-투-엔드 테스트 (실제 서브프로세스 호출).

테스트 실행

# 유닛 테스트 한 번 실행
bun run test:run

# 유닛 테스트를 워치 모드로 실행
bun run test

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

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

# 린트
bun run lint

유닛 테스트

유닛 테스트는 __tests__/ 디렉터리에 위치하며, jsdom과 함께 Vitest를 사용합니다.
__tests__/
  hooks/
    builtin-policies.test.ts      # 각 빌트인 정책 로직
    hooks-config.test.ts          # 설정 로딩 및 스코프 병합
    policy-evaluator.test.ts      # 파라미터 주입 및 평가 순서
    custom-hooks-registry.test.ts # globalThis 레지스트리 추가/조회/초기화
    custom-hooks-loader.test.ts   # ESM 로더, 전이적 임포트, 오류 처리
    manager.test.ts               # 설치/제거/목록 조회 작업
  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 instruct2stdout 비어 있음; 이유는 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을 참고하세요.