Перейти к основному содержанию
failproofai имеет два набора тестов: модульные тесты (быстрые, с mock-объектами) и сквозные тесты (реальные вызовы подпроцессов).

Запуск тестов

# Запустить все модульные тесты один раз
bun run test:run

# Запустить модульные тесты в режиме наблюдения
bun run test

# Запустить E2E тесты (требует подготовки - см. ниже)
bun run test:e2e

# Проверка типов без сборки
bunx tsc --noEmit

# Линтинг
bun run lint

Модульные тесты

Модульные тесты находятся в __tests__/ и используют Vitest с happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Логика политик для каждой встроенной
    hooks-config.test.ts          # Загрузка конфига и объединение областей видимости
    policy-evaluator.test.ts      # Инъекция параметров и порядок вычисления
    custom-hooks-registry.test.ts # Реестр globalThis add/get/clear
    custom-hooks-loader.test.ts   # ESM загрузчик, транзитивные импорты, обработка ошибок
    manager.test.ts               # Операции install/remove/list
  components/
    sessions-list.test.tsx        # Компонент списка сессий
    project-list.test.tsx         # Компонент списка проектов
    ...
  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

Написание модульного теста политики

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

Сквозные тесты (E2E)

E2E тесты запускают реальный бинарный файл failproofai как подпроцесс, передают JSON полезную нагрузку в stdin и проверяют вывод в stdout и код выхода. Это тестирует полный путь интеграции, который использует Claude Code.

Подготовка

E2E тесты запускают бинарный файл непосредственно из исходного кода репозитория. Перед первым запуском соберите CJS бандл, который используют файлы пользовательских хуков при импорте из 'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
Затем запустите тесты:
bun run test:e2e
Пересобирайте dist/ каждый раз, когда вы изменяете публичный API хуков (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts или src/hooks/policy-types.ts).

Структура E2E тестов

__tests__/e2e/
  helpers/
    hook-runner.ts      # Запуск бинарного файла, передача JSON в stdin, перехват кода выхода + stdout + stderr
    fixture-env.ts      # Изолированные по тестам временные директории с файлами конфига
    payloads.ts         # Фабрики полезных нагрузок, совместимые с Claude, для каждого типа события
  hooks/
    builtin-policies.e2e.test.ts   # Каждая встроенная политика с реальным подпроцессом
    custom-hooks.e2e.test.ts       # Загрузка и вычисление пользовательских хуков
    config-scopes.e2e.test.ts      # Объединение конфига по проектам/локальным/глобальным областям
    policy-params.e2e.test.ts      # Инъекция параметров для каждой параметризованной политики

Использование E2E помощников

FixtureEnv - изолированная среда для каждого теста:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - временная директория; передается как payload.cwd для подхвата .failproofai/policies-config.json
// env.home   - изолированная домашняя директория; реальный ~/.failproofai не утечет

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
createFixtureEnv() автоматически регистрирует очистку afterEach. runHook - запуск бинарного файла:
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 - готовые фабрики полезных нагрузок:
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)

Написание 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
  });
});

Форматы ответов E2E

РешениеКод выходаstdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (не-Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2пустой stdout; причина в stderr
Allow0пустая строка

Конфигурация Vitest

E2E тесты используют vitest.config.e2e.mts с:
  • environment: "node" - браузерные глобалы не требуются
  • pool: "forks" - настоящая изоляция процессов (тесты запускают подпроцессы)
  • testTimeout: 20_000 - 20 сек на тест (запуск бинарного файла + вычисление хука)
Пул forks важен: рабочие потоки с общей памятью совместно используют globalThis, что может помешать тестам, которые запускают подпроцессы. Процессные форки этого избегают.

CI

Полный прогон CI (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) должен пройти перед слиянием. E2E набор запускается как отдельная задача CI в параллель. Смотрите Contributing для полного предварительного чеклиста слияния.