דלג לתוכן הראשי

title: בדיקות description: “בדיקות יחידה, בדיקות End-to-End וסיוע בבדיקות” icon: flask-vial

failproofai כולל שתי חבילות בדיקות: בדיקות יחידה (מהירות, ממוקדות) ובדיקות End-to-End (הפעלות subprocess אמיתיות).

הרצת בדיקות

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

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

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

# בדיקת סוגים ללא בנייה
bunx tsc --noEmit

# Lint
bun run lint

בדיקות יחידה

בדיקות יחידה נמצאות ב-__tests__/ והן משתמשות ב-Vitest עם happy-dom.
__tests__/
  hooks/
    builtin-policies.test.ts      # לוגיקת מדיניות לכל built-in
    hooks-config.test.ts          # טעינת הקונפיגורציה ומיזוג scope
    policy-evaluator.test.ts      # הזרקת פרמטרים וסדר הערכה
    custom-hooks-registry.test.ts # ניהול registry של globalThis
    custom-hooks-loader.test.ts   # טוען ESM, 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 כ-subprocess, משדרות payload JSON ל-stdin, ובודקות את הפלט של stdout וקוד היציאה. זה בודק את נתיב האינטגרציה המלא שבו Claude Code משתמש.

הגדרה

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

מבנה בדיקת E2E

__tests__/e2e/
  helpers/
    hook-runner.ts      # הצא את הקובץ ההשראה, שדר payload JSON, תפוס קוד יציאה + stdout + stderr
    fixture-env.ts      # תיקיות זמניות מבודדות לכל בדיקה עם קבצי קונפיגורציה
    payloads.ts         # Factory payloads מדויקים ל-Claude לכל סוג event
  hooks/
    builtin-policies.e2e.test.ts   # כל מדיניות built-in עם subprocess אמיתי
    custom-hooks.e2e.test.ts       # טעינה והערכה של custom hook
    config-scopes.e2e.test.ts      # מיזוג קונפיגורציה בין project/local/global
    policy-params.e2e.test.ts      # הזרקת פרמטרים לכל מדיניות עם פרמטרים

שימוש בסיוע E2E

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

const env = createFixtureEnv();
// env.cwd    - תיקייה זמנית; העבר כ-payload.cwd כדי לקטוף את .failproofai/policies-config.json
// env.home   - תיקייה home מבודדת; אין חדירה של ~/.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 - factory payloads מוכנים:
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 instruct2stdout ריק; reason ב-stderr
Allow0מחרוזת ריקה

קונפיגורציית Vitest

בדיקות E2E משתמשות ב-vitest.config.e2e.mts עם:
  • environment: "node" - אין צורך בגלובלים של דפדפן
  • pool: "forks" - בידוד תהליך אמיתי (בדיקות מצאות subprocesses)
  • testTimeout: 20_000 - 20 שניות לכל בדיקה (הפעלת קובץ השראה + hook eval)
ה-forks pool חשוב: workerים מבוססי thread חולקים globalThis, שיכול להשפיע על בדיקות שמצאות subprocesses. forks מבוססי process מונעים זאת.

CI

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