feat: 状态扫描器及测试
This commit is contained in:
55
src/core/scanner.ts
Normal file
55
src/core/scanner.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { readdir, stat, readFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import type { ChangeStatus } from "../types.ts";
|
||||||
|
import { getChangesDir, getArchiveDir } from "./config.ts";
|
||||||
|
import { parseTasks } from "./task-parser.ts";
|
||||||
|
|
||||||
|
export async function scanChanges(
|
||||||
|
projectRoot: string,
|
||||||
|
): Promise<ChangeStatus[]> {
|
||||||
|
const changesDir = getChangesDir(projectRoot);
|
||||||
|
const results: ChangeStatus[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await readdir(changesDir);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryPath = join(changesDir, entry);
|
||||||
|
const entryStat = await stat(entryPath);
|
||||||
|
if (!entryStat.isDirectory()) continue;
|
||||||
|
|
||||||
|
const docs = await readdir(entryPath);
|
||||||
|
const documents = docs.filter((d) => d.endsWith(".md"));
|
||||||
|
|
||||||
|
let taskProgress: { completed: number; total: number } | null = null;
|
||||||
|
const taskFile = docs.find((d) => d === "task.md");
|
||||||
|
if (taskFile) {
|
||||||
|
const content = await readFile(join(entryPath, taskFile), "utf-8");
|
||||||
|
const tasks = parseTasks(content);
|
||||||
|
taskProgress = {
|
||||||
|
completed: tasks.filter((t) => t.checked).length,
|
||||||
|
total: tasks.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ name: entry, documents, taskProgress });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scanArchives(projectRoot: string): Promise<string[]> {
|
||||||
|
const archiveDir = getArchiveDir(projectRoot);
|
||||||
|
try {
|
||||||
|
const entries = await readdir(archiveDir);
|
||||||
|
const dirs: string[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryStat = await stat(join(archiveDir, entry));
|
||||||
|
if (entryStat.isDirectory()) dirs.push(entry);
|
||||||
|
}
|
||||||
|
return dirs;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
79
tests/core/scanner.test.ts
Normal file
79
tests/core/scanner.test.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
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");
|
||||||
|
expect(changes[0].documents).toContain("design.md");
|
||||||
|
expect(changes[0].documents).toContain("task.md");
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user