singularity-forge/src/tests/headless-progress.test.ts

591 lines
15 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "vitest";
import type { ProgressContext } from "../headless-ui.js";
import {
extractAssistantPreviewDelta,
formatCostLine,
formatHeadlessHeartbeat,
formatProgress,
formatPromptTraceLines,
formatThinkingLine,
summarizeToolArgs,
} from "../headless-ui.js";
// Tests run with NO_COLOR or non-TTY stderr, so ANSI codes are empty strings.
// We test content, not escape sequences.
function ctx(overrides: Partial<ProgressContext> = {}): ProgressContext {
return { verbose: true, ...overrides };
}
describe("formatProgress", () => {
describe("formatHeadlessHeartbeat", () => {
it("shows liveness details during quiet headless waits", () => {
const result = formatHeadlessHeartbeat({
elapsedMs: 125_000,
quietMs: 61_000,
totalEvents: 12,
toolCallCount: 3,
eventDelta: 4,
toolCallDelta: 1,
openToolCount: 1,
activeUnit: "execute-task M001/S01/T01",
activeModel: "zai/glm-5",
lastEventType: "agent_start",
});
assert.ok(result.includes("still running 2m5s"));
assert.ok(result.includes("quiet 1m1s"));
assert.ok(result.includes("last=agent_start"));
assert.ok(result.includes("events=12"));
assert.ok(result.includes("tools=3"));
assert.ok(result.includes("activity=+4 events, +1 tools"));
assert.ok(result.includes("openTools=1"));
assert.ok(result.includes("unit=execute-task M001/S01/T01"));
assert.ok(result.includes("model=zai/glm-5"));
});
it("shows no new events when the heartbeat is idle", () => {
const result = formatHeadlessHeartbeat({
elapsedMs: 300_000,
quietMs: 180_000,
totalEvents: 10,
toolCallCount: 2,
eventDelta: 0,
toolCallDelta: 0,
});
assert.ok(result.includes("activity=no new events"));
assert.ok(!result.includes("openTools="));
});
it("shows active tool identity when a quiet wait has an open tool", () => {
const result = formatHeadlessHeartbeat({
elapsedMs: 300_000,
quietMs: 180_000,
totalEvents: 10,
toolCallCount: 2,
openToolCount: 1,
openToolDetails: ["subagent:review M010 2m4s"],
});
assert.ok(result.includes("openTools=1[subagent:review M010 2m4s]"));
});
});
describe("formatPromptTraceLines", () => {
it("shows capped prompt preview with session path", () => {
const lines = formatPromptTraceLines(
"sf-auto",
"# Mission\nDo the work\nKeep going",
"/tmp/session.jsonl",
{ maxChars: 17, maxLines: 3 },
);
assert.ok(lines[0].includes("sf-auto instructions"));
assert.ok(lines[0].includes("/tmp/session.jsonl"));
assert.ok(lines.some((line) => line.includes("# Mission")));
assert.ok(lines.at(-1)?.includes("truncated"));
});
});
describe("tool_execution_start", () => {
it("shows tool name and summarized args in verbose mode", () => {
const result = formatProgress(
{
type: "tool_execution_start",
toolName: "bash",
args: { command: "npm run build" },
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("bash"));
assert.ok(result.includes("npm run build"));
});
it("shows Read with file path", () => {
const result = formatProgress(
{
type: "tool_execution_start",
toolName: "Read",
args: { path: "src/main.ts" },
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("Read"));
assert.ok(result.includes("src/main.ts"));
});
it("returns null in non-verbose mode", () => {
const result = formatProgress(
{
type: "tool_execution_start",
toolName: "bash",
args: { command: "npm run build" },
},
ctx({ verbose: false }),
);
assert.equal(result, null);
});
it("shows tool name alone when no args", () => {
const result = formatProgress(
{
type: "tool_execution_start",
toolName: "unknown_tool",
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("unknown_tool"));
});
});
describe("tool_execution_end", () => {
it("shows error with duration in verbose mode", () => {
const result = formatProgress(
{
type: "tool_execution_end",
toolName: "bash",
},
ctx({ isError: true, toolDuration: 1500 }),
);
assert.ok(result);
assert.ok(result.includes("bash"));
assert.ok(result.includes("error"));
assert.ok(result.includes("1.5s"));
});
it("shows done with duration in verbose mode", () => {
const result = formatProgress(
{
type: "tool_execution_end",
toolName: "read",
},
ctx({ toolDuration: 50 }),
);
assert.ok(result);
assert.ok(result.includes("done"));
assert.ok(result.includes("50ms"));
});
it("returns null in non-verbose mode", () => {
const result = formatProgress(
{
type: "tool_execution_end",
toolName: "bash",
isError: false,
},
ctx({ verbose: false }),
);
assert.equal(result, null);
});
});
describe("agent lifecycle", () => {
it("shows agent_start", () => {
const result = formatProgress({ type: "agent_start" }, ctx());
assert.ok(result);
assert.ok(result.includes("Session started"));
});
it("shows agent_end", () => {
const result = formatProgress({ type: "agent_end" }, ctx());
assert.ok(result);
assert.ok(result.includes("Session ended"));
});
it("shows agent_end with cost", () => {
const result = formatProgress(
{ type: "agent_end" },
ctx({
lastCost: { costUsd: 0.42, inputTokens: 10000, outputTokens: 500 },
}),
);
assert.ok(result);
assert.ok(result.includes("Session ended"));
assert.ok(result.includes("$0.42"));
assert.ok(result.includes("10500 tokens"));
});
});
describe("extension_error", () => {
it("shows extension path, event, and error message", () => {
const result = formatProgress(
{
type: "extension_error",
extensionPath: "/tmp/extensions/sf/index.js",
event: "startup",
error: { message: "boom" },
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("Extension error"));
assert.ok(result.includes("/tmp/extensions/sf/index.js"));
assert.ok(result.includes("startup"));
assert.ok(result.includes("boom"));
});
});
describe("extension_ui_request", () => {
it("shows notify with message", () => {
const result = formatProgress(
{
type: "extension_ui_request",
method: "notify",
message: "Autonomous mode started",
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("Autonomous mode started"));
});
it("bolds important notifications", () => {
const result = formatProgress(
{
type: "extension_ui_request",
method: "notify",
message: "Committed: fix auth flow",
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("Committed: fix auth flow"));
});
it("suppresses empty notify", () => {
const result = formatProgress(
{
type: "extension_ui_request",
method: "notify",
message: "",
},
ctx(),
);
assert.equal(result, null);
});
it("suppresses empty setStatus", () => {
const result = formatProgress(
{
type: "extension_ui_request",
method: "setStatus",
statusKey: "",
message: "",
},
ctx(),
);
assert.equal(result, null);
});
it("shows setStatus with statusKey as phase", () => {
const result = formatProgress(
{
type: "extension_ui_request",
method: "setStatus",
statusKey: "milestone:M001",
message: "Hello World CLI",
},
ctx(),
);
assert.ok(result);
assert.ok(result.includes("Milestone"));
assert.ok(result.includes("M001"));
});
it("suppresses setWidget (TUI-only)", () => {
const result = formatProgress(
{
type: "extension_ui_request",
method: "setWidget",
widgetKey: "progress",
},
ctx(),
);
assert.equal(result, null);
});
it("suppresses TUI footer widget setStatus keys", () => {
for (const key of [
"0-emoji",
"0-color-band",
"authority",
"ollama",
"sf-fast",
"sf-auto",
]) {
const r = formatProgress(
{
type: "extension_ui_request",
method: "setStatus",
statusKey: key,
message: "⏳",
},
ctx(),
);
assert.equal(r, null, `expected null for ${key}`);
}
});
it("suppresses bare unknown statusKey with no message", () => {
const r = formatProgress(
{
type: "extension_ui_request",
method: "setStatus",
statusKey: "something-new",
message: "",
},
ctx(),
);
assert.equal(r, null);
});
it("keeps workflow phases with colon prefix", () => {
const r = formatProgress(
{
type: "extension_ui_request",
method: "setStatus",
statusKey: "phase:discuss",
message: "starting",
},
ctx(),
);
assert.ok(r && r.includes("Phase: discuss"));
});
});
describe("tag prefix alignment", () => {
it("pads all tag prefixes to the same column width", () => {
const prefixes = [
formatProgress({ type: "agent_start" }, ctx())!,
formatProgress(
{
type: "extension_ui_request",
method: "notify",
message: "hi",
},
ctx(),
)!,
formatCostLine(0.01, 100, 50),
formatThinkingLine("x"),
];
// Under NO_COLOR / non-TTY, prefixes begin with "[tag]" followed by
// spaces to column 11, then content.
for (const p of prefixes) {
const m = p.match(/^\[[a-z]+\]\s+/);
assert.ok(m, `prefix missing on: ${JSON.stringify(p)}`);
assert.equal(m![0].length, 11, `wrong width on: ${JSON.stringify(p)}`);
}
});
});
describe("unknown events", () => {
it("returns null", () => {
assert.equal(formatProgress({ type: "some_random_event" }, ctx()), null);
});
});
});
describe("summarizeToolArgs", () => {
it("extracts path for Read", () => {
assert.equal(
summarizeToolArgs("Read", { path: "src/index.ts" }),
"src/index.ts",
);
});
it("extracts path for write", () => {
assert.equal(
summarizeToolArgs("write", { path: "/tmp/out.json" }),
"/tmp/out.json",
);
});
it("extracts file_path alias", () => {
assert.equal(
summarizeToolArgs("read", { file_path: "src/foo.ts" }),
"src/foo.ts",
);
});
it("prefers path over file_path when both present", () => {
assert.equal(
summarizeToolArgs("read", { path: "real.ts", file_path: "legacy.ts" }),
"real.ts",
);
});
it("extracts command for bash", () => {
assert.equal(summarizeToolArgs("bash", { command: "ls -la" }), "ls -la");
});
it("truncates long bash commands", () => {
const longCmd = "a".repeat(100);
const result = summarizeToolArgs("bash", { command: longCmd });
assert.ok(result.endsWith("..."));
assert.ok(result.length < 100);
});
it("extracts command for async_bash", () => {
assert.equal(
summarizeToolArgs("async_bash", { command: "npm run build" }),
"npm run build",
);
});
it("extracts jobs for await_job", () => {
assert.equal(
summarizeToolArgs("await_job", { jobs: ["bg_abc", "bg_def"] }),
"bg_abc, bg_def",
);
});
it("extracts pattern for grep", () => {
const result = summarizeToolArgs("grep", { pattern: "TODO", glob: "*.ts" });
assert.equal(result, "TODO *.ts");
});
it("extracts pattern and path for find", () => {
assert.equal(
summarizeToolArgs("find", { pattern: "*.ts", path: "src" }),
"*.ts in src",
);
});
it("extracts action and file for lsp", () => {
const result = summarizeToolArgs("lsp", {
action: "definition",
file: "src/main.ts",
symbol: "foo",
});
assert.equal(result, "definition src/main.ts foo");
});
it("extracts path for ls", () => {
assert.equal(summarizeToolArgs("ls", { path: "src/utils" }), "src/utils");
});
it("summarizes sf tool with milestone/slice/task IDs", () => {
assert.equal(
summarizeToolArgs("sf_task_complete", {
milestoneId: "M001",
sliceId: "S01",
taskId: "T01",
oneLiner: "Built the thing",
}),
"M001/S01/T01 Built the thing",
);
});
it("summarizes sf_plan_milestone with milestone ID", () => {
assert.equal(
summarizeToolArgs("sf_plan_milestone", { milestoneId: "M002" }),
"M002",
);
});
it("summarizes sf_decision_save with decision text", () => {
const result = summarizeToolArgs("sf_decision_save", {
decision: "Use SQLite for persistence",
});
assert.equal(result, "Use SQLite for persistence");
});
it("returns first string value for unknown tools", () => {
assert.equal(
summarizeToolArgs("custom_tool", { someKey: "hello" }),
"hello",
);
});
it("returns empty string for no args", () => {
assert.equal(summarizeToolArgs("unknown", {}), "");
});
it("extracts path for edit", () => {
assert.equal(
summarizeToolArgs("edit", { path: "src/config.ts" }),
"src/config.ts",
);
});
it("extracts path for hashline_edit", () => {
assert.equal(
summarizeToolArgs("hashline_edit", { path: "src/main.ts" }),
"src/main.ts",
);
});
it("extracts agent and task for subagent", () => {
assert.equal(
summarizeToolArgs("subagent", {
agent: "scout",
task: "Find auth patterns",
}),
"scout: Find auth patterns",
);
});
it("extracts url for browser_navigate", () => {
assert.equal(
summarizeToolArgs("browser_navigate", { url: "http://localhost:3000" }),
"http://localhost:3000",
);
});
});
describe("formatThinkingLine", () => {
it("formats short text", () => {
const result = formatThinkingLine("Analyzing the codebase");
assert.ok(result.includes("[thinking]"));
assert.ok(result.includes("Analyzing the codebase"));
});
it("truncates long text to ~120 chars", () => {
const longText = "word ".repeat(50); // 250 chars
const result = formatThinkingLine(longText);
assert.ok(result.includes("..."));
});
it("collapses whitespace", () => {
const result = formatThinkingLine("line one\n\nline two\ttab");
assert.ok(result.includes("line one line two tab"));
});
});
describe("extractAssistantPreviewDelta", () => {
it("separates assistant text deltas from thinking deltas", () => {
assert.deepEqual(
extractAssistantPreviewDelta({
type: "text_delta",
delta: "I will edit the file.",
}),
{ kind: "text", text: "I will edit the file." },
);
assert.deepEqual(
extractAssistantPreviewDelta({
type: "thinking_delta",
delta: "Need inspect first.",
}),
{ kind: "thinking", text: "Need inspect first." },
);
});
it("ignores non-preview assistant events", () => {
assert.equal(extractAssistantPreviewDelta({ type: "text_start" }), null);
assert.equal(extractAssistantPreviewDelta({ type: "thinking_end" }), null);
assert.equal(extractAssistantPreviewDelta(null), null);
});
});
describe("formatCostLine", () => {
it("formats cost with token count", () => {
const result = formatCostLine(0.0523, 4200, 1100);
assert.ok(result.includes("$0.0523"));
assert.ok(result.includes("5300 tokens"));
});
});