> ## 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.

# Thử nghiệm

> Unit tests, E2E tests, và test helpers

failproofai có hai bộ test: **unit tests** (nhanh, mocked) và **end-to-end tests** (gọi subprocess thực tế).

***

## Chạy tests

```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 (yêu cầu thiết lập - xem bên dưới)
bun run test:e2e

# Type-check mà không 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 từng 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 binary `failproofai` thực tế dưới dạng subprocess, pipe payload JSON vào stdin, và kiểm tra stdout output cùng exit code. Điều này test đường dẫn tích hợp hoàn chỉnh mà Claude Code sử dụng.

### Thiết lập

E2E tests chạy binary trực tiếp từ repo source. Trước lần chạy đầu tiên, build CJS bundle mà các tệp 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 tests:

```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 binary, pipe payload JSON, capture exit code + stdout + stderr
    fixture-env.ts      # Per-test isolated temp directories với config files
    payloads.ts         # Claude-accurate payload factories cho mỗi event type
  hooks/
    builtin-policies.e2e.test.ts   # Mỗi builtin policy với real subprocess
    custom-hooks.e2e.test.ts       # Custom hook loading và evaluation
    config-scopes.e2e.test.ts      # Config merging qua project/local/global
    policy-params.e2e.test.ts      # Parameter injection cho mỗi 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 để 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()` tự động đăng ký `afterEach` cleanup.

**`runHook`** - gọi binary:

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

`forks` pool rất quan trọng: thread-based workers chia sẻ `globalThis`, điều này có thể can thiệp vào subprocess-spawning tests. 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`) phải pass trước khi merge. E2E suite chạy như một separate CI job song song.

Xem [Contributing](../CONTRIBUTING.md) để có danh sách pre-merge hoàn chỉnh.
