179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||
import { createRunner } from "./tier1-command.ts";
|
||
import { ScenarioRunner, type BuildOverride } from "./tier2-scenario.ts";
|
||
import {
|
||
setupTempDir,
|
||
cleanupTempDir,
|
||
getTempDir,
|
||
createFreshProject,
|
||
writeDoc,
|
||
} from "./fixtures.ts";
|
||
import { scanChanges } from "../../src/core/scanner.ts";
|
||
import { validateConfig } from "../../src/core/config.ts";
|
||
import { ConfigError } from "../../src/cli/errors.ts";
|
||
import type { RuneConfig } from "../../src/types.ts";
|
||
|
||
describe("e2e: 文档依赖", () => {
|
||
let runner: ReturnType<typeof createRunner>;
|
||
|
||
beforeEach(async () => {
|
||
await setupTempDir();
|
||
runner = createRunner();
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await cleanupTempDir();
|
||
});
|
||
|
||
it("依赖文档按顺序创建(A → B → C 链式依赖)", async () => {
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [
|
||
{ name: "a", prompt: "文档 A" },
|
||
{ name: "b", prompt: "文档 B", depend: ["a"] },
|
||
{ name: "c", prompt: "文档 C", depend: ["b"] },
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
await createFreshProject();
|
||
await runner.runPlan(getTempDir(), "chain", "a", config);
|
||
|
||
let changes = await scanChanges(getTempDir(), config);
|
||
expect(changes).toHaveLength(1);
|
||
|
||
const chain = changes[0]!;
|
||
const docsA = chain.documents;
|
||
const aDoc = docsA.find((d) => d.name === "a")!;
|
||
const bDoc = docsA.find((d) => d.name === "b")!;
|
||
const cDoc = docsA.find((d) => d.name === "c")!;
|
||
|
||
expect(aDoc.completed).toBe(true);
|
||
expect(aDoc.dependMet).toBe(true);
|
||
expect(bDoc.completed).toBe(false);
|
||
// a.md 已存在,所以 b 的依赖已满足
|
||
expect(bDoc.dependMet).toBe(true);
|
||
expect(cDoc.completed).toBe(false);
|
||
// c 依赖 b,b.md 不存在,所以 dependMet=false
|
||
expect(cDoc.dependMet).toBe(false);
|
||
expect(chain.planCompleted).toBe(false);
|
||
|
||
await runner.runPlan(getTempDir(), "chain", "b", config);
|
||
changes = await scanChanges(getTempDir(), config);
|
||
const chain2 = changes[0]!;
|
||
const docsB = chain2.documents;
|
||
const bDoc2 = docsB.find((d) => d.name === "b")!;
|
||
const cDoc2 = docsB.find((d) => d.name === "c")!;
|
||
|
||
expect(bDoc2.completed).toBe(true);
|
||
expect(bDoc2.dependMet).toBe(true);
|
||
expect(cDoc2.completed).toBe(false);
|
||
// b.md 已存在,c 的依赖现在满足
|
||
expect(cDoc2.dependMet).toBe(true);
|
||
expect(chain2.planCompleted).toBe(false);
|
||
|
||
await runner.runPlan(getTempDir(), "chain", "c", config);
|
||
changes = await scanChanges(getTempDir(), config);
|
||
const chain3 = changes[0]!;
|
||
const docsC = chain3.documents;
|
||
|
||
expect(docsC.every((d) => d.completed)).toBe(true);
|
||
expect(docsC.every((d) => d.dependMet)).toBe(true);
|
||
expect(chain3.planCompleted).toBe(true);
|
||
expect(chain3.buildUnlocked).toBe(true);
|
||
});
|
||
|
||
it("引用不存在文档的依赖被校验拒绝", () => {
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [{ name: "design", prompt: "设计", depend: ["ghost"] }],
|
||
},
|
||
},
|
||
};
|
||
|
||
try {
|
||
validateConfig(config);
|
||
// 不应该走到这里
|
||
expect(true).toBe(false);
|
||
} catch (e) {
|
||
expect(e).toBeInstanceOf(ConfigError);
|
||
expect((e as ConfigError).message).toContain("ghost");
|
||
}
|
||
});
|
||
|
||
it("依赖链断开时 planCompleted 仍为 false", async () => {
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [
|
||
{ name: "design", prompt: "设计" },
|
||
{ name: "task", prompt: "任务", depend: ["design"] },
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
await createFreshProject();
|
||
await writeDoc("broken", "task", "# 任务\n");
|
||
|
||
const changes = await scanChanges(getTempDir(), config);
|
||
expect(changes).toHaveLength(1);
|
||
|
||
const broken = changes[0]!;
|
||
const taskDoc = broken.documents.find((d) => d.name === "task")!;
|
||
const designDoc = broken.documents.find((d) => d.name === "design")!;
|
||
|
||
expect(taskDoc.completed).toBe(true);
|
||
expect(taskDoc.dependMet).toBe(false);
|
||
expect(designDoc.completed).toBe(false);
|
||
expect(broken.planCompleted).toBe(false);
|
||
expect(broken.buildUnlocked).toBe(false);
|
||
});
|
||
|
||
it("依赖满足后才允许 build", async () => {
|
||
const config: RuneConfig = {
|
||
stages: {
|
||
plan: {
|
||
documents: [
|
||
{ name: "design", prompt: "设计" },
|
||
{ name: "task", prompt: "任务", depend: ["design"] },
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
await createFreshProject();
|
||
const baseRunner = createRunner();
|
||
|
||
const buildWithDependCheck: BuildOverride = async (projectDir, changeName, cfg) => {
|
||
const changes = await scanChanges(projectDir, cfg);
|
||
const change = changes.find((c) => c.name === changeName);
|
||
if (!change) throw new Error(`变更 "${changeName}" 不存在`);
|
||
if (!change.planCompleted) {
|
||
throw new Error(`变更 "${changeName}" 的 plan 阶段未完成`);
|
||
}
|
||
return baseRunner.runBuild(projectDir, changeName, cfg);
|
||
};
|
||
|
||
const scenarioRunner = new ScenarioRunner(baseRunner, { build: buildWithDependCheck });
|
||
|
||
await writeDoc("build-dep", "task", "# 任务\n- [ ] do something\n");
|
||
|
||
// 依赖未满足时 build 应抛出错误
|
||
await expect(scenarioRunner.runBuild(getTempDir(), "build-dep", config)).rejects.toThrow(
|
||
"plan 阶段未完成",
|
||
);
|
||
|
||
await runner.runPlan(getTempDir(), "build-dep", "design", config);
|
||
|
||
const changesAfter = await scanChanges(getTempDir(), config);
|
||
expect(changesAfter[0]!.planCompleted).toBe(true);
|
||
|
||
// 依赖满足后 build 应正常执行
|
||
await expect(scenarioRunner.runBuild(getTempDir(), "build-dep", config)).resolves.toBeDefined();
|
||
});
|
||
});
|