דלג לתוכן הראשי
failproofai כולל שתי חבילות בדיקה: בדיקות יחידה (מהירות, עם mocked) ובדיקות end-to-end (קריאות subprocess אמיתיות).

הרצת בדיקות

# הרץ את כל בדיקות היחידה פעם אחת
bun run test:run

# הרץ בדיקות יחידה במצב watch
bun run test

# הרץ בדיקות E2E (דורש הגדרה - ראה להלן)
bun run test:e2e

# Type-check ללא בנייה
bunx tsc --noEmit

# Lint
bun run lint

בדיקות יחידה

בדיקות יחידה נמצאות ב-__tests__/ והן משתמשות ב-Vitest עם happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # Logic מדיניות עבור כל builtin
    hooks-config.test.ts          # טעינת config והיתוך scope
    policy-evaluator.test.ts      # Param injection וסדר הערכה
    custom-hooks-registry.test.ts # globalThis registry add/get/clear
    custom-hooks-loader.test.ts   # ESM loader, transitive imports, טיפול בשגיאות
    manager.test.ts               # פעולות install/remove/list
  components/
    sessions-list.test.tsx        # רכיב רשימת sessions
    project-list.test.tsx         # רכיב רשימת projects
    ...
  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());
  });
});

בדיקות end-to-end

בדיקות E2E מפעילות את ה-failproofai הבינארי האמיתי כתת-תהליך, מעביר payload JSON ל-stdin, וקובע את ה-stdout וקוד היציאה. זה בדוק את הנתיב של האינטגרציה המלא שבו Claude Code משתמש.

הגדרה

בדיקות E2E מריצות את הבינארי ישירות מה-source של ה-repo. לפני ההרצה הראשונה, בנה את ה-CJS bundle שקובצי custom hook משתמשים בהם כאשר הם מייבאים מ-'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
ואז הרץ את הבדיקות:
bun run test:e2e
בנה מחדש את dist/ בכל פעם שאתה משנה את public hook API (src/hooks/custom-hooks-registry.ts, src/hooks/policy-helpers.ts, או src/hooks/policy-types.ts).

מבנה בדיקה E2E

__tests__/e2e/
  helpers/
    hook-runner.ts      # Spawn הבינארי, pipe payload JSON, ספור exit code + stdout + stderr
    fixture-env.ts      # temp directories מבודדים per-test עם קובצי config
    payloads.ts         # Claude-accurate payload factories עבור כל event type
  hooks/
    builtin-policies.e2e.test.ts   # כל מדיניות builtin עם subprocess אמיתי
    custom-hooks.e2e.test.ts       # טעינה והערכה של custom hook
    config-scopes.e2e.test.ts      # היתוך config על פני project/local/global
    policy-params.e2e.test.ts      # Param injection עבור כל מדיניות parametrized

שימוש ב-E2E helpers

FixtureEnv - סביבה מבודדת per-test:
import { createFixtureEnv } from "../helpers/fixture-env";

const env = createFixtureEnv();
// env.cwd    - temp dir; העבר כ-payload.cwd כדי להרים את .failproofai/policies-config.json
// env.home   - isolated home dir; אין דליפה של ~/.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 - factories payload מוכנות:
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 → empty stdout
  });
});

צורות תגובה E2E

החלטהקוד יציאהstdout
PreToolUse deny0{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}
PostToolUse deny0{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}
Instruct (non-Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2empty stdout; reason in stderr
Allow0empty string

תצורת Vitest

בדיקות E2E משתמשות ב-vitest.config.e2e.mts עם:
  • environment: "node" - אין צורך בגלובלים של דפדפן
  • pool: "forks" - isolation true process (בדיקות spawn subprocesses)
  • testTimeout: 20_000 - 20s per test (binary startup + hook eval)
ה-forks pool חשוב: עובדים המבוססים על thread משתפים globalThis, שיכול להפריע לבדיקות spawning-subprocess. Process-based forks להימנע מזה.

CI

הרצת CI המלאה (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) נדרשת להעביר לפני merge. חבילת E2E רצה כעבודת CI נפרדת במקביל. ראה Contributing לרשימת הבדיקה המוקדמת המלאה למיזוג.