import { test } from "node:test"; import assert from "node:assert/strict"; import { formatAgentParams, buildSubagentCallText } from "../../src/render-call.ts"; import type { AgentConfig } from "../../src/agents.ts"; import type { CallTheme } from "../../src/render-call.ts"; function makeAgent(overrides: Partial = {}): AgentConfig { return { name: "scout", description: "finds things", systemPromptMode: "append", inheritProjectContext: true, defaultReads: [], source: "user", filePath: "/agents/scout.md", systemPrompt: "", ...overrides, }; } test("formatAgentParams: model, thinking, and tools all set renders each verbatim", () => { const agent = makeAgent({ model: "claude-opus-4", thinking: "high", tools: ["a", "b", "c"], }); const result = formatAgentParams(agent); assert.equal(result, "model: claude-opus-4 · thinking: high · tools: a, b, c · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: none of the three set renders all as inherited", () => { const agent = makeAgent({}); const result = formatAgentParams(agent); assert.equal(result, "model: inherited · thinking: inherited · tools: inherited · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: only model set renders thinking and tools as inherited", () => { const agent = makeAgent({ model: "claude-opus-4" }); const result = formatAgentParams(agent); assert.equal(result, "model: claude-opus-4 · thinking: inherited · tools: inherited · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: empty tools array renders tools as none", () => { const agent = makeAgent({ tools: [] }); const result = formatAgentParams(agent); assert.equal(result, "model: inherited · thinking: inherited · tools: none · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: exactly 5 tools renders the full list with no +more suffix", () => { const agent = makeAgent({ tools: ["a", "b", "c", "d", "e"] }); const result = formatAgentParams(agent); assert.equal(result, "model: inherited · thinking: inherited · tools: a, b, c, d, e · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: 8 tools renders first 5 plus a +3 more suffix", () => { const agent = makeAgent({ tools: ["a", "b", "c", "d", "e", "f", "g", "h"], }); const result = formatAgentParams(agent); assert.equal( result, "model: inherited · thinking: inherited · tools: a, b, c, d, e +3 more · skills: inherited · maxTurns: inherited · timeoutMs: inherited", ); }); const fakeTheme: CallTheme = { fg: (c, t) => `<${c}>${t}`, bold: (t) => `${t}`, }; test("buildSubagentCallText: agent + task renders bold prefix, accent agent, truncated first-line task (regression pin)", () => { const result = buildSubagentCallText( { agent: "scout", task: "Find X" }, fakeTheme, new Map(), ); assert.equal( result, "subagent scout: Find X", ); }); test("buildSubagentCallText: missing agent renders literal '?' in place of the name", () => { const result = buildSubagentCallText({ task: "Find X" }, fakeTheme, new Map()); assert.equal( result, "subagent ?: Find X", ); }); test("buildSubagentCallText: missing task renders no ': ...' segment", () => { const result = buildSubagentCallText({ agent: "scout" }, fakeTheme, new Map()); assert.equal( result, "subagent scout", ); }); test("buildSubagentCallText: task longer than 80 chars is truncated with ellipsis on first line only", () => { const longLine = "a".repeat(90); const result = buildSubagentCallText( { agent: "scout", task: `${longLine}\nsecond line` }, fakeTheme, new Map(), ); const expectedTruncated = `${"a".repeat(79)}\u2026`; assert.equal( result, `subagent scout: ${expectedTruncated}`, ); }); test("buildSubagentCallText: paramAgents contains the agent appends a dim params line", () => { const agent = makeAgent({ name: "scout", model: "claude-opus-4" }); const paramAgents = new Map([["scout", agent]]); const result = buildSubagentCallText( { agent: "scout", task: "Find X" }, fakeTheme, paramAgents, ); assert.equal( result, "subagent scout: Find X" + `\n ${formatAgentParams(agent)}`, ); }); test("buildSubagentCallText: paramAgents does not contain the agent renders title only", () => { const paramAgents = new Map([["other", makeAgent({ name: "other" })]]); const result = buildSubagentCallText( { agent: "scout", task: "Find X" }, fakeTheme, paramAgents, ); assert.equal( result, "subagent scout: Find X", ); }); test("buildSubagentCallText: paramAgents undefined renders title only", () => { const result = buildSubagentCallText( { agent: "scout", task: "Find X" }, fakeTheme, new Map(), ); assert.equal( result, "subagent scout: Find X", ); }); const prefix = "subagent "; test("buildSubagentCallText: two tasks, paramAgents undefined renders parallel title (regression pin)", () => { const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files" }, { agent: "web-scout", task: "Find docs" }, ], }, fakeTheme, new Map(), ); assert.equal(result, `${prefix}(2): scout: List files, ...`); }); test("buildSubagentCallText: single task in tasks array renders no ', ...' suffix", () => { const result = buildSubagentCallText( { tasks: [{ agent: "scout", task: "List files" }] }, fakeTheme, new Map(), ); assert.equal(result, `${prefix}(1): scout: List files`); }); test("buildSubagentCallText: tasks entry missing task renders empty description instead of throwing", () => { const result = buildSubagentCallText( { tasks: [{ agent: "scout" }] }, fakeTheme, new Map(), ); assert.equal(result, `${prefix}(1): scout: `); }); test("buildSubagentCallText: tasks entry missing agent renders '?' instead of throwing", () => { const result = buildSubagentCallText( { tasks: [{ task: "List files" }] }, fakeTheme, new Map(), ); assert.equal(result, `${prefix}(1): ?: List files`); }); test("buildSubagentCallText: empty tasks array falls through to single-agent branch", () => { const result = buildSubagentCallText( { agent: "scout", task: "Find X", tasks: [] }, fakeTheme, new Map(), ); assert.equal( result, "subagent scout: Find X", ); }); test("buildSubagentCallText: two tasks, paramAgents has both appends two param lines in order", () => { const scoutAgent = makeAgent({ name: "scout", model: "claude-opus-4" }); const webScoutAgent = makeAgent({ name: "web-scout", model: "claude-haiku" }); const paramAgents = new Map([ ["scout", scoutAgent], ["web-scout", webScoutAgent], ]); const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files" }, { agent: "web-scout", task: "Find docs" }, ], }, fakeTheme, paramAgents, ); assert.equal( result, `${prefix}(2): scout: List files, ...` + `\n scout: ${formatAgentParams(scoutAgent)}` + `\n web-scout: ${formatAgentParams(webScoutAgent)}`, ); }); test("buildSubagentCallText: two tasks, paramAgents has only the second appends exactly one param line", () => { const webScoutAgent = makeAgent({ name: "web-scout", model: "claude-haiku" }); const paramAgents = new Map([["web-scout", webScoutAgent]]); const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files" }, { agent: "web-scout", task: "Find docs" }, ], }, fakeTheme, paramAgents, ); assert.equal( result, `${prefix}(2): scout: List files, ...` + `\n web-scout: ${formatAgentParams(webScoutAgent)}`, ); }); test("buildSubagentCallText: duplicate agent name in tasks produces two separate param lines", () => { const scoutAgent = makeAgent({ name: "scout", model: "claude-opus-4" }); const paramAgents = new Map([["scout", scoutAgent]]); const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files" }, { agent: "scout", task: "Find docs" }, ], }, fakeTheme, paramAgents, ); assert.equal( result, `${prefix}(2): scout: List files, ...` + `\n scout: ${formatAgentParams(scoutAgent)}` + `\n scout: ${formatAgentParams(scoutAgent)}`, ); }); test("buildSubagentCallText: tasks present with paramAgents undefined renders title only, no param lines", () => { const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files" }, { agent: "web-scout", task: "Find docs" }, ], }, fakeTheme, new Map(), ); assert.equal(result, `${prefix}(2): scout: List files, ...`); }); test("formatAgentParams: 6 tools renders first 5 plus a +1 more suffix", () => { const agent = makeAgent({ tools: ["a", "b", "c", "d", "e", "f"] }); const result = formatAgentParams(agent); assert.equal( result, "model: inherited · thinking: inherited · tools: a, b, c, d, e +1 more · skills: inherited · maxTurns: inherited · timeoutMs: inherited", ); }); test("formatAgentParams: modelOverride present shows the override instead of agent.model", () => { const agent = makeAgent({ model: "claude-opus-4" }); const result = formatAgentParams(agent, { model: "claude-haiku" }); assert.equal(result, "model: claude-haiku · thinking: inherited · tools: inherited · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: tools override renders 'none' regardless of agent's configured tools", () => { const agent = makeAgent({ tools: ["a", "b", "c"] }); const result = formatAgentParams(agent, { tools: [] }); assert.equal(result, "model: inherited · thinking: inherited · tools: none · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: empty override object behaves exactly like no override", () => { const agent = makeAgent({ model: "claude-opus-4", tools: ["a", "b"] }); const result = formatAgentParams(agent, {}); assert.equal(result, "model: claude-opus-4 · thinking: inherited · tools: a, b · skills: inherited · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: skills override renders a new skills segment with the effective skills", () => { const agent = makeAgent({}); const result = formatAgentParams(agent, { skills: ["a", "b"] }); assert.equal(result, "model: inherited · thinking: inherited · tools: inherited · skills: a, b · maxTurns: inherited · timeoutMs: inherited"); }); test("formatAgentParams: tools and skills both overridden together renders both effective values", () => { const agent = makeAgent({ tools: ["a", "b"], skills: ["x"] }); const result = formatAgentParams(agent, { tools: ["read", "grep"], skills: ["tdd", "gof-design-patterns"] }); assert.equal( result, "model: inherited · thinking: inherited · tools: read, grep · skills: tdd, gof-design-patterns · maxTurns: inherited · timeoutMs: inherited", ); }); test("formatAgentParams: agent with maxTurns renders a 'maxTurns: ' segment in the param line", () => { const agent = makeAgent({ maxTurns: 7 }); const result = formatAgentParams(agent); assert.ok( result.includes("maxTurns: 7"), `expected rendered line to contain "maxTurns: 7", got: ${result}`, ); }); test("formatAgentParams: agent without maxTurns renders a 'maxTurns: inherited' segment in the param line", () => { const agent = makeAgent({}); const result = formatAgentParams(agent); assert.ok( result.includes("maxTurns: inherited"), `expected rendered line to contain "maxTurns: inherited", got: ${result}`, ); }); test("formatAgentParams: maxTurns override takes precedence when agent has no maxTurns (renders the override value)", () => { const agent = makeAgent({}); const result = formatAgentParams(agent, { maxTurns: 3 }); assert.ok( result.includes("maxTurns: 3"), `expected rendered line to contain "maxTurns: 3", got: ${result}`, ); }); test("formatAgentParams: thinking override takes precedence over the agent's frontmatter thinking (regression: must read the effective value, not agent.thinking directly)", () => { const agent = makeAgent({ thinking: "low" }); const result = formatAgentParams(agent, { thinking: "max" }); assert.ok( result.includes("thinking: max"), `expected rendered line to contain "thinking: max", got: ${result}`, ); assert.ok( !result.includes("thinking: low"), `expected rendered line to not contain the overridden-away "thinking: low", got: ${result}`, ); }); test("formatAgentParams: agent with timeoutMs renders a 'timeoutMs: ' segment in the param line", () => { const agent = makeAgent({ timeoutMs: 60_000 }); const result = formatAgentParams(agent); assert.ok( result.includes("timeoutMs: 60000"), `expected rendered line to contain "timeoutMs: 60000", got: ${result}`, ); }); test("formatAgentParams: timeoutMs override takes precedence when agent has no timeoutMs (renders the override value)", () => { const agent = makeAgent({}); const result = formatAgentParams(agent, { timeoutMs: 20 }); assert.ok( result.includes("timeoutMs: 20"), `expected rendered line to contain "timeoutMs: 20", got: ${result}`, ); }); test("buildSubagentCallText: single-mode args.model overrides the agent's configured model in the param line", () => { const agent = makeAgent({ name: "scout", model: "claude-opus-4" }); const paramAgents = new Map([["scout", agent]]); const result = buildSubagentCallText( { agent: "scout", task: "Find X", model: "claude-haiku" }, fakeTheme, paramAgents, ); assert.equal( result, "subagent scout: Find X" + `\n ${formatAgentParams(agent, { model: "claude-haiku" })}`, ); }); test("buildSubagentCallText: parallel tasks each show their own model override independently", () => { const scoutAgent = makeAgent({ name: "scout", model: "claude-opus-4" }); const webScoutAgent = makeAgent({ name: "web-scout", model: "claude-haiku" }); const paramAgents = new Map([ ["scout", scoutAgent], ["web-scout", webScoutAgent], ]); const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files", model: "claude-sonnet" }, { agent: "web-scout", task: "Find docs" }, ], }, fakeTheme, paramAgents, ); assert.equal( result, `${prefix}(2): scout: List files, ...` + `\n scout: ${formatAgentParams(scoutAgent, { model: "claude-sonnet" })}` + `\n web-scout: ${formatAgentParams(webScoutAgent, undefined)}`, ); }); test("buildSubagentCallText: parallel tasks each show their own tools/skills override independently", () => { const scoutAgent = makeAgent({ name: "scout", tools: ["read", "grep"] }); const webScoutAgent = makeAgent({ name: "web-scout", skills: ["web-fetch"] }); const paramAgents = new Map([ ["scout", scoutAgent], ["web-scout", webScoutAgent], ]); const result = buildSubagentCallText( { tasks: [ { agent: "scout", task: "List files", tools: [] }, { agent: "web-scout", task: "Find docs", skills: ["search"] }, ], }, fakeTheme, paramAgents, ); assert.equal( result, `${prefix}(2): scout: List files, ...` + `\n scout: ${formatAgentParams(scoutAgent, { tools: [] })}` + `\n web-scout: ${formatAgentParams(webScoutAgent, { skills: ["search"] })}`, ); }); test("buildSubagentCallText: task exactly 80 chars renders without truncation", () => { const exactTask = "a".repeat(80); const result = buildSubagentCallText( { agent: "scout", task: exactTask }, fakeTheme, new Map(), ); assert.equal( result, `subagent scout: ${exactTask}`, ); });