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

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

# Запустить все модульные тесты один раз
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 добавления/получения/очистки
    custom-hooks-loader.test.ts   # ESM загрузчик, транзитивные импорты, обработка ошибок
    manager.test.ts               # Операции установки/удаления/списания
  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 тесты вызывают реальный бинарный файл 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, захват кода выхода + 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, что может помешать тестам, запускающим подпроцессы. Процессные fork избегают этой проблемы.

CI

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