d63d58ccb5
- Move 15 old workflow-* packages to legacy-packages/ (inactive, preserved for reference)
- Rename templates/ → examples/ for clarity
- Rewrite docs/architecture.md to reflect current uwf architecture
- Active packages remain in packages/: cli-uwf, uwf-agent-hermes, uwf-agent-kit, uwf-moderator, uwf-protocol, workflow-util
小橘 🍊(NEKO Team)
263 lines
8.3 KiB
TypeScript
263 lines
8.3 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { tableToModerator } from "@uncaged/workflow-protocol/moderator-table.js";
|
|
import { validateWorkflowDescriptor } from "@uncaged/workflow-register";
|
|
import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime";
|
|
import { buildDevelopDescriptor } from "../src/descriptor.js";
|
|
import { developTable } from "../src/moderator.js";
|
|
import type { CommitterMeta, PlannerMeta } from "../src/roles/index.js";
|
|
import type { DevelopMeta } from "../src/roles.js";
|
|
|
|
const developModerator = tableToModerator(developTable);
|
|
|
|
type PlannedMeta = Extract<PlannerMeta, { status: "planned" }>;
|
|
|
|
const DEFAULT_PHASES: PlannedMeta["phases"] = [
|
|
{
|
|
hash: "4KNMR2PX",
|
|
title: "Do the work",
|
|
},
|
|
];
|
|
|
|
function makeStart(): ModeratorContext<DevelopMeta>["start"] {
|
|
return {
|
|
role: START,
|
|
content: "Implement the feature",
|
|
meta: {},
|
|
timestamp: 0,
|
|
parentState: null,
|
|
};
|
|
}
|
|
|
|
function makeCtx(steps: ModeratorContext<DevelopMeta>["steps"]): ModeratorContext<DevelopMeta> {
|
|
return {
|
|
threadId: "01TEST000000000000000000TR",
|
|
depth: 0,
|
|
bundleHash: "TESTHASH00001",
|
|
start: makeStart(),
|
|
steps,
|
|
};
|
|
}
|
|
|
|
function plannerStep(phases: PlannedMeta["phases"] = DEFAULT_PHASES): RoleStep<DevelopMeta> {
|
|
return {
|
|
role: "planner",
|
|
contentHash: "STUBHASHPLANNER001",
|
|
meta: { status: "planned" as const, phases },
|
|
refs: phases.map((p) => p.hash),
|
|
timestamp: 1,
|
|
};
|
|
}
|
|
|
|
function coderStep(completedPhase = "4KNMR2PX"): RoleStep<DevelopMeta> {
|
|
return {
|
|
role: "coder",
|
|
contentHash: "STUBHASHCODER00001",
|
|
meta: { completedPhase, filesChanged: ["a.ts"], summary: "implemented" },
|
|
refs: [completedPhase],
|
|
timestamp: 2,
|
|
};
|
|
}
|
|
|
|
function reviewerStep(approved: boolean): RoleStep<DevelopMeta> {
|
|
return {
|
|
role: "reviewer",
|
|
contentHash: "STUBHASHREVIEWER01",
|
|
meta: approved
|
|
? { status: "approved" as const }
|
|
: { status: "rejected" as const, issues: ["needs fix"] },
|
|
refs: [],
|
|
timestamp: 3,
|
|
};
|
|
}
|
|
|
|
function testerStep(passed: boolean): RoleStep<DevelopMeta> {
|
|
return {
|
|
role: "tester",
|
|
contentHash: "STUBHASHTESTER01",
|
|
meta: passed
|
|
? { status: "passed" as const, details: "all checks passed" }
|
|
: { status: "failed" as const, details: "lint failed" },
|
|
refs: [],
|
|
timestamp: 4,
|
|
};
|
|
}
|
|
|
|
function committerStep(meta: CommitterMeta): RoleStep<DevelopMeta> {
|
|
return {
|
|
role: "committer",
|
|
contentHash: "STUBHASHCOMMITTER1",
|
|
meta,
|
|
refs: [],
|
|
timestamp: 5,
|
|
};
|
|
}
|
|
|
|
describe("developModerator", () => {
|
|
test("routes initial → planner → coder → reviewer → tester → committer → END", () => {
|
|
expect(developModerator(makeCtx([]))).toBe("planner");
|
|
expect(developModerator(makeCtx([plannerStep()]))).toBe("coder");
|
|
expect(developModerator(makeCtx([plannerStep(), coderStep()]))).toBe("reviewer");
|
|
expect(developModerator(makeCtx([plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
|
|
"tester",
|
|
);
|
|
expect(
|
|
developModerator(makeCtx([plannerStep(), coderStep(), reviewerStep(true), testerStep(true)])),
|
|
).toBe("committer");
|
|
expect(
|
|
developModerator(
|
|
makeCtx([
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(true),
|
|
testerStep(true),
|
|
committerStep({ status: "committed", branch: "feat/x", commitSha: "abc1234" }),
|
|
]),
|
|
),
|
|
).toBe(END);
|
|
});
|
|
|
|
test("reviewer rejects → coder retry when budget allows", () => {
|
|
const steps: ModeratorContext<DevelopMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(false),
|
|
];
|
|
expect(developModerator(makeCtx(steps))).toBe("coder");
|
|
});
|
|
|
|
test("reviewer rejects → coder retry (supervisor controls termination)", () => {
|
|
const steps: ModeratorContext<DevelopMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(false),
|
|
];
|
|
expect(developModerator(makeCtx(steps))).toBe("coder");
|
|
});
|
|
|
|
test("tester failed → coder retry when budget allows", () => {
|
|
const steps: ModeratorContext<DevelopMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(true),
|
|
testerStep(false),
|
|
];
|
|
expect(developModerator(makeCtx(steps))).toBe("coder");
|
|
});
|
|
|
|
test("tester failed → coder retry (supervisor controls termination)", () => {
|
|
const steps: ModeratorContext<DevelopMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(true),
|
|
testerStep(false),
|
|
];
|
|
expect(developModerator(makeCtx(steps))).toBe("coder");
|
|
});
|
|
|
|
test("multiple planner phases → coder until all complete, then reviewer", () => {
|
|
const phases: PlannedMeta["phases"] = [
|
|
{ hash: "AA000001", title: "first phase" },
|
|
{ hash: "AA000002", title: "second phase" },
|
|
];
|
|
expect(developModerator(makeCtx([plannerStep(phases)]))).toBe("coder");
|
|
expect(developModerator(makeCtx([plannerStep(phases), coderStep("AA000001")]))).toBe("coder");
|
|
expect(
|
|
developModerator(
|
|
makeCtx([plannerStep(phases), coderStep("AA000001"), coderStep("AA000002")]),
|
|
),
|
|
).toBe("reviewer");
|
|
});
|
|
|
|
test("one-shot coder reports only last phase hash → reviewer (moderator treats as all phases done)", () => {
|
|
const phases: PlannedMeta["phases"] = [
|
|
{ hash: "BB000001", title: "setup branch" },
|
|
{ hash: "BB000002", title: "write tests" },
|
|
{ hash: "BB000003", title: "verify" },
|
|
{ hash: "BB000004", title: "polish" },
|
|
];
|
|
expect(developModerator(makeCtx([plannerStep(phases), coderStep("BB000004")]))).toBe(
|
|
"reviewer",
|
|
);
|
|
});
|
|
|
|
test("unrecognised completedPhase hash → coder retry when budget allows", () => {
|
|
const phases: PlannedMeta["phases"] = [
|
|
{ hash: "CC000001", title: "first phase" },
|
|
{ hash: "CC000002", title: "second phase" },
|
|
];
|
|
expect(developModerator(makeCtx([plannerStep(phases), coderStep("all-done")]))).toBe("coder");
|
|
});
|
|
|
|
test("incomplete phases → coder retry (supervisor controls termination)", () => {
|
|
const phases: PlannedMeta["phases"] = [
|
|
{ hash: "DD000001", title: "first phase" },
|
|
{ hash: "DD000002", title: "second phase" },
|
|
];
|
|
const steps: ModeratorContext<DevelopMeta>["steps"] = [
|
|
plannerStep(phases),
|
|
coderStep("DD000001"),
|
|
];
|
|
expect(developModerator(makeCtx(steps))).toBe("coder");
|
|
});
|
|
|
|
test("planner aborted → END", () => {
|
|
const abortedStep: RoleStep<DevelopMeta> = {
|
|
role: "planner",
|
|
contentHash: "STUBHASHABORT001",
|
|
meta: { status: "aborted", reason: "No workspace path provided" },
|
|
refs: [],
|
|
timestamp: 1,
|
|
};
|
|
expect(developModerator(makeCtx([abortedStep]))).toBe("__end__");
|
|
});
|
|
|
|
test("committer → END for any committer meta status", () => {
|
|
const committed = committerStep({ status: "committed", branch: "f", commitSha: "x" });
|
|
const recoverable = committerStep({
|
|
status: "recoverable",
|
|
error: "merge conflict",
|
|
logRef: null,
|
|
});
|
|
const unrecoverable = committerStep({
|
|
status: "unrecoverable",
|
|
error: "repo missing",
|
|
logRef: "log1",
|
|
});
|
|
const base: ModeratorContext<DevelopMeta>["steps"] = [
|
|
plannerStep(),
|
|
coderStep(),
|
|
reviewerStep(true),
|
|
testerStep(true),
|
|
];
|
|
expect(developModerator(makeCtx([...base, committed]))).toBe(END);
|
|
expect(developModerator(makeCtx([...base, recoverable]))).toBe(END);
|
|
expect(developModerator(makeCtx([...base, unrecoverable]))).toBe(END);
|
|
});
|
|
});
|
|
|
|
describe("buildDevelopDescriptor", () => {
|
|
test("lists all roles with schemas that validate", () => {
|
|
const descriptor = buildDevelopDescriptor();
|
|
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",
|
|
"tester",
|
|
]);
|
|
expect(validated.value.graph.edges.length).toBeGreaterThan(0);
|
|
for (const key of ["planner", "coder", "reviewer", "tester", "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);
|
|
}
|
|
});
|
|
});
|