232 lines
8.3 KiB
TypeScript
232 lines
8.3 KiB
TypeScript
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 config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [
|
||
{ name: "design", prompt: "生成设计" },
|
||
{ name: "task", prompt: "生成任务" },
|
||
],
|
||
},
|
||
},
|
||
metadata: { tracked: true },
|
||
};
|
||
const changes = await scanChanges(TMP_DIR, config);
|
||
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(true);
|
||
expect(changes[0].buildUnlocked).toBe(true);
|
||
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);
|
||
});
|
||
|
||
it("tracked=false 时不扫描 task.md,taskProgress 为 null", async () => {
|
||
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||
await mkdir(join(changesDir, "test-change"), { recursive: true });
|
||
await writeFile(join(changesDir, "test-change", "design.md"), "# 设计");
|
||
await writeFile(join(changesDir, "test-change", "task.md"), "- [x] 完成\n- [ ] 未完成");
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [{ name: "design", prompt: "生成设计" }],
|
||
},
|
||
},
|
||
metadata: { tracked: false },
|
||
};
|
||
const results = await scanChanges(TMP_DIR, config);
|
||
expect(results[0].taskProgress).toBeNull();
|
||
});
|
||
|
||
it("tracked=true 时扫描 task.md,taskProgress 有值", async () => {
|
||
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||
await mkdir(join(changesDir, "test-change2"), { recursive: true });
|
||
await writeFile(join(changesDir, "test-change2", "design.md"), "# 设计");
|
||
await writeFile(join(changesDir, "test-change2", "task.md"), "- [x] 完成\n- [ ] 未完成");
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [
|
||
{ name: "design", prompt: "生成设计" },
|
||
{ name: "task", prompt: "生成任务" },
|
||
],
|
||
},
|
||
},
|
||
metadata: { tracked: true },
|
||
};
|
||
const results = await scanChanges(TMP_DIR, config);
|
||
expect(results[0].taskProgress).toEqual({ completed: 1, total: 2 });
|
||
});
|
||
|
||
it("tracked 未配置(undefined)时不扫描 task.md", async () => {
|
||
const changesDir = join(TMP_DIR, ".rune", "changes");
|
||
await mkdir(join(changesDir, "test-change3"), { recursive: true });
|
||
await writeFile(join(changesDir, "test-change3", "design.md"), "# 设计");
|
||
await writeFile(join(changesDir, "test-change3", "task.md"), "- [x] 完成");
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [{ name: "design", prompt: "生成设计" }],
|
||
},
|
||
},
|
||
};
|
||
const results = await scanChanges(TMP_DIR, config);
|
||
expect(results[0].taskProgress).toBeNull();
|
||
});
|
||
});
|
||
|
||
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");
|
||
});
|
||
});
|