> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tests

> Tests unitaires, tests E2E et utilitaires de test

failproofai dispose de deux suites de tests : des **tests unitaires** (rapides, avec mocks) et des **tests end-to-end** (invocations réelles de sous-processus).

***

## Exécuter les tests

```bash theme={null}
# 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érifier les types sans compilation
bunx tsc --noEmit

# Linter
bun run lint
```

***

## Tests unitaires

Les tests unitaires se trouvent dans `__tests__/` et utilisent [Vitest](https://vitest.dev) avec `jsdom`.

```text theme={null}
__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

```typescript theme={null}
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 end-to-end

Les tests E2E invoquent le véritable binaire `failproofai` en tant que sous-processus, envoient un payload JSON sur stdin et vérifient la sortie stdout ainsi que le code de sortie. Ils testent le chemin d'intégration complet qu'utilise Claude Code.

### Configuration

Les tests E2E exécutent le binaire directement depuis les sources du dépôt. Avant le premier lancement, compilez le bundle CJS que les fichiers de hooks personnalisés utilisent lors de leurs imports depuis `'failproofai'` :

```bash theme={null}
bun build src/index.ts --outdir dist --target node --format cjs
```

Puis lancez les tests :

```bash theme={null}
bun run test:e2e
```

Recompilez `dist/` à chaque modification de 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

```text theme={null}
__tests__/e2e/
  helpers/
    hook-runner.ts      # Démarre le binaire, envoie le JSON du payload, capture le code de sortie + stdout + stderr
    fixture-env.ts      # Répertoires temporaires isolés par test avec fichiers de config
    payloads.ts         # Factories de payloads 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 la config entre les 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 :

```typescript theme={null}
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - répertoire temporaire ; à passer en payload.cwd pour charger .failproofai/policies-config.json
// env.home   - répertoire home isolé ; aucune fuite de ~/.failproofai réel

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
```

`createFixtureEnv()` enregistre automatiquement un nettoyage via `afterEach`.

**`runHook`** - invoquer le binaire :

```typescript theme={null}
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 payloads prêtes à l'emploi :

```typescript theme={null}
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

```typescript theme={null}
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écision             | Code de sortie | stdout                                                                                  |
| -------------------- | -------------- | --------------------------------------------------------------------------------------- |
| `PreToolUse` deny    | `0`            | `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` |
| `PostToolUse` deny   | `0`            | `{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}`               |
| Instruct (hors Stop) | `0`            | `{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}`      |
| Stop instruct        | `2`            | stdout vide ; raison dans stderr                                                        |
| Allow                | `0`            | chaîne vide                                                                             |

### Configuration Vitest

Les tests E2E utilisent `vitest.config.e2e.mts` avec :

* `environment: "node"` - aucune variable globale navigateur requise
* `pool: "forks"` - isolation réelle des processus (les tests lancent des sous-processus)
* `testTimeout: 20_000` - 20 secondes par test (démarrage du binaire + évaluation du hook)

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 de la CI (`bun run lint && bunx tsc --noEmit && bun run test:run && bun run build`) doit réussir avant toute fusion. La suite E2E s'exécute en tant que job CI séparé, en parallèle.

Consultez [Contributing](../CONTRIBUTING.md) pour la liste de vérification complète avant fusion.
