Pular para o conteúdo principal
failproofai possui duas suítes de testes: testes unitários (rápidos, com mocks) e testes end-to-end (invocações reais de subprocessos).

Executando os testes

# Executar todos os testes unitários uma vez
bun run test:run

# Executar testes unitários em modo watch
bun run test

# Executar testes E2E (requer configuração - veja abaixo)
bun run test:e2e

# Verificar tipos sem compilar
bunx tsc --noEmit

# Lint
bun run lint

Testes unitários

Os testes unitários ficam em __tests__/ e utilizam o Vitest com happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Lógica de políticas para cada builtin
    hooks-config.test.ts          # Carregamento de configuração e mesclagem de escopos
    policy-evaluator.test.ts      # Injeção de parâmetros e ordem de avaliação
    custom-hooks-registry.test.ts # Registro globalThis: add/get/clear
    custom-hooks-loader.test.ts   # Loader ESM, imports transitivos, tratamento de erros
    manager.test.ts               # Operações install/remove/list
  components/
    sessions-list.test.tsx        # Componente de lista de sessões
    project-list.test.tsx         # Componente de lista de projetos
    ...
  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

Escrevendo um teste unitário de 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());
  });
});

Testes end-to-end

Os testes E2E invocam o binário real do failproofai como subprocesso, enviam um payload JSON via stdin e verificam a saída no stdout e o código de saída. Isso testa o caminho completo de integração que o Claude Code utiliza.

Configuração

Os testes E2E executam o binário diretamente do código-fonte do repositório. Antes da primeira execução, compile o bundle CJS que os arquivos de hook customizados usam ao importar de 'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
Em seguida, execute os testes:
bun run test:e2e
Recompile o dist/ sempre que alterar a API pública de hooks (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts ou src/hooks/policy-types.ts).

Estrutura dos testes E2E

__tests__/e2e/
  helpers/
    hook-runner.ts      # Inicia o binário, envia payload JSON, captura código de saída + stdout + stderr
    fixture-env.ts      # Diretórios temporários isolados por teste com arquivos de configuração
    payloads.ts         # Fábricas de payload compatíveis com Claude para cada tipo de evento
  hooks/
    builtin-policies.e2e.test.ts   # Cada política builtin com subprocesso real
    custom-hooks.e2e.test.ts       # Carregamento e avaliação de hooks customizados
    config-scopes.e2e.test.ts      # Mesclagem de configuração entre escopos project/local/global
    policy-params.e2e.test.ts      # Injeção de parâmetros para cada política parametrizada

Usando os utilitários E2E

FixtureEnv - ambiente isolado por teste:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - diretório temporário; passe como payload.cwd para carregar .failproofai/policies-config.json
// env.home   - home isolado; evita vazamento do ~/.failproofai real

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() registra a limpeza via afterEach automaticamente. runHook - invoca o binário:
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 payload prontas para uso:
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)

Escrevendo um teste 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 vazio
  });
});

Formatos de resposta E2E

DecisãoCódigo de saídastdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (não-Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2stdout vazio; motivo no stderr
Allow0string vazia

Configuração do Vitest

Os testes E2E utilizam vitest.config.e2e.mts com:
  • environment: "node" - sem necessidade de globals do navegador
  • pool: "forks" - isolamento real de processos (os testes iniciam subprocessos)
  • testTimeout: 20_000 - 20s por teste (inicialização do binário + avaliação do hook)
O pool forks é importante: workers baseados em threads compartilham globalThis, o que pode interferir com testes que iniciam subprocessos. Forks baseados em processos evitam esse problema.

CI

A execução completa de CI (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) deve passar antes do merge. A suíte E2E é executada como um job de CI separado, em paralelo. Consulte Contributing para o checklist completo de pré-merge.