> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 测试

> 单元测试、端到端测试及测试辅助工具

failproofai 包含两套测试套件：**单元测试**（快速、模拟）和**端到端测试**（真实子进程调用）。

***

## 运行测试

```bash theme={null}
# 单次运行所有单元测试
bun run test:run

# 以监视模式运行单元测试
bun run test

# 运行 E2E 测试（需要配置环境，详见下文）
bun run test:e2e

# 类型检查（不构建）
bunx tsc --noEmit

# 代码检查
bun run lint
```

***

## 单元测试

单元测试位于 `__tests__/` 目录，使用 [Vitest](https://vitest.dev) 和 `jsdom`。

```text theme={null}
__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
```

### 编写策略单元测试

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

***

## 端到端测试

E2E 测试将真实的 `failproofai` 二进制文件作为子进程调用，向 stdin 传入 JSON 载荷，并对 stdout 输出和退出码进行断言。这覆盖了 Claude Code 所使用的完整集成路径。

### 环境配置

E2E 测试直接从仓库源码运行二进制文件。在首次运行前，请构建自定义 hook 文件在导入 `'failproofai'` 时所依赖的 CJS 包：

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

然后运行测试：

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

每当你修改公共 hook API（`src/hooks/custom-hooks-registry.ts`、`src/hooks/policy-helpers.ts` 或 `src/hooks/policy-types.ts`）后，请重新构建 `dist/`。

### E2E 测试结构

```text theme={null}
__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       # 自定义 hook 的加载与评估
    config-scopes.e2e.test.ts      # 跨项目/本地/全局的配置合并
    policy-params.e2e.test.ts      # 参数化策略的参数注入
```

### 使用 E2E 辅助工具

**`FixtureEnv`** — 每个测试独立的隔离环境：

```typescript theme={null}
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`** — 调用二进制文件：

```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`** — 内置载荷工厂函数：

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

### 编写 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
  });
});
```

### E2E 响应格式

| 决策               | 退出码 | stdout                                                                                  |
| ---------------- | --- | --------------------------------------------------------------------------------------- |
| `PreToolUse` 拒绝  | `0` | `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` |
| `PostToolUse` 拒绝 | `0` | `{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}`               |
| 指令（非 Stop）       | `0` | `{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}`      |
| Stop 指令          | `2` | stdout 为空；原因输出至 stderr                                                                  |
| 允许               | `0` | 空字符串                                                                                    |

### Vitest 配置

E2E 测试使用 `vitest.config.e2e.mts`，配置如下：

* `environment: "node"` — 无需浏览器全局变量
* `pool: "forks"` — 真正的进程隔离（测试会启动子进程）
* `testTimeout: 20_000` — 每个测试超时 20 秒（包含二进制启动 + hook 评估）

`forks` 池至关重要：基于线程的 worker 共享 `globalThis`，可能干扰需要启动子进程的测试，而基于进程的 forks 可以避免这一问题。

***

## 持续集成

合并前必须通过完整的 CI 流程（`bun run lint && bunx tsc --noEmit && bun run test:run && bun run build`）。E2E 套件作为独立的 CI 任务并行运行。

完整的合并前检查清单请参见 [Contributing](../CONTRIBUTING.md)。
