Zum Hauptinhalt springen
failproofai verfügt über zwei Test-Suites: Unit-Tests (schnell, gemockt) und End-to-End-Tests (echte Subprocess-Aufrufe).

Tests ausführen

# Alle Unit-Tests einmalig ausführen
bun run test:run

# Unit-Tests im Watch-Modus ausführen
bun run test

# E2E-Tests ausführen (erfordert Setup – siehe unten)
bun run test:e2e

# Typprüfung ohne Build
bunx tsc --noEmit

# Lint
bun run lint

Unit-Tests

Unit-Tests befinden sich in __tests__/ und verwenden Vitest mit happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Policy-Logik für jeden eingebauten Policy
    hooks-config.test.ts          # Konfigurationsladen und Scope-Zusammenführung
    policy-evaluator.test.ts      # Parameter-Injection und Auswertungsreihenfolge
    custom-hooks-registry.test.ts # globalThis-Registry add/get/clear
    custom-hooks-loader.test.ts   # ESM-Loader, transitive Imports, Fehlerbehandlung
    manager.test.ts               # install/remove/list-Operationen
  components/
    sessions-list.test.tsx        # Sitzungslisten-Komponente
    project-list.test.tsx         # Projektlisten-Komponente
    ...
  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

Einen Policy-Unit-Test schreiben

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-Tests

E2E-Tests rufen die echte failproofai-Binary als Subprocess auf, leiten einen JSON-Payload über stdin weiter und prüfen die stdout-Ausgabe sowie den Exit-Code. Damit wird der vollständige Integrationspfad getestet, den Claude Code verwendet.

Setup

E2E-Tests führen die Binary direkt aus dem Repository-Quellcode aus. Vor dem ersten Durchlauf muss das CJS-Bundle erstellt werden, das von Custom-Hook-Dateien verwendet wird, wenn sie aus 'failproofai' importieren:
bun build src/index.ts --outdir dist --target node --format cjs
Anschließend die Tests ausführen:
bun run test:e2e
dist/ muss neu gebaut werden, wenn Änderungen an der öffentlichen Hook-API vorgenommen werden (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts oder src/hooks/policy-types.ts).

E2E-Teststruktur

__tests__/e2e/
  helpers/
    hook-runner.ts      # Binary starten, Payload-JSON weiterleiten, Exit-Code + stdout + stderr erfassen
    fixture-env.ts      # Pro-Test isolierte temporäre Verzeichnisse mit Konfigurationsdateien
    payloads.ts         # Claude-konforme Payload-Factories für jeden Event-Typ
  hooks/
    builtin-policies.e2e.test.ts   # Jede eingebaute Policy mit echtem Subprocess
    custom-hooks.e2e.test.ts       # Laden und Auswertung von Custom Hooks
    config-scopes.e2e.test.ts      # Konfigurationsmerge über project/local/global
    policy-params.e2e.test.ts      # Parameter-Injection für jede parametrisierte Policy

Die E2E-Hilfsmittel verwenden

FixtureEnv – isolierte Umgebung pro Test:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - temporäres Verzeichnis; als payload.cwd übergeben, um .failproofai/policies-config.json einzulesen
// env.home   - isoliertes Home-Verzeichnis; kein echtes ~/.failproofai wird eingelesen

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() registriert die afterEach-Bereinigung automatisch. runHook – Binary aufrufen:
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 – vorgefertigte Payload-Factories:
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)

Einen E2E-Test schreiben

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

E2E-Antwortformate

EntscheidungExit-Codestdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (nicht Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2leerer stdout; Grund in stderr
Allow0leerer String

Vitest-Konfiguration

E2E-Tests verwenden vitest.config.e2e.mts mit:
  • environment: "node" – keine Browser-Globals erforderlich
  • pool: "forks" – echte Prozessisolierung (Tests starten Subprocesse)
  • testTimeout: 20_000 – 20 Sekunden pro Test (Binary-Start + Hook-Auswertung)
Der forks-Pool ist wichtig: Thread-basierte Worker teilen sich globalThis, was zu Interferenzen mit Subprocess-startenden Tests führen kann. Prozessbasierte Forks vermeiden dies.

CI

Der vollständige CI-Durchlauf (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) muss vor dem Mergen erfolgreich abgeschlossen sein. Die E2E-Suite läuft als separater CI-Job parallel dazu. Siehe Contributing für die vollständige Checkliste vor dem Mergen.