> ## Documentation Index
> Fetch the complete documentation index at: https://exosphere-auto-translate-docs-20260623-1106.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Kiểm thử

> Unit tests, E2E tests, và test helpers

failproofai có hai bộ kiểm thử: **unit tests** (nhanh, mocked) và **end-to-end tests** (subprocess thực).

***

## Chạy kiểm thử

```bash theme={null}
# Chạy tất cả unit tests một lần
bun run test:run

# Chạy unit tests ở chế độ watch
bun run test

# Chạy E2E tests (cần setup - xem bên dưới)
bun run test:e2e

# Kiểm tra kiểu mà không cần build
bunx tsc --noEmit

# Lint
bun run lint
```

***

## Unit tests

Unit tests nằm trong `__tests__/` và sử dụng [Vitest](https://vitest.dev) với `jsdom`.

```text theme={null}
__tests__/
  hooks/
    builtin-policies.test.ts      # Logic policy cho mỗi builtin
    hooks-config.test.ts          # Config loading và scope merging
    policy-evaluator.test.ts      # Param injection và evaluation order
    custom-hooks-registry.test.ts # globalThis registry add/get/clear
    custom-hooks-loader.test.ts   # ESM loader, transitive imports, error handling
    manager.test.ts               # install/remove/list operations
  components/
    sessions-list.test.tsx        # Session list component
    project-list.test.tsx         # Project list component
    ...
  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
```

### Viết unit test cho policy

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

***

## End-to-end tests

E2E tests gọi nhị phân `failproofai` thực làm subprocess, pipe payload JSON tới stdin, và assert stdout output cũng như exit code. Điều này kiểm thử đường dẫn tích hợp hoàn chỉnh mà Claude Code sử dụng.

### Setup

E2E tests chạy nhị phân trực tiếp từ nguồn repo. Trước lần chạy đầu tiên, build bundle CJS mà các file custom hook sử dụng khi import từ `'failproofai'`:

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

Sau đó chạy các kiểm thử:

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

Rebuild `dist/` bất cứ khi nào bạn thay đổi public hook API (`src/hooks/custom-hooks-registry.ts`, `src/hooks/policy-helpers.ts`, hoặc `src/hooks/policy-types.ts`).

### Cấu trúc E2E test

```text theme={null}
__tests__/e2e/
  helpers/
    hook-runner.ts      # Spawn the binary, pipe payload JSON, capture exit code + stdout + stderr
    fixture-env.ts      # Per-test isolated temp directories with config files
    payloads.ts         # Claude-accurate payload factories for each event type
  hooks/
    builtin-policies.e2e.test.ts   # Each builtin policy with real subprocess
    custom-hooks.e2e.test.ts       # Custom hook loading and evaluation
    config-scopes.e2e.test.ts      # Config merging across project/local/global
    policy-params.e2e.test.ts      # Parameter injection for each parameterized policy
```

### Sử dụng E2E helpers

**`FixtureEnv`** - isolated per-test environment:

```typescript theme={null}
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - temp dir; pass as payload.cwd to pick up .failproofai/policies-config.json
// env.home   - isolated home dir; no real ~/.failproofai leaks in

env.writeConfig({
  enabledPolicies: ["block-sudo"],
  policyParams: {
    "block-sudo": { allowPatterns: ["sudo systemctl status"] },
  },
});
```

`createFixtureEnv()` đăng ký cleanup `afterEach` tự động.

**`runHook`** - gọi nhị phân:

```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`** - ready-made payload factories:

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

### Viết E2E test

```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 → empty stdout
  });
});
```

### Hình dạng E2E response

| Decision            | Exit code | stdout                                                                                  |
| ------------------- | --------- | --------------------------------------------------------------------------------------- |
| `PreToolUse` deny   | `0`       | `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` |
| `PostToolUse` deny  | `0`       | `{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}`               |
| Instruct (non-Stop) | `0`       | `{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}`      |
| Stop instruct       | `2`       | empty stdout; reason in stderr                                                          |
| Allow               | `0`       | empty string                                                                            |

### Vitest config

E2E tests sử dụng `vitest.config.e2e.mts` với:

* `environment: "node"` - không cần browser globals
* `pool: "forks"` - true process isolation (tests spawn subprocesses)
* `testTimeout: 20_000` - 20s per test (binary startup + hook eval)

Pool `forks` rất quan trọng: thread-based workers chia sẻ `globalThis`, điều này có thể gây ảnh hưởng tới các tests spawning subprocess. Process-based forks tránh điều này.

***

## CI

Full CI run (`bun run lint && bunx tsc --noEmit && bun run test:run && bun run build`) được yêu cầu pass trước khi merge. E2E suite chạy như một separate CI job song song.

Xem [Contributing](../CONTRIBUTING.md) để biết complete pre-merge checklist.
