import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { scanChanges, scanArchives } from "../../src/core/scanner.ts"; import type { RuneConfig } from "../../src/types.ts"; const TMP_DIR = join(import.meta.dir, "__tmp_scanner_test__"); beforeEach(async () => { await mkdir(TMP_DIR, { recursive: true }); }); afterEach(async () => { await rm(TMP_DIR, { recursive: true, force: true }); }); describe("scanChanges", () => { it("无 changes 目录时返回空数组", async () => { await mkdir(join(TMP_DIR, ".rune"), { recursive: true }); const changes = await scanChanges(TMP_DIR); expect(changes).toEqual([]); }); it("扫描到变更及其文档", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "user-auth"), { recursive: true }); await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计"); await writeFile( join(changesDir, "user-auth", "task.md"), `- [x] 任务一\n- [ ] 任务二`, ); const changes = await scanChanges(TMP_DIR); expect(changes).toHaveLength(1); expect(changes[0].name).toBe("user-auth"); const docNames = changes[0].documents.map((d) => `${d.name}.md`); expect(docNames).toContain("design.md"); expect(docNames).toContain("task.md"); expect(changes[0].planCompleted).toBe(false); expect(changes[0].buildUnlocked).toBe(false); expect(changes[0].taskProgress).toEqual({ completed: 1, total: 2 }); }); it("无 task.md 时 taskProgress 为 null", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "feature-a"), { recursive: true }); await writeFile(join(changesDir, "feature-a", "design.md"), "# 设计"); const changes = await scanChanges(TMP_DIR); expect(changes).toHaveLength(1); expect(changes[0].taskProgress).toBeNull(); }); it("扫描多个并行变更", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "auth"), { recursive: true }); await mkdir(join(changesDir, "payment"), { recursive: true }); await writeFile(join(changesDir, "auth", "task.md"), `- [ ] 任务`); await writeFile(join(changesDir, "payment", "task.md"), `- [x] 任务`); const changes = await scanChanges(TMP_DIR); expect(changes).toHaveLength(2); }); it("返回 DocumentStatus 含 completed 和 dependMet", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "user-auth"), { recursive: true }); await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计"); const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计" }, { name: "task", prompt: "生成任务", depend: ["design"] }, ], }, }, }; const changes = await scanChanges(TMP_DIR, config); expect(changes).toHaveLength(1); expect(changes[0].documents).toHaveLength(2); expect(changes[0].documents[0]).toEqual({ name: "design", completed: true, dependMet: true, }); expect(changes[0].documents[1]).toEqual({ name: "task", completed: false, dependMet: true, }); expect(changes[0].planCompleted).toBe(false); expect(changes[0].buildUnlocked).toBe(false); }); it("depend 未满足时 dependMet 为 false", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "user-auth"), { recursive: true }); const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计" }, { name: "task", prompt: "生成任务", depend: ["design"] }, ], }, }, }; const changes = await scanChanges(TMP_DIR, config); expect(changes[0].documents[1].dependMet).toBe(false); }); it("所有文档完成时 planCompleted 和 buildUnlocked 为 true", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "user-auth"), { recursive: true }); await writeFile(join(changesDir, "user-auth", "design.md"), "# 设计"); await writeFile(join(changesDir, "user-auth", "task.md"), "- [ ] 任务"); const config: RuneConfig = { stages: { plan: { documents: [ { name: "design", prompt: "生成设计" }, { name: "task", prompt: "生成任务" }, ], }, }, }; const changes = await scanChanges(TMP_DIR, config); expect(changes[0].planCompleted).toBe(true); expect(changes[0].buildUnlocked).toBe(true); }); it("无 config 时使用文件扫描兼容模式", async () => { const changesDir = join(TMP_DIR, ".rune", "changes"); await mkdir(join(changesDir, "feature-a"), { recursive: true }); await writeFile(join(changesDir, "feature-a", "design.md"), "# 设计"); const changes = await scanChanges(TMP_DIR); expect(changes).toHaveLength(1); expect(changes[0].documents[0].name).toBe("design"); expect(changes[0].documents[0].completed).toBe(true); expect(changes[0].planCompleted).toBe(false); expect(changes[0].buildUnlocked).toBe(false); }); }); describe("scanArchives", () => { it("无 archive 目录时返回空数组", async () => { await mkdir(join(TMP_DIR, ".rune"), { recursive: true }); const archives = await scanArchives(TMP_DIR); expect(archives).toEqual([]); }); it("扫描归档目录", async () => { const archiveDir = join(TMP_DIR, ".rune", "archive"); await mkdir(join(archiveDir, "2026-06-08-user-auth"), { recursive: true }); await mkdir(join(archiveDir, "2026-06-09-payment"), { recursive: true }); const archives = await scanArchives(TMP_DIR); expect(archives).toHaveLength(2); expect(archives).toContain("2026-06-08-user-auth"); expect(archives).toContain("2026-06-09-payment"); }); });