Saltar al contenido principal
failproofai cuenta con dos suites de pruebas: pruebas unitarias (rápidas, con mocks) y pruebas end-to-end (invocaciones reales de subprocesos).

Ejecutar las pruebas

# 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 utilizan Vitest con happy-dom.
__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   # Loader 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 para una política

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 end-to-end

Las pruebas E2E invocan el binario real de failproofai como un subproceso, envían un payload JSON a stdin y verifican la salida por stdout y el código de salida. Esto prueba el flujo 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 utilizan los archivos de hooks personalizados cuando importan desde 'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
Luego ejecuta las pruebas:
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

__tests__/e2e/
  helpers/
    hook-runner.ts      # Lanza el binario, envía el payload JSON, captura código de salida + stdout + stderr
    fixture-env.ts      # Directorios temporales aislados por prueba con archivos de configuración
    payloads.ts         # Factorías de payloads precisas para cada tipo de evento de Claude
  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 scopes: project/local/global
    policy-params.e2e.test.ts      # Inyección de parámetros para cada política parametrizada

Uso de los helpers E2E

FixtureEnv — entorno aislado por prueba:
import { createFixtureEnv } from "../helpers/fixture-env";

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

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() registra la limpieza con afterEach automáticamente. runHook — invocar el binario:
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 — factorías de payloads predefinidas:
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

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

Formas de respuesta E2E

DecisiónCódigo de salidastdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (no Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2stdout vacío; motivo en stderr
Allow0cadena vacía

Configuración de Vitest

Las pruebas E2E utilizan 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 (arranque del binario + evaluación del hook)
El pool forks es importante: los workers basados en hilos comparten globalThis, lo que puede interferir con 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 hacer merge. La suite E2E se ejecuta como un job de CI separado en paralelo. Consulta Contributing para ver el checklist completo previo al merge.