import { test } from "node:test";
import assert from "node:assert/strict";
import {
initialTaskProgress,
applyToolEvent,
markDone,
buildProgressLines,
buildProgressStream,
createProgressTracker,
toSubagentToolEvent,
type TaskProgress,
type ProgressTheme,
} from "../../src/progress.ts";
import type { RunUsage } from "../../src/usage.ts";
const sampleUsage: RunUsage = {
input: 12500, output: 840, cacheRead: 1_200_000, cacheWrite: 3000,
cost: 0.4123, isSubscription: false,
context: { percent: 12.34, window: 200000 },
};
const sampleUsageFooter = "\u219113k \u2193840 R1.2M W3.0k CH98.7% $0.412 12.3%/200k";
const fakeTheme: ProgressTheme = {
fg: (c, t) => `<${c}>${t}${c}>`,
};
test("initialTaskProgress: returns zeroed progress for the given agent", () => {
const p = initialTaskProgress("scout");
assert.deepEqual(p, { agent: "scout", runningTools: [], history: [], done: false });
});
test("applyToolEvent: tool_start appends to runningTools and history", () => {
const initial = initialTaskProgress("scout");
const p = applyToolEvent(initial, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
assert.deepEqual(p.runningTools, [{ toolCallId: "a", toolName: "read" }]);
assert.deepEqual(p.history, ["read foo.ts"]);
});
test("applyToolEvent: tool_end removes only the matching running tool, history is untouched", () => {
let p = initialTaskProgress("scout");
p = applyToolEvent(p, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
p = applyToolEvent(p, { type: "tool_start", toolCallId: "b", toolName: "grep", summary: "grep /x/" });
p = applyToolEvent(p, { type: "tool_end", toolCallId: "a" });
assert.deepEqual(p.runningTools, [{ toolCallId: "b", toolName: "grep" }]);
assert.deepEqual(p.history, ["read foo.ts", "grep /x/"]);
});
test("applyToolEvent: tool_end with unknown toolCallId is a no-op", () => {
let p = initialTaskProgress("scout");
p = applyToolEvent(p, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
const before = p.runningTools;
const after = applyToolEvent(p, { type: "tool_end", toolCallId: "unknown" });
assert.deepEqual(after.runningTools, before);
});
test("applyToolEvent: history preserves arrival order across interleaved starts/ends", () => {
let p = initialTaskProgress("scout");
p = applyToolEvent(p, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read a.ts" });
p = applyToolEvent(p, { type: "tool_end", toolCallId: "a" });
p = applyToolEvent(p, { type: "tool_start", toolCallId: "b", toolName: "grep", summary: "grep /x/" });
assert.deepEqual(p.history, ["read a.ts", "grep /x/"]);
});
test("markDone: sets done true, clears runningTools, preserves history", () => {
let p = initialTaskProgress("scout");
p = applyToolEvent(p, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
const done = markDone(p);
assert.equal(done.done, true);
assert.equal(done.agent, p.agent);
assert.deepEqual(done.history, p.history);
assert.deepEqual(done.runningTools, []);
});
test("markDone: called without usage does not add the usage key", () => {
const p = initialTaskProgress("scout");
const done = markDone(p);
assert.equal("usage" in done, false);
});
test("markDone: called with usage attaches it, history stays intact", () => {
let p = initialTaskProgress("scout");
p = applyToolEvent(p, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
const done = markDone(p, sampleUsage);
assert.equal(done.usage, sampleUsage);
assert.deepEqual(done.history, p.history);
});
test("applyToolEvent: returns a new object and does not mutate the original runningTools array", () => {
const p: TaskProgress = initialTaskProgress("scout");
const originalRunningTools = p.runningTools;
const event = { type: "tool_start" as const, toolCallId: "a", toolName: "read", summary: "read foo.ts" };
const next = applyToolEvent(p, event);
assert.notEqual(next, p);
assert.equal(p.runningTools, originalRunningTools);
assert.equal(originalRunningTools.length, 0);
});
test("buildProgressLines: one task, one running tool renders accent agent, dim tool count and running tool name", () => {
const p: TaskProgress = {
agent: "scout",
runningTools: [{ toolCallId: "a", toolName: "read" }],
history: ["read foo.ts"],
done: false,
};
const result = buildProgressLines([p], fakeTheme);
assert.equal(result, "scout \u00b7 tools: 1 \u00b7 running: read");
});
test("buildProgressLines: two running tools list names in start order", () => {
const p: TaskProgress = {
agent: "scout",
runningTools: [
{ toolCallId: "a", toolName: "read" },
{ toolCallId: "b", toolName: "grep" },
],
history: ["read foo.ts", "grep /x/"],
done: false,
};
const result = buildProgressLines([p], fakeTheme);
assert.equal(result, "scout \u00b7 tools: 2 \u00b7 running: read, grep");
});
test("buildProgressLines: no running tools, not done renders working\u2026", () => {
const p: TaskProgress = { agent: "scout", runningTools: [], history: [], done: false };
const result = buildProgressLines([p], fakeTheme);
assert.equal(result, "scout \u00b7 tools: 0 \u00b7 working\u2026");
});
test("buildProgressLines: done true renders done regardless of runningTools", () => {
const p: TaskProgress = {
agent: "scout",
runningTools: [{ toolCallId: "a", toolName: "read" }],
history: ["read a.ts", "read b.ts", "read c.ts"],
done: true,
};
const result = buildProgressLines([p], fakeTheme);
assert.equal(result, "scout \u00b7 tools: 3 \u00b7 done");
});
test("buildProgressLines: done with usage appends the usage footer after the status", () => {
const p: TaskProgress = { agent: "scout", runningTools: [], history: [], done: true, usage: sampleUsage };
const result = buildProgressLines([p], fakeTheme);
assert.equal(
result,
`scout \u00b7 tools: 0 \u00b7 done \u00b7 ${sampleUsageFooter}`,
);
});
test("buildProgressLines: not done with usage does not render the footer (footer only appears once settled)", () => {
const p: TaskProgress = { agent: "scout", runningTools: [], history: [], done: false, usage: sampleUsage };
const result = buildProgressLines([p], fakeTheme);
assert.equal(result, "scout \u00b7 tools: 0 \u00b7 working\u2026");
});
test("buildProgressLines: done with a usage that renders empty omits the footer segment", () => {
const emptyRunUsage: RunUsage = {
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, isSubscription: false, context: undefined,
};
const p: TaskProgress = { agent: "scout", runningTools: [], history: [], done: true, usage: emptyRunUsage };
const result = buildProgressLines([p], fakeTheme);
assert.equal(result, "scout \u00b7 tools: 0 \u00b7 done");
});
test("buildProgressLines: two tasks render two lines joined by newline in input order", () => {
const p1: TaskProgress = { agent: "scout", runningTools: [], history: [], done: false };
const p2: TaskProgress = { agent: "web-scout", runningTools: [], history: ["read a.ts", "read b.ts"], done: true };
const result = buildProgressLines([p1, p2], fakeTheme);
assert.equal(
result,
"scout \u00b7 tools: 0 \u00b7 working\u2026\n"
+ "web-scout \u00b7 tools: 2 \u00b7 done",
);
});
test("buildProgressStream: single task, empty history renders only the status line (same as buildProgressLines)", () => {
const p: TaskProgress = { agent: "scout", runningTools: [], history: [], done: false };
const result = buildProgressStream([p], fakeTheme);
assert.equal(result, buildProgressLines([p], fakeTheme));
});
test("buildProgressStream: single task with history renders the status line followed by indented dim entries in order", () => {
const p: TaskProgress = {
agent: "scout",
runningTools: [{ toolCallId: "a", toolName: "grep" }],
history: ["read foo.ts", "grep /x/"],
done: false,
};
const result = buildProgressStream([p], fakeTheme);
assert.equal(
result,
"scout \u00b7 tools: 2 \u00b7 running: grep\n"
+ " read foo.ts\n"
+ " grep /x/",
);
});
test("buildProgressStream: two tasks render each block in input order", () => {
const p1: TaskProgress = { agent: "scout", runningTools: [], history: ["read a.ts"], done: false };
const p2: TaskProgress = { agent: "web-scout", runningTools: [], history: [], done: true };
const result = buildProgressStream([p1, p2], fakeTheme);
assert.equal(
result,
"scout \u00b7 tools: 1 \u00b7 working\u2026\n"
+ " read a.ts\n"
+ "web-scout \u00b7 tools: 0 \u00b7 done",
);
});
test("buildProgressStream: done with usage puts the footer on the header line, history lines untouched", () => {
const p: TaskProgress = {
agent: "scout",
runningTools: [],
history: ["read foo.ts"],
done: true,
usage: sampleUsage,
};
const result = buildProgressStream([p], fakeTheme);
assert.equal(
result,
`scout \u00b7 tools: 1 \u00b7 done \u00b7 ${sampleUsageFooter}\n`
+ " read foo.ts",
);
});
test("toSubagentToolEvent: tool_execution_start maps args through formatToolCall into summary", () => {
const event = toSubagentToolEvent({
type: "tool_execution_start",
toolCallId: "a",
toolName: "read",
args: { path: "a.ts" },
} as any);
assert.deepEqual(event, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read a.ts" });
});
test("toSubagentToolEvent: tool_execution_end maps without a summary", () => {
const event = toSubagentToolEvent({
type: "tool_execution_end",
toolCallId: "a",
toolName: "read",
result: "big content",
isError: false,
} as any);
assert.deepEqual(event, { type: "tool_end", toolCallId: "a" });
});
test("toSubagentToolEvent: tool_execution_update is ignored", () => {
const event = toSubagentToolEvent({
type: "tool_execution_update",
toolCallId: "a",
toolName: "read",
args: {},
partialResult: {},
} as any);
assert.equal(event, undefined);
});
test("createProgressTracker: onToolEvent on one index leaves other tasks' progress untouched", () => {
const emitted: Array = [];
const tracker = createProgressTracker(["scout", "planner"], (details) => emitted.push(details.progress));
tracker.onToolEvent(0, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
const last = emitted[emitted.length - 1];
assert.deepEqual(last[1], { agent: "planner", runningTools: [], history: [], done: false });
});
test("createProgressTracker: emits a fresh array reference on every event", () => {
const emitted: Array = [];
const tracker = createProgressTracker(["scout", "planner"], (details) => emitted.push(details.progress));
tracker.onToolEvent(0, { type: "tool_start", toolCallId: "a", toolName: "read", summary: "read foo.ts" });
tracker.markTaskDone(0);
assert.notEqual(emitted[0], emitted[1]);
});
test("createProgressTracker: onToolEvent is a no-op for an index already marked done", () => {
const emitted: Array = [];
const tracker = createProgressTracker(["scout", "planner"], (details) => emitted.push(details.progress));
tracker.markTaskDone(0);
const emitCountAfterDone = emitted.length;
tracker.onToolEvent(0, { type: "tool_start", toolCallId: "x", toolName: "read", summary: "read foo.ts" });
assert.equal(emitted.length, emitCountAfterDone);
const last = emitted[emitted.length - 1];
assert.deepEqual(last[0], { agent: "scout", runningTools: [], history: [], done: true });
});
test("createProgressTracker: markTaskDone only marks its own slot done", () => {
const emitted: Array = [];
const tracker = createProgressTracker(["scout", "planner"], (details) => emitted.push(details.progress));
tracker.markTaskDone(1);
const last = emitted[emitted.length - 1];
assert.equal(last[0].done, false);
assert.equal(last[1].done, true);
});
test("createProgressTracker: markTaskDone with usage attaches usage only to its own slot", () => {
const emitted: Array = [];
const tracker = createProgressTracker(["scout", "planner"], (details) => emitted.push(details.progress));
tracker.markTaskDone(1, sampleUsage);
const last = emitted[emitted.length - 1];
assert.equal("usage" in last[0], false);
assert.equal(last[1].usage, sampleUsage);
});