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

הפעלת בדיקות

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

# הפעלת בדיקות יחידה במצב watch
bun run test

# הפעלת בדיקות E2E (דורש התקנה - ראה למטה)
bun run test:e2e

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

# Lint
bun run lint

בדיקות יחידה

בדיקות היחידה נמצאות ב-__tests__/ ומשתמשות ב-Vitest עם jsdom.
__tests__/
  hooks/
    builtin-policies.test.ts      # לוגיקת מדיניות לכל builtin
    hooks-config.test.ts          # טעינת קונפיגורציה ומיזוג טווח
    policy-evaluator.test.ts      # הזרקת פרמטרים וסדר הערכה
    custom-hooks-registry.test.ts # globalThis registry add/get/clear
    custom-hooks-loader.test.ts   # ESM loader, ייבוא טרנזיטיבי, ניהול שגיאות
    manager.test.ts               # פעולות install/remove/list
  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

כתיבת בדיקת יחידה של מדיניות

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 האמיתי כתהליך משנה, חוצים עומס JSON ל-stdin, ובודקות את פלט stdout וקוד ההחروج. זה בודק את נתיב האינטגרציה המלא שבו Claude Code משתמש.

התקנה

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

מבנה בדיקת E2E

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

שימוש ב-E2E helpers

FixtureEnv - סביבה מבודדת לכל בדיקה:
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 - שדרות הקובץ:
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 - מפעלות עומס מוכנות:
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: ..."}}
הוראה (ללא Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop הוראה2empty stdout; סיבה ב-stderr
אישור0מחרוזת ריקה

תצורת Vitest

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

CI

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