> ## Documentation Index
> Fetch the complete documentation index at: https://exosphere-auto-translate-docs-20260623-1106.mintlify.site/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 : les **tests unitaires** (rapides, avec mocks) et les **tests de bout en bout** (invocations réelles de sous-processus).

***

## Lancer les tests

```bash theme={null}
# Exécuter tous les tests unitaires une seule fois
bun run test:run

# Exécuter les tests unitaires en mode watch
bun run test

# Exécuter les tests E2E (configuration requise — voir ci-dessous)
bun run test:e2e

# Vérifier les types sans compiler
bunx tsc --noEmit

# Lint
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 chaque politique intégrée
    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 de bout en bout

Les tests E2E invoquent le vrai binaire `failproofai` en tant que sous-processus, lui transmettent un payload JSON via stdin et vérifient la sortie stdout ainsi que le code de sortie. Cette approche teste 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 que les fichiers de hooks personnalisés utilisent lors de l'import 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 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

```text theme={null}
__tests__/e2e/
  helpers/
    hook-runner.ts      # Lance le binaire, transmet le payload JSON, capture le code de sortie + stdout + stderr
    fixture-env.ts      # Répertoires temporaires isolés par test avec fichiers de config
    payloads.ts         # Fabriques de payloads fidèles à Claude pour chaque type d'événement
  hooks/
    builtin-policies.e2e.test.ts   # Chaque politique intégrée 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 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 comme payload.cwd pour charger .failproofai/policies-config.json
// env.home   - répertoire home isolé ; aucune fuite depuis le vrai ~/.failproofai

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

`createFixtureEnv()` enregistre automatiquement le 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`** — fabriques 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"` — aucun global de navigateur requis
* `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 essentiel : 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 parallèle dans un job CI distinct.

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