Passer au contenu principal
failproofai dispose de deux suites de tests : les tests unitaires (rapides, avec mocks) et les tests de bout en bout (invocations réelles de sous-processus).

Lancer les tests

# Lancer tous les tests unitaires une fois
bun run test:run

# Lancer les tests unitaires en mode watch
bun run test

# Lancer les tests E2E (nécessite une configuration - voir ci-dessous)
bun run test:e2e

# Vérification de types sans compilation
bunx tsc --noEmit

# Lint
bun run lint

Tests unitaires

Les tests unitaires se trouvent dans __tests__/ et utilisent Vitest avec happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Logique de politique pour chaque builtin
    hooks-config.test.ts          # Chargement de la config et fusion des scopes
    policy-evaluator.test.ts      # Injection de paramètres et ordre d'évaluation
    custom-hooks-registry.test.ts # Registre globalThis add/get/clear
    custom-hooks-loader.test.ts   # Chargeur ESM, imports transitifs, gestion des erreurs
    manager.test.ts               # Opérations install/remove/list
  components/
    sessions-list.test.tsx        # Composant liste des sessions
    project-list.test.tsx         # Composant liste des projets
    ...
  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

Écrire un test unitaire de politique

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

Tests de bout en bout

Les tests E2E invoquent le vrai binaire failproofai en tant que sous-processus, lui envoient une charge utile JSON via stdin et vérifient la sortie stdout ainsi que le code de sortie. Cela permet de tester le chemin d’intégration complet utilisé par Claude Code.

Configuration

Les tests E2E exécutent le binaire directement depuis les sources du dépôt. Avant la première exécution, compilez le bundle CJS utilisé par les fichiers de hooks personnalisés lorsqu’ils importent depuis 'failproofai' :
bun build src/index.ts --outdir dist --target node --format cjs
Lancez ensuite les tests :
bun run test:e2e
Recompilez dist/ à chaque fois que vous modifiez l’API publique des hooks (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts ou src/hooks/policy-types.ts).

Structure des tests E2E

__tests__/e2e/
  helpers/
    hook-runner.ts      # Lance le binaire, envoie le JSON de la charge utile, capture le code de sortie + stdout + stderr
    fixture-env.ts      # Répertoires temporaires isolés par test avec fichiers de configuration
    payloads.ts         # Factories de charges utiles fidèles à Claude pour chaque type d'événement
  hooks/
    builtin-policies.e2e.test.ts   # Chaque politique builtin avec un vrai sous-processus
    custom-hooks.e2e.test.ts       # Chargement et évaluation des hooks personnalisés
    config-scopes.e2e.test.ts      # Fusion de config entre scopes projet/local/global
    policy-params.e2e.test.ts      # Injection de paramètres pour chaque politique paramétrée

Utiliser les utilitaires E2E

FixtureEnv - environnement isolé par test :
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - répertoire temporaire ; à passer en tant que payload.cwd pour charger .failproofai/policies-config.json
// env.home   - répertoire home isolé ; évite les fuites depuis ~/.failproofai

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() enregistre automatiquement un nettoyage afterEach. runHook - invoquer le binaire :
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 - factories de charges utiles prêtes à l’emploi :
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)

Écrire un test 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 vide
  });
});

Formats de réponse E2E

DécisionCode de sortiestdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (hors Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2stdout vide ; raison dans stderr
Allow0chaîne vide

Configuration Vitest

Les tests E2E utilisent vitest.config.e2e.mts avec :
  • environment: "node" - aucune variable globale de navigateur requise
  • pool: "forks" - véritable isolation des processus (les tests lancent des sous-processus)
  • testTimeout: 20_000 - 20 secondes par test (démarrage du binaire + évaluation des hooks)
Le pool forks est important : les workers basés sur des threads partagent globalThis, ce qui peut interférer avec les tests qui lancent des sous-processus. Les forks basés sur des processus évitent ce problème.

Intégration continue

L’exécution complète en CI (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) doit passer avant toute fusion. La suite E2E s’exécute en tant que job CI distinct, en parallèle. Consultez Contributing pour la liste de vérification complète avant fusion.