Files
Rune-Spec/tests/agent/e2e-depend.test.ts

179 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { createRunner } from "./agent-mock.ts";
import { ScenarioRunner, type BuildOverride } from "./agent-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 依赖 bb.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();
});
});