> ## 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.

# Testing

> Pruebas unitarias, pruebas E2E y helpers de prueba

failproofai tiene dos conjuntos de pruebas: **pruebas unitarias** (rápidas, con mocks) y **pruebas de extremo a extremo** (invocaciones reales de subprocesos).

***

## Ejecutar pruebas

```bash theme={null}
# Ejecutar todas las pruebas unitarias una vez
bun run test:run

# Ejecutar pruebas unitarias en modo watch
bun run test

# Ejecutar pruebas E2E (requiere configuración previa - ver más abajo)
bun run test:e2e

# Verificar tipos sin compilar
bunx tsc --noEmit

# Linting
bun run lint
```

***

## Pruebas unitarias

Las pruebas unitarias se encuentran en `__tests__/` y usan [Vitest](https://vitest.dev) con `jsdom`.

```text theme={null}
__tests__/
  hooks/
    builtin-policies.test.ts      # Lógica de políticas para cada builtin
    hooks-config.test.ts          # Carga de configuración y fusión de scopes
    policy-evaluator.test.ts      # Inyección de parámetros y orden de evaluación
    custom-hooks-registry.test.ts # Registro globalThis: add/get/clear
    custom-hooks-loader.test.ts   # Cargador ESM, imports transitivos, manejo de errores
    manager.test.ts               # Operaciones install/remove/list
  components/
    sessions-list.test.tsx        # Componente de lista de sesiones
    project-list.test.tsx         # Componente de lista de proyectos
    ...
  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
```

### Escribir una prueba unitaria de política

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

***

## Pruebas de extremo a extremo

Las pruebas E2E invocan el binario real de `failproofai` como subproceso, envían un payload JSON por stdin y verifican la salida de stdout y el código de salida. Esto prueba el camino de integración completo que utiliza Claude Code.

### Configuración

Las pruebas E2E ejecutan el binario directamente desde el código fuente del repositorio. Antes de la primera ejecución, compila el bundle CJS que usan los archivos de hooks personalizados cuando importan desde `'failproofai'`:

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

Luego ejecuta las pruebas:

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

Recompila `dist/` cada vez que modifiques la API pública de hooks (`src/hooks/custom-hooks-registry.ts`, `src/hooks/policy-helpers.ts` o `src/hooks/policy-types.ts`).

### Estructura de las pruebas E2E

```text theme={null}
__tests__/e2e/
  helpers/
    hook-runner.ts      # Inicia el binario, envía el JSON del payload, captura el código de salida + stdout + stderr
    fixture-env.ts      # Directorios temporales aislados por prueba con archivos de configuración
    payloads.ts         # Fábricas de payloads fieles a Claude para cada tipo de evento
  hooks/
    builtin-policies.e2e.test.ts   # Cada política builtin con subproceso real
    custom-hooks.e2e.test.ts       # Carga y evaluación de hooks personalizados
    config-scopes.e2e.test.ts      # Fusión de configuración entre project/local/global
    policy-params.e2e.test.ts      # Inyección de parámetros para cada política parametrizada
```

### Usar los helpers E2E

**`FixtureEnv`** - entorno aislado por prueba:

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

const env = createFixtureEnv();
// env.cwd    - directorio temporal; pásalo como payload.cwd para cargar .failproofai/policies-config.json
// env.home   - directorio home aislado; evita que se filtre el ~/.failproofai real

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

`createFixtureEnv()` registra la limpieza con `afterEach` automáticamente.

**`runHook`** - invocar el binario:

```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`** - fábricas de payloads predefinidos:

```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)
```

### Escribir una prueba 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 vacío
  });
});
```

### Formatos de respuesta E2E

| Decisión           | Código de salida | stdout                                                                                  |
| ------------------ | ---------------- | --------------------------------------------------------------------------------------- |
| `PreToolUse` deny  | `0`              | `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` |
| `PostToolUse` deny | `0`              | `{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}`               |
| Instruct (no Stop) | `0`              | `{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}`      |
| Stop instruct      | `2`              | stdout vacío; motivo en stderr                                                          |
| Allow              | `0`              | cadena vacía                                                                            |

### Configuración de Vitest

Las pruebas E2E usan `vitest.config.e2e.mts` con:

* `environment: "node"` - no se necesitan globales del navegador
* `pool: "forks"` - aislamiento real de procesos (las pruebas lanzan subprocesos)
* `testTimeout: 20_000` - 20 segundos por prueba (inicio del binario + evaluación del hook)

El pool `forks` es importante: los workers basados en hilos comparten `globalThis`, lo que puede interferir con las pruebas que lanzan subprocesos. Los forks basados en procesos evitan este problema.

***

## CI

La ejecución completa de CI (`bun run lint && bunx tsc --noEmit && bun run test:run && bun run build`) debe pasar antes de fusionar cambios. El conjunto de pruebas E2E se ejecuta como un job de CI independiente en paralelo.

Consulta [Contributing](../CONTRIBUTING.md) para ver la lista de verificación completa previa a la fusión.
