/** * Decision Memory Tests */ import { describe, it, expect, beforeAll, afterAll } from "bun:test"; import { mkdir, rm, mkdtemp, readFile } from "fs/promises"; import { join } from "path"; import { tmpdir } from "os"; import { parseADRMarkdown, formatADRAsMarkdown, getAllDecisions, getDecisionById, searchDecisions, saveDecision, checkConsistency, generateCompactArchitecture, getNextDecisionId, type ArchitectureDecision, } from "./decision-memory"; // ═══════════════════════════════════════════════════════════════════════════ // Test Setup // ═══════════════════════════════════════════════════════════════════════════ let TEST_DIR: string; const SAMPLE_ADR_CONTENT = `# Use JWT for API Authentication **ID:** ADR-001 **Status:** accepted **Date:** 2024-01-15 **Tags:** auth, api, security, jwt ## Context API 인증 방식을 결정해야 합니다. 세션 기반과 토큰 기반 중 선택이 필요합니다. ## Decision JWT (JSON Web Token) + Refresh Token 조합을 사용합니다. - Access Token: 15분 만료 - Refresh Token: 7일 만료, Redis에 저장 ## Consequences - 토큰 만료 관리가 필요합니다 - Redis 세션 저장소가 필요합니다 - Stateless 아키텍처 유지 가능 ## Related Decisions - ADR-002 `; const SAMPLE_ADR_CONTENT_2 = `# Use Redis for Session Storage **ID:** ADR-002 **Status:** accepted **Date:** 2024-01-16 **Tags:** cache, session, redis, infrastructure ## Context 세션 및 캐시 저장소 선택이 필요합니다. ## Decision Redis를 통합 캐시/세션 저장소로 사용합니다. server/infra/cache/를 통해서만 접근합니다. ## Consequences - 직접 Redis 클라이언트 사용 금지 - 캐시 추상화 레이어 필요 ## Related Decisions - ADR-001 `; beforeAll(async () => { // 임시 디렉토리 생성 TEST_DIR = await mkdtemp(join(tmpdir(), "test-decision-memory-")); // spec/decisions 디렉토리 생성 await mkdir(join(TEST_DIR, "spec", "decisions"), { recursive: true }); // 샘플 ADR 파일 생성 await Bun.write( join(TEST_DIR, "spec", "decisions", "ADR-001-jwt-auth.md"), SAMPLE_ADR_CONTENT ); await Bun.write( join(TEST_DIR, "spec", "decisions", "ADR-002-redis-session.md"), SAMPLE_ADR_CONTENT_2 ); // package.json 생성 (프로젝트 이름용) await Bun.write( join(TEST_DIR, "package.json"), JSON.stringify({ name: "test-project" }, null, 2) ); }); afterAll(async () => { await rm(TEST_DIR, { recursive: true, force: true }); }); // ═══════════════════════════════════════════════════════════════════════════ // Unit Tests // ═══════════════════════════════════════════════════════════════════════════ describe("Decision Memory", () => { describe("parseADRMarkdown", () => { it("should parse ADR markdown correctly", () => { const result = parseADRMarkdown(SAMPLE_ADR_CONTENT, "ADR-001-jwt-auth.md"); expect(result).not.toBeNull(); expect(result!.id).toBe("ADR-001"); expect(result!.title).toBe("Use JWT for API Authentication"); expect(result!.status).toBe("accepted"); expect(result!.date).toBe("2024-01-15"); expect(result!.tags).toContain("auth"); expect(result!.tags).toContain("jwt"); expect(result!.context).toContain("API 인증 방식"); expect(result!.decision).toContain("JWT"); expect(result!.consequences.length).toBeGreaterThan(0); expect(result!.relatedDecisions).toContain("ADR-002"); }); it("should handle malformed content gracefully", () => { const result = parseADRMarkdown("# Simple Title\n\nSome content", "test.md"); expect(result).not.toBeNull(); expect(result!.title).toBe("Simple Title"); }); }); describe("formatADRAsMarkdown", () => { it("should format decision as markdown", () => { const decision: ArchitectureDecision = { id: "ADR-003", title: "Test Decision", status: "proposed", date: "2024-02-01", tags: ["test", "example"], context: "This is the context", decision: "This is the decision", consequences: ["Consequence 1", "Consequence 2"], relatedDecisions: ["ADR-001"], }; const markdown = formatADRAsMarkdown(decision); expect(markdown).toContain("# Test Decision"); expect(markdown).toContain("**ID:** ADR-003"); expect(markdown).toContain("**Status:** proposed"); expect(markdown).toContain("test, example"); expect(markdown).toContain("This is the context"); expect(markdown).toContain("- Consequence 1"); expect(markdown).toContain("- ADR-001"); }); }); describe("getAllDecisions", () => { it("should load all decisions from spec/decisions", async () => { const decisions = await getAllDecisions(TEST_DIR); expect(decisions.length).toBe(2); expect(decisions[0].id).toBe("ADR-001"); expect(decisions[1].id).toBe("ADR-002"); }); it("should return empty array for non-existent directory", async () => { const emptyDir = await mkdtemp(join(tmpdir(), "empty-")); const decisions = await getAllDecisions(emptyDir); expect(decisions).toEqual([]); await rm(emptyDir, { recursive: true, force: true }); }); }); describe("getDecisionById", () => { it("should find decision by ID", async () => { const decision = await getDecisionById(TEST_DIR, "ADR-001"); expect(decision).not.toBeNull(); expect(decision!.title).toBe("Use JWT for API Authentication"); }); it("should return null for non-existent ID", async () => { const decision = await getDecisionById(TEST_DIR, "ADR-999"); expect(decision).toBeNull(); }); }); describe("searchDecisions", () => { it("should find decisions by tags", async () => { const result = await searchDecisions(TEST_DIR, ["auth"]); expect(result.decisions.length).toBe(1); expect(result.decisions[0].id).toBe("ADR-001"); }); it("should find decisions by multiple tags", async () => { const result = await searchDecisions(TEST_DIR, ["auth", "cache"]); expect(result.decisions.length).toBe(2); }); it("should return empty for non-matching tags", async () => { const result = await searchDecisions(TEST_DIR, ["nonexistent"]); expect(result.decisions.length).toBe(0); }); it("should perform partial tag matching", async () => { const result = await searchDecisions(TEST_DIR, ["sec"]); // should match "security" expect(result.decisions.length).toBeGreaterThan(0); }); }); describe("saveDecision", () => { it("should save new decision as markdown file", async () => { // Local tempdir so the added ADR-003 does not leak into the shared // TEST_DIR used by getAllDecisions / searchDecisions / …, which // assert exact counts or specific ADR contents under --randomize. const localDir = await mkdtemp(join(tmpdir(), "save-decision-test-")); try { await mkdir(join(localDir, "spec", "decisions"), { recursive: true }); const newDecision: Omit = { id: "ADR-003", title: "Use Feature Flags", status: "proposed", tags: ["feature", "deployment"], context: "Need controlled rollout", decision: "Use feature flags for gradual rollout", consequences: ["Need flag management system"], }; const result = await saveDecision(localDir, newDecision); expect(result.success).toBe(true); expect(result.filePath).toContain("ADR-003"); // 파일이 실제로 생성되었는지 확인 const content = await readFile(result.filePath, "utf-8"); expect(content).toContain("Use Feature Flags"); } finally { await rm(localDir, { recursive: true, force: true }); } }); }); describe("checkConsistency", () => { it("should find related decisions for consistency check", async () => { const result = await checkConsistency(TEST_DIR, "Add caching layer", ["cache"]); expect(result.relatedDecisions.length).toBeGreaterThan(0); expect(result.suggestions.length).toBeGreaterThan(0); }); it("should return consistent=true when no warnings", async () => { const result = await checkConsistency(TEST_DIR, "New feature", ["auth"]); // accepted 상태이므로 warning 없음 expect(result.consistent).toBe(true); }); }); describe("generateCompactArchitecture", () => { it("should generate compact architecture summary", async () => { const compact = await generateCompactArchitecture(TEST_DIR); expect(compact.project).toBe("test-project"); expect(compact.keyDecisions.length).toBeGreaterThan(0); expect(compact.tagCounts["auth"]).toBeGreaterThan(0); }); }); describe("getNextDecisionId", () => { it("should return next sequential ID", async () => { // Use an isolated tempdir so other tests in this suite (which assert // on the shared TEST_DIR's ADR count) are not polluted by the fixture // this test needs. const localDir = await mkdtemp(join(tmpdir(), "next-id-test-")); try { await mkdir(join(localDir, "spec", "decisions"), { recursive: true }); await Bun.write( join(localDir, "spec", "decisions", "ADR-001-a.md"), "**ID:** ADR-001\n" ); await Bun.write( join(localDir, "spec", "decisions", "ADR-002-b.md"), "**ID:** ADR-002\n" ); await Bun.write( join(localDir, "spec", "decisions", "ADR-003-c.md"), "**ID:** ADR-003\n" ); const nextId = await getNextDecisionId(localDir); expect(nextId).toBe("ADR-004"); } finally { await rm(localDir, { recursive: true, force: true }); } }); it("should return ADR-001 for empty project", async () => { const emptyDir = await mkdtemp(join(tmpdir(), "empty-decisions-")); const nextId = await getNextDecisionId(emptyDir); expect(nextId).toBe("ADR-001"); await rm(emptyDir, { recursive: true, force: true }); }); }); });