creatrip-agent-rules-builder
Version:
Unified converter for AI coding agent rules across Cursor, Windsurf, and Claude
298 lines (244 loc) • 10.6 kB
text/typescript
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",
);
});
});
});