creatrip-agent-rules-builder
Version:
Unified converter for AI coding agent rules across Cursor, Windsurf, and Claude
272 lines (221 loc) • 8.6 kB
text/typescript
import * as fs from "fs";
import * as path from "path";
import { buildRules } from "../src/build";
// console.log를 모킹하여 테스트 출력 방지
jest.spyOn(console, "log").mockImplementation(() => {});
jest.spyOn(console, "error").mockImplementation(() => {});
// process.exit 모킹 - 절대 실제로 exit 하지 않음
const exitMock = jest.spyOn(process, "exit").mockImplementation((code) => {
throw new Error(`Process exited with code ${code}`);
});
describe("Integration Tests", () => {
const testOutputDir = path.join(__dirname, "temp-integration");
const originalCwd = process.cwd();
beforeEach(() => {
if (!fs.existsSync(testOutputDir)) {
fs.mkdirSync(testOutputDir, { recursive: true });
}
process.chdir(testOutputDir);
});
afterEach(() => {
process.chdir(originalCwd);
if (fs.existsSync(testOutputDir)) {
fs.rmSync(testOutputDir, { recursive: true, force: true });
}
});
it("should build all agent rules from AGENTS.md", async () => {
// 테스트 파일 생성
const rulesContent = `
- Integration test rule 1
- Integration test rule 2
\`\`\`jsonc agent-rules-config
{
"cursor": {
"globs": ["*.test.ts"],
"description": "Integration test rules"
},
"windsurf": {},
"claude": {}
}
\`\`\``;
fs.writeFileSync("AGENTS.md", rulesContent);
// 빌드 실행
await buildRules({});
// 생성된 파일들 확인
expect(fs.existsSync(".cursor/rules/rules.mdc")).toBe(true);
expect(fs.existsSync(".windsurfrules")).toBe(true);
expect(fs.existsSync("CLAUDE.md")).toBe(true);
// Cursor 파일 내용 확인
const cursorContent = fs.readFileSync(".cursor/rules/rules.mdc", "utf8");
expect(cursorContent).toContain("---");
expect(cursorContent).toContain("description: Integration test rules");
expect(cursorContent).toContain("globs: *.test.ts");
expect(cursorContent).toContain("# Test Integration Rules");
// Windsurf 파일 내용 확인
const windsurfContent = fs.readFileSync(".windsurfrules", "utf8");
expect(windsurfContent).toContain("# Test Integration Rules");
expect(windsurfContent).not.toContain("agent-rules-config");
// Claude 파일 내용 확인
const claudeContent = fs.readFileSync("CLAUDE.md", "utf8");
expect(claudeContent).toContain("# Test Integration Rules");
expect(claudeContent).not.toContain("agent-rules-config");
});
it("should handle custom file path", async () => {
const customRulesContent = `
- Custom rule`;
fs.writeFileSync("CUSTOM_RULES.md", customRulesContent);
await buildRules({ file: "CUSTOM_RULES.md" });
expect(fs.existsSync(".cursor/rules/rules.mdc")).toBe(true);
expect(fs.existsSync(".windsurfrules")).toBe(true);
expect(fs.existsSync("CLAUDE.md")).toBe(true);
const content = fs.readFileSync(".windsurfrules", "utf8");
expect(content).toContain("# Custom Rules File");
});
it("should throw error for non-existent file", async () => {
await expect(buildRules({ file: "non-existent.md" })).rejects.toThrow();
});
it("should build recursively with multiple AGENTS.md files", async () => {
// 프로젝트 구조 생성
const subDir1 = path.join(testOutputDir, "packages", "frontend");
const subDir2 = path.join(testOutputDir, "packages", "backend");
const nodeModulesDir = path.join(
testOutputDir,
"node_modules",
"some-package",
);
fs.mkdirSync(subDir1, { recursive: true });
fs.mkdirSync(subDir2, { recursive: true });
fs.mkdirSync(nodeModulesDir, { recursive: true });
// 메인 AGENTS.md
const mainRulesContent = `
- Main rule 1
\`\`\`jsonc agent-rules-config
{
"cursor": {
"description": "Main project rules",
"globs": ["*.ts", "*.js"]
}
}
\`\`\``;
// 서브 프로젝트들의 AGENTS.md
const frontendRulesContent = `
- Frontend rule 1
\`\`\`jsonc agent-rules-config
{
"cursor": {
"description": "Frontend rules",
"alwaysApply": true
}
}
\`\`\``;
const backendRulesContent = `
- Backend rule 1`;
// node_modules에도 AGENTS.md (무시되어야 함)
const nodeModulesRulesContent = `
fs.writeFileSync("AGENTS.md", mainRulesContent);
fs.writeFileSync(path.join(subDir1, "AGENTS.md"), frontendRulesContent);
fs.writeFileSync(path.join(subDir2, "AGENTS.md"), backendRulesContent);
fs.writeFileSync(
path.join(nodeModulesDir, "AGENTS.md"),
nodeModulesRulesContent,
);
// 재귀 빌드 실행 - process.exit으로 인한 에러 처리
try {
await buildRules({ recursive: true });
} catch (error: any) {
// process.exit(1)이 호출되었다면 그냥 무시
if (!error.message?.includes("Process exited with code")) {
throw error;
}
}
// 메인 디렉토리 파일들 확인
expect(fs.existsSync(".cursor/rules/rules.mdc")).toBe(true);
expect(fs.existsSync("CLAUDE.md")).toBe(true);
expect(fs.existsSync(".windsurfrules")).toBe(true);
// 서브 디렉토리 파일들 확인
expect(fs.existsSync(path.join(subDir1, ".cursor/rules/rules.mdc"))).toBe(
true,
);
expect(fs.existsSync(path.join(subDir1, "CLAUDE.md"))).toBe(true);
expect(fs.existsSync(path.join(subDir1, ".windsurfrules"))).toBe(true);
expect(fs.existsSync(path.join(subDir2, ".cursor/rules/rules.mdc"))).toBe(
true,
);
expect(fs.existsSync(path.join(subDir2, "CLAUDE.md"))).toBe(true);
expect(fs.existsSync(path.join(subDir2, ".windsurfrules"))).toBe(true);
// node_modules는 무시되어야 함
expect(
fs.existsSync(path.join(nodeModulesDir, ".cursor/rules/rules.mdc")),
).toBe(false);
// 각 파일의 내용 확인
const mainCursorContent = fs.readFileSync(
".cursor/rules/rules.mdc",
"utf8",
);
expect(mainCursorContent).toContain("Main Project Rules");
expect(mainCursorContent).toContain("description: Main project rules");
const frontendCursorContent = fs.readFileSync(
path.join(subDir1, ".cursor/rules/rules.mdc"),
"utf8",
);
expect(frontendCursorContent).toContain("Frontend Rules");
expect(frontendCursorContent).toContain("alwaysApply: true");
const backendClaudeContent = fs.readFileSync(
path.join(subDir2, "CLAUDE.md"),
"utf8",
);
expect(backendClaudeContent).toContain("Backend Rules");
});
it("should handle recursive build with .gitignore exclusions", async () => {
// .gitignore 파일 생성
const gitignoreContent = `
node_modules/
.pnp
temp/
*.log`;
fs.writeFileSync(".gitignore", gitignoreContent);
// 무시되어야 할 디렉토리들
const tempDir = path.join(testOutputDir, "temp");
const nodeModulesDir = path.join(testOutputDir, "node_modules");
fs.mkdirSync(tempDir, { recursive: true });
fs.mkdirSync(nodeModulesDir, { recursive: true });
// 처리되어야 할 디렉토리
const srcDir = path.join(testOutputDir, "src");
fs.mkdirSync(srcDir, { recursive: true });
// AGENTS.md 파일들 생성
fs.writeFileSync("AGENTS.md", "# Root Rules");
fs.writeFileSync(path.join(tempDir, "AGENTS.md"), "# Should be ignored");
fs.writeFileSync(
path.join(nodeModulesDir, "AGENTS.md"),
"# Should be ignored",
);
fs.writeFileSync(path.join(srcDir, "AGENTS.md"), "# Src Rules");
// 재귀 빌드 실행 - process.exit으로 인한 에러 처리
try {
await buildRules({ recursive: true });
} catch (error: any) {
// process.exit(1)이 호출되었다면 그냥 무시
if (!error.message?.includes("Process exited with code")) {
throw error;
}
}
// 루트와 src는 처리되어야 함
expect(fs.existsSync("CLAUDE.md")).toBe(true);
expect(fs.existsSync(path.join(srcDir, "CLAUDE.md"))).toBe(true);
// temp와 node_modules는 무시되어야 함
expect(fs.existsSync(path.join(tempDir, "CLAUDE.md"))).toBe(false);
expect(fs.existsSync(path.join(nodeModulesDir, "CLAUDE.md"))).toBe(false);
});
it("should reject when both file and recursive options are provided", async () => {
fs.writeFileSync("AGENTS.md", "# Test Rules");
await expect(
buildRules({ file: "AGENTS.md", recursive: true }),
).rejects.toThrow("Process exited with code 1");
});
});