Ana içeriğe atla
failproofai’nin iki test paketi vardır: unit testler (hızlı, mock’lanmış) ve end-to-end testler (gerçek subprocess çağrıları).

Testleri çalıştırma

# Tüm unit testleri bir kez çalıştır
bun run test:run

# Unit testleri watch modunda çalıştır
bun run test

# E2E testlerini çalıştır (setup gerekli - aşağıya bakın)
bun run test:e2e

# Derlenmeden tip kontrolü yap
bunx tsc --noEmit

# Lint kontrolü
bun run lint

Unit testler

Unit testler __tests__/ dizininde yer alır ve Vitest ile happy-dom kullanır.
__tests__/
  hooks/
    builtin-policies.test.ts      # Her builtin için politika mantığı
    hooks-config.test.ts          # Config yüklemesi ve scope birleştirmesi
    policy-evaluator.test.ts      # Param enjeksiyonu ve değerlendirme sırası
    custom-hooks-registry.test.ts # globalThis kayıt defteri add/get/clear
    custom-hooks-loader.test.ts   # ESM loader, geçişli importlar, hata işleme
    manager.test.ts               # install/remove/list işlemleri
  components/
    sessions-list.test.tsx        # Oturum listesi bileşeni
    project-list.test.tsx         # Proje listesi bileşeni
    ...
  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

Politika unit test yazma

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 testler

E2E testler gerçek failproofai binary dosyasını subprocess olarak çağırır, stdin’e JSON payload gönderir ve stdout çıktısı ile çıkış koduna karşı doğrulama yapar. Bu, Claude Code’un kullandığı tam entegrasyon yolunu test eder.

Setup

E2E testler, repo kaynağından doğrudan binary dosyasını çalıştırır. İlk çalıştırmadan önce, custom hook dosyalarının 'failproofai'’den import ettiğinde kullandığı CJS bundle’ını derleyin:
bun build src/index.ts --outdir dist --target node --format cjs
Ardından testleri çalıştırın:
bun run test:e2e
Public hook API’sini değiştirdiğinizde (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts veya src/hooks/policy-types.ts) dist/ dizinini yeniden derleyin.

E2E test yapısı

__tests__/e2e/
  helpers/
    hook-runner.ts      # Binary'yi spawn et, payload JSON'ını pipe et, çıkış kodu + stdout + stderr yakala
    fixture-env.ts      # Test başına izole edilmiş geçici dizinler ve config dosyaları
    payloads.ts         # Her event türü için Claude'a uygun payload factory'leri
  hooks/
    builtin-policies.e2e.test.ts   # Her builtin politika gerçek subprocess ile
    custom-hooks.e2e.test.ts       # Custom hook yükleme ve değerlendirmesi
    config-scopes.e2e.test.ts      # Config birleştirmesi proje/yerel/global arasında
    policy-params.e2e.test.ts      # Parametrelendirilmiş her politika için param enjeksiyonu

E2E yardımcılarını kullanma

FixtureEnv - test başına izole edilmiş ortam:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - geçici dizin; .failproofai/policies-config.json'ı almak için payload.cwd olarak geçin
// env.home   - izole edilmiş home dizini; gerçek ~/.failproofai sızıntısı olmaz

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() afterEach cleanup’ını otomatik olarak kaydeder. runHook - binary dosyasını çağır:
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 - hazır payload factory’leri:
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 test yazma

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 → boş stdout
  });
});

E2E yanıt formatları

KararÇıkış kodustdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (Stop olmayan)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2boş stdout; stderr’de sebep
Allow0boş string

Vitest konfigürasyonu

E2E testler vitest.config.e2e.mts kullanır:
  • environment: "node" - tarayıcı globalleri gerekmez
  • pool: "forks" - gerçek process izolasyonu (testler subprocess’leri spawn eder)
  • testTimeout: 20_000 - test başına 20s (binary startup + hook eval)
forks pool’u önemlidir: thread tabanlı worker’lar globalThis’i paylaşır, bu subprocess-spawning testleriyle müdahale edebilir. Process tabanlı fork’lar bunu önler.

CI

Tam CI çalıştırması (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) merge olmadan geçmelidir. E2E suite paralel olarak ayrı bir CI işi olarak çalışır. Tam merge öncesi kontrol listesi için Contributing bölümüne bakın.