/** * Self-Healing Guard Tests */ import { describe, it, expect, beforeAll, afterAll } from "bun:test"; import { mkdir, writeFile, rm, mkdtemp } from "fs/promises"; import { join } from "path"; import { tmpdir } from "os"; import { checkWithHealing, generateHealing, explainRule, type HealingResult, } from "./healing"; import type { Violation, GuardConfig } from "./types"; // ═══════════════════════════════════════════════════════════════════════════ // Test Setup // ═══════════════════════════════════════════════════════════════════════════ // 임시 디렉토리 사용 (고정 경로 대신) let TEST_DIR: string; beforeAll(async () => { // 임시 디렉토리 생성 (안전한 격리된 테스트 환경) TEST_DIR = await mkdtemp(join(tmpdir(), "test-healing-")); // Create test directory structure await mkdir(join(TEST_DIR, "src", "shared", "utils"), { recursive: true }); await mkdir(join(TEST_DIR, "src", "features", "auth", "model"), { recursive: true }); await mkdir(join(TEST_DIR, "src", "features", "user", "model"), { recursive: true }); await mkdir(join(TEST_DIR, "src", "widgets", "header"), { recursive: true }); // Create test files with violations // Violation 1: Layer violation (features importing widgets) await writeFile( join(TEST_DIR, "src", "features", "auth", "model", "store.ts"), `// Layer violation: features cannot import widgets import { HeaderButton } from '@/widgets/header'; export const authStore = { button: HeaderButton, }; ` ); // Violation 2: Cross-slice (features/auth importing features/user directly) await writeFile( join(TEST_DIR, "src", "features", "auth", "model", "user-ref.ts"), `// Cross-slice violation import { userStore } from '@/features/user/model/store'; export const getUserFromAuth = () => userStore.get(); ` ); // Valid file (no violations) await writeFile( join(TEST_DIR, "src", "features", "auth", "model", "valid.ts"), `// Valid: importing from shared import { formatDate } from '@/shared/utils'; export const formatAuthDate = (date: Date) => formatDate(date); ` ); // Shared utils await writeFile( join(TEST_DIR, "src", "shared", "utils", "index.ts"), `export const formatDate = (date: Date) => date.toISOString(); ` ); // Widgets await writeFile( join(TEST_DIR, "src", "widgets", "header", "index.ts"), `export const HeaderButton = () => ''; ` ); // Features user await writeFile( join(TEST_DIR, "src", "features", "user", "model", "store.ts"), `export const userStore = { get: () => ({ name: 'test' }) }; ` ); }); afterAll(async () => { // Cleanup await rm(TEST_DIR, { recursive: true, force: true }); }); // ═══════════════════════════════════════════════════════════════════════════ // Unit Tests // ═══════════════════════════════════════════════════════════════════════════ describe("Self-Healing Guard", () => { describe("explainRule", () => { it("should explain layer-violation rule", () => { const explanation = explainRule( "layer-violation", "features", "widgets", "fsd" ); expect(explanation.rule).toBe("layer-violation"); expect(explanation.why).toContain("features"); expect(explanation.why).toContain("widgets"); expect(explanation.how).toBeDefined(); expect(explanation.examples.bad).toContain("❌"); expect(explanation.examples.good).toContain("✅"); }); it("should explain circular-dependency rule", () => { const explanation = explainRule( "circular-dependency", "moduleA", "moduleB", "clean" ); expect(explanation.rule).toBe("circular-dependency"); expect(explanation.why).toContain("순환"); expect(explanation.how).toContain("shared"); }); it("should explain cross-slice rule", () => { const explanation = explainRule( "cross-slice", "features", "features", "fsd" ); expect(explanation.rule).toBe("cross-slice"); expect(explanation.why).toContain("슬라이스"); }); it("should explain deep-nesting rule", () => { const explanation = explainRule( "deep-nesting", "pages", "features", "fsd" ); expect(explanation.rule).toBe("deep-nesting"); expect(explanation.why).toContain("내부"); expect(explanation.how).toContain("Public API"); }); }); describe("generateHealing", () => { it("should generate healing options for layer violation", () => { const violation: Violation = { type: "layer-violation", filePath: join(TEST_DIR, "src", "features", "auth", "model", "store.ts"), line: 2, column: 1, importStatement: "import { HeaderButton } from '@/widgets/header';", importPath: "@/widgets/header", fromLayer: "features", toLayer: "widgets", ruleName: "layer-dependency", ruleDescription: "features cannot import from widgets", severity: "error", allowedLayers: ["entities", "shared"], suggestions: ["Move to shared or use props"], }; const config: GuardConfig = { preset: "fsd" }; const healing = generateHealing(violation, config, TEST_DIR); expect(healing.primary).toBeDefined(); expect(healing.primary.label).toBeDefined(); expect(healing.primary.explanation).toBeDefined(); expect(healing.alternatives.length).toBeGreaterThan(0); expect(healing.context.layerHierarchy).toContain("→"); expect(healing.context.allowedLayers).toContain("shared"); }); it("should generate healing options with autoFix for fixable violations", () => { const violation: Violation = { type: "deep-nesting", filePath: join(TEST_DIR, "src", "pages", "home", "index.ts"), line: 1, column: 1, importStatement: "import { X } from '@/features/auth/model/internal/helper';", importPath: "@/features/auth/model/internal/helper", fromLayer: "pages", toLayer: "features", ruleName: "public-api", ruleDescription: "Import from internal path", severity: "warn", allowedLayers: ["features", "widgets", "entities", "shared"], suggestions: ["Use public API"], }; const config: GuardConfig = { preset: "fsd" }; const healing = generateHealing(violation, config, TEST_DIR); // Deep nesting violations should have autoFix const hasAutoFix = healing.primary.autoFix !== undefined || healing.alternatives.some((alt) => alt.autoFix !== undefined); expect(hasAutoFix).toBe(true); }); }); describe("checkWithHealing", () => { it("should return HealingResult with violations and suggestions", async () => { const config: GuardConfig = { preset: "fsd", srcDir: "src", }; // This test may not find violations if the test files aren't properly set up // for the FSD preset. This is more of an integration test placeholder. const result = await checkWithHealing(config, TEST_DIR); expect(result).toBeDefined(); expect(typeof result.totalViolations).toBe("number"); expect(typeof result.autoFixable).toBe("number"); expect(Array.isArray(result.items)).toBe(true); expect(typeof result.filesAnalyzed).toBe("number"); expect(typeof result.analysisTime).toBe("number"); }); }); }); describe("Healing Context", () => { it("should provide correct layer hierarchy for each preset", () => { // 각 프리셋별로 기대되는 계층 구조 키워드 const expectedKeywords: Record = { fsd: "→", // "app → pages → widgets..." clean: "domain", // "api → application → domain..." hexagonal: "ports", // "adapters → ports → domain" atomic: "atoms", // "pages → templates → organisms → molecules → atoms" mandu: "client", // "client(FSD) | shared | server(Clean)" }; for (const [preset, keyword] of Object.entries(expectedKeywords)) { const explanation = explainRule( "layer-violation", "a", "b", preset as "fsd" | "clean" | "hexagonal" | "atomic" | "mandu" ); expect(explanation.why).toContain(keyword); } }); it("should include documentation links", () => { const explanation = explainRule("layer-violation", "features", "widgets", "fsd"); expect(explanation.documentation).toContain("http"); }); });