메인 콘텐츠로 건너뛰기
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    - 임시 디렉터리; payload.cwd로 전달하면 .failproofai/policies-config.json을 읽음
// 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을 참고하세요.