c7b0beb6be
- Add RoleDefinition<Meta> = { description, run, schema } to core types
- WorkflowDefinition now carries description and RoleDefinition per role
- Add buildDescriptor() in core to derive WorkflowDescriptor from WorkflowDefinition
- Remove buildDescriptorFromRoles / RoleDescriptorInput from workflow-util-role
- Update solve-issue template, examples, and all tests
小橘 <xiaoju@shazhou.work>
150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
END,
|
|
type RoleStep,
|
|
START,
|
|
type ThreadContext,
|
|
validateWorkflowDescriptor,
|
|
} from "@uncaged/workflow";
|
|
|
|
import { buildSolveIssueDescriptor } from "../src/descriptor.js";
|
|
import { solveIssueModerator } from "../src/moderator.js";
|
|
import { createSolveIssueRoles, type SolveIssueMeta } from "../src/roles.js";
|
|
|
|
function makeStart(maxRounds: number): ThreadContext<SolveIssueMeta>["start"] {
|
|
return {
|
|
role: START,
|
|
content: "Fix the flaky login test",
|
|
meta: { maxRounds },
|
|
timestamp: 0,
|
|
};
|
|
}
|
|
|
|
function makeCtx(
|
|
maxRounds: number,
|
|
steps: ThreadContext<SolveIssueMeta>["steps"],
|
|
): ThreadContext<SolveIssueMeta> {
|
|
return {
|
|
threadId: "01TEST000000000000000000TR",
|
|
start: makeStart(maxRounds),
|
|
steps,
|
|
};
|
|
}
|
|
|
|
function plannerStep(): RoleStep<SolveIssueMeta> {
|
|
return {
|
|
role: "planner",
|
|
content: "plan",
|
|
meta: { plan: "do work", files: ["a.ts"], approach: "minimal fix" },
|
|
timestamp: 1,
|
|
};
|
|
}
|
|
|
|
function coderStep(): RoleStep<SolveIssueMeta> {
|
|
return {
|
|
role: "coder",
|
|
content: "code",
|
|
meta: { filesChanged: ["a.ts"], summary: "fixed" },
|
|
timestamp: 2,
|
|
};
|
|
}
|
|
|
|
function reviewerStep(approved: boolean): RoleStep<SolveIssueMeta> {
|
|
return {
|
|
role: "reviewer",
|
|
content: "rev",
|
|
meta: approved
|
|
? { status: "approved" as const }
|
|
: { status: "rejected" as const, issues: ["needs fix"] },
|
|
timestamp: 3,
|
|
};
|
|
}
|
|
|
|
function committerStep(): RoleStep<SolveIssueMeta> {
|
|
return {
|
|
role: "committer",
|
|
content: "commit",
|
|
meta: { status: "committed", branch: "feat/issue-1", commitSha: "abc1234" },
|
|
timestamp: 4,
|
|
};
|
|
}
|
|
|
|
describe("solveIssueModerator", () => {
|
|
test("routes planner → coder → reviewer → committer → END", () => {
|
|
expect(solveIssueModerator(makeCtx(20, []))).toBe("planner");
|
|
expect(solveIssueModerator(makeCtx(20, [plannerStep()]))).toBe("coder");
|
|
expect(solveIssueModerator(makeCtx(20, [plannerStep(), coderStep()]))).toBe("reviewer");
|
|
expect(solveIssueModerator(makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
|
|
"committer",
|
|
);
|
|
expect(
|
|
solveIssueModerator(
|
|
makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true), committerStep()]),
|
|
),
|
|
).toBe(END);
|
|
});
|
|
|
|
test("reviewer rejects → coder retry when budget allows", () => {
|
|
const steps: ThreadContext<SolveIssueMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(false),
|
|
];
|
|
expect(solveIssueModerator(makeCtx(20, steps))).toBe("coder");
|
|
});
|
|
|
|
test("reviewer rejects → END when max rounds exhausted", () => {
|
|
const steps: ThreadContext<SolveIssueMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(false),
|
|
];
|
|
expect(solveIssueModerator(makeCtx(4, steps))).toBe(END);
|
|
});
|
|
});
|
|
|
|
describe("createSolveIssueRoles", () => {
|
|
test("returns all four role callables", async () => {
|
|
const agent = async () => '{"plan":"x","files":[],"approach":"y"}';
|
|
const roles = createSolveIssueRoles({
|
|
agent,
|
|
workdir: "/tmp/repo",
|
|
extract: null,
|
|
});
|
|
|
|
expect(typeof roles.planner.run).toBe("function");
|
|
expect(typeof roles.coder.run).toBe("function");
|
|
expect(typeof roles.reviewer.run).toBe("function");
|
|
expect(typeof roles.committer.run).toBe("function");
|
|
|
|
const ctx = makeCtx(10, []);
|
|
const plannerOut = await roles.planner.run(ctx as unknown as ThreadContext);
|
|
expect(plannerOut.meta.plan).toBe("");
|
|
expect(Array.isArray(plannerOut.meta.files)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("buildSolveIssueDescriptor", () => {
|
|
test("lists all roles with schemas that validate", () => {
|
|
const descriptor = buildSolveIssueDescriptor();
|
|
const validated = validateWorkflowDescriptor(descriptor);
|
|
expect(validated.ok).toBe(true);
|
|
if (!validated.ok) {
|
|
throw new Error(validated.error);
|
|
}
|
|
expect(Object.keys(validated.value.roles).sort()).toEqual([
|
|
"coder",
|
|
"committer",
|
|
"planner",
|
|
"reviewer",
|
|
]);
|
|
for (const key of ["planner", "coder", "reviewer", "committer"] as const) {
|
|
const role = validated.value.roles[key];
|
|
expect(role).toBeDefined();
|
|
expect(typeof role.schema).toBe("object");
|
|
expect(role.schema).not.toBeNull();
|
|
expect(Array.isArray(role.schema)).toBe(false);
|
|
}
|
|
});
|
|
});
|