/** * @file Tests for server/lib/agent-edits.js (issue #86): the files a * subagent wrote or edited are read from its own transcript — +/- lines from * `structuredPatch` or a created file's content, a failed call left out, a * nested subagent folded in — and put on the parent's Agent result as `edits`; * and `turnCarry` hands a page of the chat the edits its cut-off first turn * made before the page began. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agent-edits-")); process.env.CLAUDE_HOME = HOME; const { encodeCwd } = require("../lib/claude-home"); const { agentEdits, attachAgentEdits, turnCarry, _cache } = require("../lib/agent-edits"); const CWD = "/work/repo"; const SID = "11111111-2222-3333-4444-555555555555"; const subDir = path.join(HOME, "projects", encodeCwd(CWD), SID, "subagents"); const call = (id, name, input) => ({ type: "assistant", message: { content: [{ type: "tool_use", id, name, input }] }, }); const result = (id, toolUseResult, isError = false) => ({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: id, is_error: isError }] }, toolUseResult, }); const write = (agentId, lines) => fs.writeFileSync( path.join(subDir, `agent-${agentId}.jsonl`), lines.map((l) => JSON.stringify(l)).join("\n") ); before(() => { fs.mkdirSync(subDir, { recursive: true }); write("a1", [ call("t1", "Edit", { file_path: "/work/repo/main.go" }), result("t1", { structuredPatch: [{ lines: [" x", "-old", "+new", "+more"] }] }), call("t2", "Write", { file_path: "/work/repo/new_test.go" }), result("t2", { type: "create", content: "a\nb\nc\n" }), call("t3", "Edit", { file_path: "/work/repo/broken.go" }), result("t3", "String to replace not found", true), call("t4", "Agent", { prompt: "go deeper" }), result("t4", { agentId: "a2", status: "completed" }), ]); write("a2", [ call("u1", "Edit", { file_path: "/work/repo/main.go" }), result("u1", { structuredPatch: [{ lines: ["+x"] }] }), call("u2", "Bash", { command: "go test ./..." }), result("u2", { stdout: "ok" }), ]); }); after(() => fs.rmSync(HOME, { recursive: true, force: true })); describe("agentEdits", () => { it("lists a subagent's files with their lines, nested subagents folded in", async () => { const edits = await agentEdits(SID, CWD, "a1"); assert.deepEqual(edits, [ { path: "/work/repo/main.go", added: 3, removed: 1, counted: true }, { path: "/work/repo/new_test.go", added: 3, removed: 0, counted: true }, ]); }); it("answers an empty list for a subagent with no transcript", async () => { assert.deepEqual(await agentEdits(SID, CWD, "nope"), []); }); it("reads a transcript again only after it changed", async () => { await agentEdits(SID, CWD, "a2"); const file = path.join(subDir, "agent-a2.jsonl"); const before = _cache.get(file); await agentEdits(SID, CWD, "a2"); assert.equal(_cache.get(file), before); fs.appendFileSync( file, "\n" + [ call("u3", "Write", { file_path: "/work/repo/late.go" }), result("u3", { type: "create", content: "z\n" }), ] .map((l) => JSON.stringify(l)) .join("\n") ); const edits = await agentEdits(SID, CWD, "a2"); assert.ok(edits.some((e) => e.path === "/work/repo/late.go")); }); }); describe("attachAgentEdits", () => { it("puts edits on an Agent result only", async () => { const messages = [ { content: [ { type: "tool_result", id: "t9", agent: { id: "a1" } }, { type: "tool_result", id: "t8", diff: { added: 1, removed: 0 } }, { type: "tool_result", id: "t7", agent: { id: "a1" }, is_error: true }, ], }, ]; await attachAgentEdits(messages, SID, CWD); const [ok, plain, failed] = messages[0].content; assert.ok(ok.edits.some((e) => e.path === "/work/repo/new_test.go")); assert.equal(plain.edits, undefined); assert.equal(failed.edits, undefined); }); }); describe("turnCarry", () => { const prompt = (text) => ({ type: "user", sender: "user", content: [{ type: "text", text }] }); const use = (id, name, file_path) => ({ type: "assistant", sender: "assistant", content: [{ type: "tool_use", id, name, input: file_path ? { file_path } : {} }], }); const res = (id, extra = {}) => ({ type: "user", sender: "tool", content: [{ type: "tool_result", id, ...extra }], }); it("answers the files the cut-off turn changed before the page", async () => { const c = turnCarry(); for (const m of [ prompt("old turn"), use("o1", "Write", "/w/old.go"), res("o1", { diff: { added: 9, removed: 0 } }), prompt("this turn"), use("1", "Write", "/w/a_test.go"), res("1", { diff: { added: 176, removed: 0 } }), use("2", "Edit", "/w/bad.go"), res("2", { is_error: true }), use("3", "Edit", "/w/b.go"), ]) c.drop(m); // The page: the result of a dropped call, then more of the same turn. const page = [res("3", { diff: { added: 2, removed: 1 } }), use("4", "Write", "/w/c.go")]; assert.deepEqual(await c.result(page, SID, CWD), [ { path: "/w/a_test.go", added: 176, removed: 0, counted: true }, { path: "/w/b.go", added: 2, removed: 1, counted: true }, ]); }); it("answers null when the page starts on a prompt", async () => { const c = turnCarry(); c.drop(prompt("p")); c.drop(use("1", "Write", "/w/a.go")); c.drop(res("1", { diff: { added: 1, removed: 0 } })); assert.equal(await c.result([prompt("next")], SID, CWD), null); }); it("folds in a dropped subagent's own edits", async () => { const c = turnCarry(); c.drop(prompt("p")); c.drop(use("9", "Agent")); c.drop(res("9", { agent: { id: "a1" } })); const got = await c.result([use("10", "Bash")], SID, CWD); assert.ok(got.some((e) => e.path === "/work/repo/new_test.go")); }); });