Files
united-workforce/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts
T
xiaoju 2c642b1a53 refactor: committer as pure agent role with discriminated union meta (#17)
- Remove hardcoded git commands (git-exec.ts deleted)
- CommitterMeta is now a discriminated union: committed | failed
- Agent handles git operations, committer extracts structured result
- Moderator can route on meta.status for retry logic

Closes #17
2026-05-06 10:06:27 +00:00

147 lines
4.1 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 {
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 },
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).toBe("function");
expect(typeof roles.coder).toBe("function");
expect(typeof roles.reviewer).toBe("function");
expect(typeof roles.committer).toBe("function");
const ctx = makeCtx(10, []);
const plannerOut = await roles.planner(ctx);
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);
}
});
});