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

# テスト

> ユニットテスト、E2Eテスト、テストヘルパー

failproofai には2つのテストスイートがあります：**ユニットテスト**（高速、モック使用）と**エンドツーエンドテスト**（実際のサブプロセス呼び出し）。

***

## テストの実行

```bash theme={null}
# ユニットテストを1回実行
bun run test:run

# ウォッチモードでユニットテストを実行
bun run test

# E2Eテストを実行（事前セットアップが必要 - 下記参照）
bun run test:e2e

# ビルドせずに型チェック
bunx tsc --noEmit

# リント
bun run lint
```

***

## ユニットテスト

ユニットテストは `__tests__/` ディレクトリに配置されており、`jsdom` を使った [Vitest](https://vitest.dev) で動作します。

```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` バイナリをサブプロセスとして起動し、JSON ペイロードを stdin にパイプして、stdout の出力と終了コードをアサートします。これにより、Claude Code が使用する完全な統合パスをテストできます。

### セットアップ

E2Eテストはリポジトリのソースから直接バイナリを実行します。初回実行前に、カスタムフックファイルが `'failproofai'` からインポートする際に使用する CJS バンドルをビルドしてください：

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

その後、テストを実行します：

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

パブリックフック 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       # カスタムフックの読み込みと評価
    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` deny  | `0`   | `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` |
| `PostToolUse` deny | `0`   | `{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}`               |
| Instruct（Stop 以外）  | `0`   | `{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}`      |
| Stop instruct      | `2`   | 空の stdout；理由は stderr に出力                                                                |
| Allow              | `0`   | 空文字列                                                                                    |

### 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](../CONTRIBUTING.md) を参照してください。
