UNPKG

creatrip-agent-rules-builder

Version:

Unified converter for AI coding agent rules across Cursor, Windsurf, and Claude

298 lines (244 loc) 10.6 kB
import * as fs from "fs"; import * as path from "path"; import { verifyAgents } from "../src/verifiers"; import { normalizeContent, compareContent, extractRulesFromMdc, } from "../src/verifiers/comparator"; describe("Verify Command", () => { describe("Content Normalization", () => { it("정규화: 다양한 줄바꿈 문자를 통일", () => { const content = "line1\r\nline2\rline3\nline4"; const normalized = normalizeContent(content); expect(normalized).toBe("line1\nline2\nline3\nline4"); }); it("정규화: 앞뒤 공백 제거", () => { const content = " \n content \n "; const normalized = normalizeContent(content); expect(normalized).toBe("content"); }); }); describe("MDC Frontmatter Extraction", () => { it("frontmatter가 있는 MDC 파일에서 규칙 추출", () => { const content = `--- description: Test rules globs: *.js,*.ts alwaysApply: false --- # My Rules Rule content here`; const extracted = extractRulesFromMdc(content); expect(extracted).toBe("# My Rules\nRule content here"); }); it("frontmatter가 없는 일반 파일 처리", () => { const content = "# My Rules\nRule content here"; const extracted = extractRulesFromMdc(content); expect(extracted).toBe("# My Rules\nRule content here"); }); it("구분자가 하나만 있는 경우 처리", () => { const content = "---\nSome content"; const extracted = extractRulesFromMdc(content); expect(extracted).toBe("---\nSome content"); }); }); describe("Content Comparison", () => { it("동일한 내용 비교", () => { const source = "# Rules\nContent"; const target = "# Rules\nContent"; expect(compareContent(source, target)).toBe(true); }); it("다른 내용 비교", () => { const source = "# Rules\nContent A"; const target = "# Rules\nContent B"; expect(compareContent(source, target)).toBe(false); }); it("줄바꿈이 다른 경우에도 동일하게 처리", () => { const source = "# Rules\r\nContent"; const target = "# Rules\nContent"; expect(compareContent(source, target)).toBe(true); }); }); describe("Integration Test", () => { const testDir = path.join(__dirname, "test-verify"); const agentsPath = path.join(testDir, "AGENTS.md"); const cursorPath = path.join(testDir, ".cursor", "rules", "rules.mdc"); const windsurfPath = path.join(testDir, ".windsurfrules"); const claudePath = path.join(testDir, "CLAUDE.md"); beforeEach(() => { // 테스트 디렉토리 생성 fs.mkdirSync(testDir, { recursive: true }); fs.mkdirSync(path.join(testDir, ".cursor", "rules"), { recursive: true }); // AGENTS.md 생성 const agentContent = "# Test Rules\nThis is a test rule"; fs.writeFileSync(agentsPath, agentContent); // 동기화된 파일들 생성 fs.writeFileSync( cursorPath, `--- description: Test globs: alwaysApply: false --- ${agentContent}`, ); fs.writeFileSync(windsurfPath, agentContent); fs.writeFileSync(claudePath, agentContent); }); afterEach(() => { // 테스트 디렉토리 정리 fs.rmSync(testDir, { recursive: true, force: true }); }); it("모든 파일이 동기화된 경우", async () => { const result = await verifyAgents(testDir, "json"); expect(result.status).toBe("pass"); expect(result.totalLocations).toBe(1); expect(result.locations).toHaveLength(1); const location = result.locations[0]; expect(location.status).toBe("pass"); expect(location.summary.passed).toBe(3); expect(location.summary.failed).toBe(0); expect(location.agents.cursor.passed).toBe(true); expect(location.agents.windsurf.passed).toBe(true); expect(location.agents.claude.passed).toBe(true); // diagnosis 검증 expect(location.diagnosis).toBeDefined(); expect(location.diagnosis?.pattern).toBe("all_synced"); expect(location.diagnosis?.action).toBe("none"); }); it("단일 파일만 불일치한 경우 (수동 편집 의심)", async () => { // windsurf 파일만 다르게 수정 fs.writeFileSync(windsurfPath, "# Different Content"); const result = await verifyAgents(testDir, "json"); expect(result.status).toBe("fail"); const location = result.locations[0]; expect(location.summary.passed).toBe(2); expect(location.summary.failed).toBe(1); expect(location.agents.windsurf.passed).toBe(false); expect(location.agents.windsurf.error).toBe("content mismatch"); // diagnosis 검증 expect(location.diagnosis?.pattern).toBe("single_diverged"); expect(location.diagnosis?.action).toBe("review"); expect(location.diagnosis?.diverged).toEqual(["windsurf"]); }); it("모든 파일이 불일치한 경우 (build 필요)", async () => { // 모든 파일을 다르게 수정 fs.writeFileSync( cursorPath, "---\ndescription: Test\nglobs: \nalwaysApply: false\n---\n\n# Different", ); fs.writeFileSync(windsurfPath, "# Different Content"); fs.writeFileSync(claudePath, "# Different Content"); const result = await verifyAgents(testDir, "json"); expect(result.status).toBe("fail"); const location = result.locations[0]; expect(location.summary.failed).toBe(3); // diagnosis 검증 expect(location.diagnosis?.pattern).toBe("all_outdated"); expect(location.diagnosis?.action).toBe("build"); }); it("여러 파일이 불일치한 경우 (수동 검토 필요)", async () => { // 2개 파일을 다르게 수정 fs.writeFileSync(windsurfPath, "# Different Content"); fs.writeFileSync(claudePath, "# Different Content"); const result = await verifyAgents(testDir, "json"); expect(result.status).toBe("fail"); const location = result.locations[0]; expect(location.summary.failed).toBe(2); // diagnosis 검증 expect(location.diagnosis?.pattern).toBe("multiple_diverged"); expect(location.diagnosis?.action).toBe("manual"); expect(location.diagnosis?.diverged?.length).toBe(2); expect(location.diagnosis?.diverged).toContain("windsurf"); expect(location.diagnosis?.diverged).toContain("claude"); }); it("파일이 없는 경우", async () => { // claude 파일 삭제 fs.unlinkSync(claudePath); const result = await verifyAgents(testDir, "json"); expect(result.status).toBe("fail"); const location = result.locations[0]; expect(location.agents.claude.passed).toBe(false); expect(location.agents.claude.error).toBe("file not found"); // diagnosis는 여전히 작동해야 함 expect(location.diagnosis?.pattern).toBe("single_diverged"); expect(location.diagnosis?.diverged).toEqual(["claude"]); }); it("AGENTS.md 파일이 없는 경우", async () => { fs.unlinkSync(agentsPath); await expect(verifyAgents(testDir, "json")).rejects.toThrow( "Source file not found", ); }); }); describe("Recursive Mode", () => { const testDir = path.join(__dirname, "test-verify-recursive"); const subDir1 = path.join(testDir, "packages", "web"); const subDir2 = path.join(testDir, "packages", "api"); beforeEach(() => { // 테스트 디렉토리 구조 생성 fs.mkdirSync(subDir1, { recursive: true }); fs.mkdirSync(subDir2, { recursive: true }); // 각 디렉토리에 AGENTS.md와 생성 파일들 생성 const agentContent = "# Test Rules"; // Root 디렉토리 fs.writeFileSync(path.join(testDir, "AGENTS.md"), agentContent); fs.mkdirSync(path.join(testDir, ".cursor", "rules"), { recursive: true }); fs.writeFileSync( path.join(testDir, ".cursor", "rules", "rules.mdc"), `---\ndescription: Test\nglobs: \nalwaysApply: false\n---\n\n${agentContent}`, ); fs.writeFileSync(path.join(testDir, ".windsurfrules"), agentContent); fs.writeFileSync(path.join(testDir, "CLAUDE.md"), agentContent); // SubDir1 fs.writeFileSync(path.join(subDir1, "AGENTS.md"), agentContent); fs.mkdirSync(path.join(subDir1, ".cursor", "rules"), { recursive: true }); fs.writeFileSync( path.join(subDir1, ".cursor", "rules", "rules.mdc"), `---\ndescription: Test\nglobs: \nalwaysApply: false\n---\n\n${agentContent}`, ); fs.writeFileSync(path.join(subDir1, ".windsurfrules"), agentContent); fs.writeFileSync(path.join(subDir1, "CLAUDE.md"), agentContent); // SubDir2 - 일부 불일치 fs.writeFileSync(path.join(subDir2, "AGENTS.md"), agentContent); fs.mkdirSync(path.join(subDir2, ".cursor", "rules"), { recursive: true }); fs.writeFileSync( path.join(subDir2, ".cursor", "rules", "rules.mdc"), `---\ndescription: Test\nglobs: \nalwaysApply: false\n---\n\n${agentContent}`, ); fs.writeFileSync(path.join(subDir2, ".windsurfrules"), "# Different"); fs.writeFileSync(path.join(subDir2, "CLAUDE.md"), agentContent); }); afterEach(() => { fs.rmSync(testDir, { recursive: true, force: true }); }); it("재귀 모드에서 여러 위치 검증", async () => { const result = await verifyAgents(testDir, "json", true); expect(result.totalLocations).toBe(3); expect(result.locations).toHaveLength(3); // Root와 subDir1은 통과, subDir2는 실패 const passedLocations = result.locations.filter( (loc) => loc.status === "pass", ); const failedLocations = result.locations.filter( (loc) => loc.status === "fail", ); expect(passedLocations).toHaveLength(2); expect(failedLocations).toHaveLength(1); // 전체 상태는 fail (하나라도 실패하면) expect(result.status).toBe("fail"); // subDir2의 diagnosis 확인 const failedLocation = failedLocations[0]; expect(failedLocation.diagnosis?.pattern).toBe("single_diverged"); expect(failedLocation.diagnosis?.diverged).toEqual(["windsurf"]); }); it("재귀 모드에서 AGENTS.md 파일이 없을 때", async () => { // 빈 디렉토리 생성 const emptyDir = path.join(testDir, "empty"); fs.mkdirSync(emptyDir, { recursive: true }); await expect(verifyAgents(emptyDir, "json", true)).rejects.toThrow( "No AGENTS.md files found", ); }); }); });