الانتقال إلى المحتوى الرئيسي
يحتوي failproofai على مجموعتي اختبار: اختبارات الوحدة (سريعة، بنماذج محاكاة) واختبارات end-to-end (استدعاءات عمليات فرعية حقيقية).

تشغيل الاختبارات

# تشغيل جميع اختبارات الوحدة مرة واحدة
bun run test:run

# تشغيل اختبارات الوحدة في وضع المراقبة
bun run test

# تشغيل اختبارات E2E (يتطلب الإعداد - انظر أدناه)
bun run test:e2e

# فحص النوع بدون البناء
bunx tsc --noEmit

# فحص الأسلوب
bun run lint

اختبارات الوحدة

تقع اختبارات الوحدة في __tests__/ وتستخدم Vitest مع happy-dom.
__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

كتابة اختبار وحدة للسياسة

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 ملف binary الحقيقي لـ failproofai كعملية فرعية، وتنقل حمولة JSON إلى stdin، وتؤكد على مخرجات stdout وكود الخروج. يختبر هذا مسار التكامل الكامل الذي يستخدمه Claude Code.

الإعداد

تشغل اختبارات E2E ملف binary مباشرة من مصدر المستودع. قبل التشغيل الأول، قم ببناء حزمة 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      # توليد ملف binary وإرسال حمولة JSON وحفظ كود الخروج + stdout + stderr
    fixture-env.ts      # دليل مؤقت معزول لكل اختبار مع ملفات الإعدادات
    payloads.ts         # مصانع حمولة دقيقة لـ Claude لكل نوع حدث
  hooks/
    builtin-policies.e2e.test.ts   # كل سياسة مدمجة مع عملية فرعية حقيقية
    custom-hooks.e2e.test.ts       # تحميل وتقييم hook مخصص
    config-scopes.e2e.test.ts      # دمج الإعدادات عبر المشروع/المحلي/العام
    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 - استدعاء ملف binary:
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: ..."}}
Instruct (non-Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2empty stdout؛ السبب في stderr
Allow0سلسلة فارغة

تكوين Vitest

تستخدم اختبارات E2E vitest.config.e2e.mts مع:
  • environment: "node" - لا توجد متغيرات عامة في المتصفح مطلوبة
  • pool: "forks" - عزل عملية حقيقي (الاختبارات توليد عمليات فرعية)
  • testTimeout: 20_000 - 20 ثانية لكل اختبار (بدء ملف binary + تقييم hook)
حوض forks مهم: العمال القائمون على الخيوط يشاركون globalThis، الذي قد يتعارض مع اختبارات توليد العمليات الفرعية. تجنب forks القائمة على العملية هذا.

CI

التشغيل الكامل للـ CI (bun run lint && bunx tsc --noEmit && bun run test:run && bun run build) مطلوب للنجاح قبل الدمج. تعمل مجموعة E2E كـ CI منفصلة بالتوازي. انظر المساهمة للحصول على قائمة التحقق الكاملة قبل الدمج.