feat: @uncaged/workflow-role-committer + @uncaged/workflow-role-reviewer
- Committer: git add/commit/push with LLM-generated branch+message - Reviewer: code review role with approval meta - Both use zod@4 schemas, no nerve-core deps - 98 tests pass, biome clean Closes #12 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, spyOn, test } from "bun:test";
|
||||
import { execFile } from "node:child_process";
|
||||
import { appendFile, mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import type { AgentFn, ThreadContext } from "@uncaged/workflow";
|
||||
import { START } from "@uncaged/workflow";
|
||||
import * as workflowRoleLlm from "@uncaged/workflow-role-llm";
|
||||
|
||||
import { createCommitterRole } from "../src/committer.js";
|
||||
import { gitExec } from "../src/git-exec.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
async function git(repo: string, args: string[]): Promise<void> {
|
||||
await gitExec(repo, args);
|
||||
}
|
||||
|
||||
async function setupRepoWithRemote(): Promise<{ repo: string }> {
|
||||
const base = await mkdtemp(join(tmpdir(), "wf-committer-"));
|
||||
const bare = join(base, "origin.git");
|
||||
const repo = join(base, "work");
|
||||
await mkdir(repo, { recursive: true });
|
||||
await mkdir(bare, { recursive: true });
|
||||
await execFileAsync("git", ["init"], { cwd: repo, encoding: "utf8" });
|
||||
await git(repo, ["config", "user.email", "t@t"]);
|
||||
await git(repo, ["config", "user.name", "t"]);
|
||||
await writeFile(join(repo, "README.md"), "# hi\n", "utf8");
|
||||
await git(repo, ["add", "README.md"]);
|
||||
await git(repo, ["commit", "-m", "init"]);
|
||||
await execFileAsync("git", ["init", "--bare"], { cwd: bare, encoding: "utf8" });
|
||||
await git(repo, ["remote", "add", "origin", bare]);
|
||||
await git(repo, ["push", "-u", "origin", "HEAD"]);
|
||||
return { repo };
|
||||
}
|
||||
|
||||
function makeCtx(): ThreadContext {
|
||||
return {
|
||||
start: {
|
||||
role: START,
|
||||
content: "do thing",
|
||||
meta: { maxRounds: 10 },
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
steps: [],
|
||||
};
|
||||
}
|
||||
|
||||
const provider = { baseUrl: "https://example.com/v1", apiKey: "k", model: "m" };
|
||||
|
||||
describe("createCommitterRole", () => {
|
||||
test("returns committed false when working tree clean", async () => {
|
||||
const { repo } = await setupRepoWithRemote();
|
||||
const agent: AgentFn = async () => {
|
||||
throw new Error("agent should not run");
|
||||
};
|
||||
const role = createCommitterRole(
|
||||
agent,
|
||||
{ provider, dryRun: null },
|
||||
{ cwd: repo, remote: "origin", threadId: null },
|
||||
);
|
||||
const out = await role(makeCtx());
|
||||
expect(out.meta.committed).toBe(false);
|
||||
});
|
||||
|
||||
test("dry-run skips pipeline", async () => {
|
||||
const agent: AgentFn = async () => {
|
||||
throw new Error("agent should not run");
|
||||
};
|
||||
const role = createCommitterRole(agent, { provider, dryRun: true });
|
||||
const out = await role(makeCtx());
|
||||
expect(out.content).toBe("[dry-run] committer skipped");
|
||||
expect(out.meta).toEqual({ committed: true });
|
||||
});
|
||||
|
||||
test("commits and pushes when changes exist", async () => {
|
||||
const { repo } = await setupRepoWithRemote();
|
||||
await appendFile(join(repo, "README.md"), "\nmore\n", "utf8");
|
||||
|
||||
const spy = spyOn(workflowRoleLlm, "extractMetaOrThrow").mockResolvedValue({
|
||||
branch: "feat/test-commit",
|
||||
message: "feat: add more",
|
||||
});
|
||||
|
||||
const agent: AgentFn = async () => "plan text";
|
||||
const role = createCommitterRole(
|
||||
agent,
|
||||
{ provider, dryRun: null },
|
||||
{ cwd: repo, remote: "origin", threadId: null },
|
||||
);
|
||||
|
||||
const out = await role(makeCtx());
|
||||
expect(out.meta.committed).toBe(true);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
const branches = await gitExec(repo, ["branch", "--list", "feat/test-commit"]);
|
||||
expect(branches).toContain("feat/test-commit");
|
||||
|
||||
const remoteRefs = await gitExec(repo, ["ls-remote", "--heads", "origin", "feat/test-commit"]);
|
||||
expect(remoteRefs.trim().length).toBeGreaterThan(0);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user