5ccbaf5c23
A skill that hands its work to subagents (vp-auto) showed "Edited 1 file" for a turn that changed 4: a subagent's Write/Edit calls are in its own agent-<id>.jsonl, so the card only saw the parent's. - server/lib/agent-edits.js reads a subagent's transcript (readLines, a member's only as theirs) for the files it wrote or edited with their +/- lines, nested subagents folded in (3 levels), cached by size + mtime. - The transcript route puts them on each Claude Agent/Task result as `edits`; the card adds them to the turn's own edits. A file only a subagent wrote is listed, never an artifact card (its versions are rebuilt from this transcript's inputs, which a subagent's are not).
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
/**
|
|
* @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`.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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, _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);
|
|
});
|
|
});
|