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

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

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

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

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

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

# التحقق من الأسلوب
bun run lint

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

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

الإعداد

تشغل اختبارات E2E الملف الثنائي مباشرة من مصدر المستودع. قبل التشغيل الأول، قم ببناء حزمة CJS التي تستخدمها ملفات الخطاف المخصصة عند استيرادها من 'failproofai':
bun build src/index.ts --outdir dist --target node --format cjs
ثم قم بتشغيل الاختبارات:
bun run test:e2e
أعد بناء dist/ كلما غيرت واجهة برمجية عامة للخطاف (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   # كل سياسة مدمجة مع subprocess فعلي
    custom-hooks.e2e.test.ts       # تحميل الخطاف المخصص والتقييم
    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   - مجلد منزلي معزول؛ لا يتسرب ~/.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: ..."}}
Instruct (غير Stop)0{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}
Stop instruct2stdout فارغ؛ السبب في stderr
Allow0سلسلة فارغة

إعداد Vitest

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

CI

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