Files
harness-tools/extensions/interactive-subagents/test/test.ts
T

3318 lines
116 KiB
TypeScript

import { describe, it, before, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { visibleWidth } from "@earendil-works/pi-tui";
import * as subagentsModule from "../pi-extension/subagents/index.ts";
import {
getLeafId,
getNewEntries,
countSessionEntryLines,
getSessionId,
readNameRegistry,
readSubagentLoadout,
registerName,
resolveNameInRegistry,
nameRegistryPath,
writeSubagentLoadout,
loadoutSidecarPath,
type SubagentLoadout,
resetSessionIndexCache,
resolveSessionFileById,
findLastAssistantMessage,
appendBranchSummary,
copySessionFile,
mergeNewEntries,
seedSubagentSessionFile,
summarizeSessionStats,
} from "../pi-extension/subagents/session.ts";
import {
__pollForExitTest__,
pollForExit,
selectMuxBackend,
shellEscape,
} from "../pi-extension/subagents/surface.ts";
import { parseCreatedPane, parseCreatedTab } from "../pi-extension/subagents/herdr.ts";
import {
__orcaTest__,
parseCreatedSplit,
parseCreatedTerminal,
parseReadTail,
} from "../pi-extension/subagents/orca.ts";
import {
advanceStatusState,
capStatusLines,
classifyStatus,
createStatusState,
forceStatusAfterInterrupt,
formatStatusAggregate,
formatStatusLine,
formatTransitionLine,
observeStatus,
loadStatusConfig,
parseStatusConfig,
} from "../pi-extension/subagents/status.ts";
import {
createSubagentActivityRecorder,
getSubagentActivityFile,
readSubagentActivityFile,
} from "../pi-extension/subagents/activity.ts";
import {
shouldMarkUserTookOver,
shouldAutoExitOnAgentEnd,
findLatestAssistantError,
runningChildrenCount,
} from "../pi-extension/subagents/subagent-done.ts";
import subagentDoneExtension from "../pi-extension/subagents/subagent-done.ts";
// --- Helpers ---
function createTestDir(): string {
return mkdtempSync(join(tmpdir(), "subagents-test-"));
}
function createSessionFile(dir: string, entries: object[]): string {
const file = join(dir, "test-session.jsonl");
const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
writeFileSync(file, content);
return file;
}
function withTempDir(run: (dir: string) => void) {
const dir = createTestDir();
try {
run(dir);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
function createMockExtensionApi() {
const registeredTools: Array<any> = [];
const registeredCommands: Array<any> = [];
const registeredMessageRenderers: Array<any> = [];
const eventHandlers = new Map<string, Array<(event: any, ctx: any) => any>>();
const sentUserMessages: string[] = [];
const sentMessages: Array<any> = [];
return {
registeredTools,
registeredCommands,
registeredMessageRenderers,
eventHandlers,
sentUserMessages,
sentMessages,
api: {
on(event: string, handler: (event: any, ctx: any) => any) {
const handlers = eventHandlers.get(event) ?? [];
handlers.push(handler);
eventHandlers.set(event, handlers);
},
registerTool(tool: any) {
registeredTools.push(tool);
},
registerCommand(name: string, command: any) {
registeredCommands.push({ name, ...command });
},
registerMessageRenderer(name: string, renderer: any) {
registeredMessageRenderers.push({ name, renderer });
},
registerShortcut() {},
sendUserMessage(message: string) {
sentUserMessages.push(message);
},
sendMessage(message: any, options?: any) {
sentMessages.push({ message, options });
},
getAllTools() {
return [];
},
getActiveTools() {
return [];
},
} as any,
};
}
async function emitMockEvent(
mock: ReturnType<typeof createMockExtensionApi>,
event: string,
payload: Record<string, unknown>,
ctx: Record<string, unknown> = {},
) {
for (const handler of mock.eventHandlers.get(event) ?? []) {
await handler({ type: event, ...payload }, ctx);
}
}
type SharedRuntimeTestState = {
runningSubagents: Map<string, unknown>;
latestCtx: unknown;
latestPi: unknown;
widgetInterval: ReturnType<typeof setInterval> | null;
statusInterval: ReturnType<typeof setInterval> | null;
standaloneWidgetSyncScheduled: boolean;
pendingDeliveries: { length: number };
deliveryFlushScheduled: boolean;
activeRuntimeToken: symbol | null;
registeredRuntimeToken: symbol | null;
statusWidgetClaim: unknown;
statusWidget?: unknown;
};
function resetSharedRuntimeForTest(shared: SharedRuntimeTestState): void {
if (shared.widgetInterval) clearInterval(shared.widgetInterval);
if (shared.statusInterval) clearInterval(shared.statusInterval);
shared.widgetInterval = null;
shared.statusInterval = null;
shared.standaloneWidgetSyncScheduled = false;
shared.runningSubagents.clear();
shared.latestCtx = null;
shared.latestPi = null;
shared.statusWidgetClaim = null;
shared.pendingDeliveries.length = 0;
shared.deliveryFlushScheduled = false;
shared.activeRuntimeToken = null;
shared.registeredRuntimeToken = null;
}
function restoreEnvVar(name: string, value: string | undefined) {
if (value === undefined) {
delete process.env[name];
return;
}
process.env[name] = value;
}
function withMockedNow<T>(now: number, fn: () => T): T {
const originalNow = Date.now;
Date.now = () => now;
try {
return fn();
} finally {
Date.now = originalNow;
}
}
function writeAgentFile(
agentsDir: string,
name: string,
frontmatter: string,
body = "You are a test agent.",
) {
mkdirSync(agentsDir, { recursive: true });
writeFileSync(join(agentsDir, `${name}.md`), `---\n${frontmatter}\n---\n\n${body}\n`);
}
async function withIsolatedAgentEnv(
fn: (paths: {
projectDir: string;
projectAgentsDir: string;
globalDir: string;
globalAgentsDir: string;
}) => Promise<void> | void,
) {
const root = createTestDir();
const previousCwd = process.cwd();
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
const projectDir = join(root, "project");
const projectAgentsDir = join(projectDir, ".pi", "agents");
const globalDir = join(root, "global");
const globalAgentsDir = join(globalDir, "agents");
mkdirSync(projectAgentsDir, { recursive: true });
mkdirSync(globalAgentsDir, { recursive: true });
process.chdir(projectDir);
process.env.PI_CODING_AGENT_DIR = globalDir;
try {
await fn({ projectDir, projectAgentsDir, globalDir, globalAgentsDir });
} finally {
process.chdir(previousCwd);
restoreEnvVar("PI_CODING_AGENT_DIR", previousAgentDir);
rmSync(root, { recursive: true, force: true });
}
}
const SESSION_HEADER = { type: "session", id: "sess-001", version: 3 };
const MODEL_CHANGE = { type: "model_change", id: "mc-001", parentId: null };
const USER_MSG = {
type: "message",
id: "user-001",
parentId: "mc-001",
message: {
role: "user",
content: [{ type: "text", text: "Hello, plan something" }],
},
};
const ASSISTANT_MSG = {
type: "message",
id: "asst-001",
parentId: "user-001",
message: {
role: "assistant",
content: [{ type: "text", text: "Here is my plan..." }],
},
};
const ASSISTANT_MSG_2 = {
type: "message",
id: "asst-002",
parentId: "asst-001",
message: {
role: "assistant",
content: [
{ type: "thinking", thinking: "Let me think..." },
{ type: "text", text: "Updated plan with details." },
],
},
};
const TOOL_RESULT = {
type: "message",
id: "tool-001",
parentId: "asst-001",
message: {
role: "toolResult",
toolCallId: "tc-001",
toolName: "bash",
content: [{ type: "text", text: "output here" }],
},
};
// --- Tests ---
describe("session.ts", () => {
let dir: string;
before(() => {
dir = createTestDir();
});
after(() => {
rmSync(dir, { recursive: true, force: true });
});
describe("getLeafId", () => {
it("returns last entry id", () => {
const file = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE, USER_MSG, ASSISTANT_MSG]);
assert.equal(getLeafId(file), "asst-001");
});
it("returns null for empty file", () => {
const file = join(dir, "empty.jsonl");
writeFileSync(file, "");
assert.equal(getLeafId(file), null);
});
});
describe("getNewEntries", () => {
it("returns entries after a given line", () => {
const file = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE, USER_MSG, ASSISTANT_MSG]);
const entries = getNewEntries(file, 2);
assert.equal(entries.length, 2);
assert.equal(entries[0].id, "user-001");
assert.equal(entries[1].id, "asst-001");
});
it("returns empty array when no new entries", () => {
const file = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE]);
const entries = getNewEntries(file, 2);
assert.equal(entries.length, 0);
});
it("countSessionEntryLines matches getNewEntries(0).length without parsing", () => {
const file = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE, USER_MSG, ASSISTANT_MSG]);
assert.equal(countSessionEntryLines(file), getNewEntries(file, 0).length);
assert.equal(countSessionEntryLines(file), 4);
});
it("countSessionEntryLines ignores blank lines and returns 0 for missing files", () => {
const file = join(dir, "blanks.jsonl");
writeFileSync(file, JSON.stringify({ type: "session", id: "x" }) + "\n\n\n");
assert.equal(countSessionEntryLines(file), 1);
assert.equal(countSessionEntryLines(join(dir, "does-not-exist.jsonl")), 0);
});
});
describe("getSessionId / resolveSessionFileById", () => {
function writeSession(d: string, fname: string, id: string): string {
const p = join(d, fname);
writeFileSync(p, JSON.stringify({ type: "session", id, version: 3 }) + "\n");
return p;
}
// The resolver caches an id→file index per root; reset it so each test
// builds a fresh index from the current on-disk state.
beforeEach(() => {
resetSessionIndexCache();
});
it("reads the header id from a session file", () => {
const file = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE, USER_MSG]);
assert.equal(getSessionId(file), "sess-001");
});
it("returns null for a file without a session header", () => {
const file = createSessionFile(dir, [USER_MSG]);
assert.equal(getSessionId(file), null);
});
it("resolves a session file by exact id under the root", () => {
const a = writeSession(dir, "a.jsonl", "019f-aaaa");
writeSession(dir, "b.jsonl", "019f-bbbb");
assert.equal(resolveSessionFileById("019f-aaaa", dir), a);
});
it("resolves a session file by id prefix", () => {
const a = writeSession(dir, "p.jsonl", "019f-prefix-match");
assert.equal(resolveSessionFileById("019f-prefix", dir), a);
});
it("returns null when no session matches", () => {
writeSession(dir, "c.jsonl", "abc");
assert.equal(resolveSessionFileById("zzz", dir), null);
});
it("picks up newly added sessions on repeat calls without a reset", () => {
// Prime the index (first call builds it).
writeSession(dir, "first.jsonl", "id-first");
assert.equal(resolveSessionFileById("id-first", dir) !== null, true);
// Add a new session AFTER the index was built — no reset. The resolver's
// cheap refresh should index it.
const b = writeSession(dir, "second.jsonl", "id-second");
assert.equal(resolveSessionFileById("id-second", dir), b);
});
});
describe("subagent loadout snapshot", () => {
const sample: SubagentLoadout = {
agent: "worker",
toolAllowlist: "read,write,edit,safe_bash,web_search,subagent,ask_question",
model: "openrouter/z-ai/glm-5.2",
thinking: "medium",
systemPromptMode: "append",
identity: "You are a worker agent.",
spawnable: ["scout", "researcher"],
autoExit: true,
cwd: "/work/dir",
agentDir: "/home/u/.pi/agent",
};
it("writes the sidecar next to the session file", () => {
const sf = join(dir, "s1.jsonl");
writeSubagentLoadout(sf, sample);
assert.equal(loadoutSidecarPath(sf), sf + ".loadout.json");
assert.ok(existsSync(sf + ".loadout.json"));
});
it("round-trips the full loadout", () => {
const sf = join(dir, "s2.jsonl");
writeSubagentLoadout(sf, sample);
assert.deepEqual(readSubagentLoadout(sf), sample);
});
it("returns null when the sidecar is absent", () => {
assert.equal(readSubagentLoadout(join(dir, "missing.jsonl")), null);
});
it("returns null when the sidecar is corrupt", () => {
const sf = join(dir, "s3.jsonl");
writeFileSync(sf + ".loadout.json", "not json{", "utf8");
assert.equal(readSubagentLoadout(sf), null);
});
});
describe("subagent name registry", () => {
it("registers and resolves a name to its session file", () => {
const adir = join(dir, "art-1");
registerName(adir, "worker", { sessionFile: "/s/worker.jsonl", sessionId: "id-worker" });
const entry = resolveNameInRegistry(adir, "worker");
assert.deepEqual(entry, { sessionFile: "/s/worker.jsonl", sessionId: "id-worker" });
assert.ok(existsSync(nameRegistryPath(adir)));
});
it("accumulates multiple names and overwrites on re-register", () => {
const adir = join(dir, "art-2");
registerName(adir, "scout", { sessionFile: "/s/scout.jsonl", sessionId: "id-scout" });
registerName(adir, "scout-2", { sessionFile: "/s/scout2.jsonl", sessionId: "id-scout2" });
const reg = readNameRegistry(adir);
assert.deepEqual(Object.keys(reg).sort(), ["scout", "scout-2"]);
// Overwrite scout with a new session file.
registerName(adir, "scout", { sessionFile: "/s/scout-new.jsonl", sessionId: "id-scout-new" });
assert.equal(resolveNameInRegistry(adir, "scout")!.sessionFile, "/s/scout-new.jsonl");
});
it("returns null for unknown names and {} for a missing/corrupt registry", () => {
const adir = join(dir, "art-3");
assert.equal(resolveNameInRegistry(adir, "nope"), null);
assert.deepEqual(readNameRegistry(adir), {});
mkdirSync(adir, { recursive: true });
writeFileSync(nameRegistryPath(adir), "not json{", "utf8");
assert.deepEqual(readNameRegistry(adir), {});
});
});
describe("findLastAssistantMessage", () => {
it("finds last assistant text", () => {
const entries = [USER_MSG, ASSISTANT_MSG, ASSISTANT_MSG_2] as any[];
const text = findLastAssistantMessage(entries);
assert.equal(text, "Updated plan with details.");
});
it("skips thinking blocks, gets text only", () => {
const entries = [ASSISTANT_MSG_2] as any[];
const text = findLastAssistantMessage(entries);
assert.equal(text, "Updated plan with details.");
});
it("skips tool results", () => {
const entries = [ASSISTANT_MSG, TOOL_RESULT] as any[];
const text = findLastAssistantMessage(entries);
assert.equal(text, "Here is my plan...");
});
it("returns null when no assistant messages", () => {
const entries = [USER_MSG] as any[];
assert.equal(findLastAssistantMessage(entries), null);
});
it("returns null for empty array", () => {
assert.equal(findLastAssistantMessage([]), null);
});
it("skips empty assistant messages and returns real content above", () => {
const realMsg = {
type: "message",
message: {
role: "assistant",
content: [{ type: "text", text: "Real summary content." }],
},
};
const emptyMsg = {
type: "message",
message: {
role: "assistant",
content: [],
},
};
const entries = [realMsg, emptyMsg] as any[];
assert.equal(findLastAssistantMessage(entries), "Real summary content.");
});
it("surfaces errorMessage when last assistant ended with stopReason=error and no text", () => {
// Reproduces the overload-exhaustion case: an earlier turn looked
// normal, then the provider went 529 and auto-retry gave up. Without
// the errorMessage fallback we'd return the stale earlier summary and
// the orchestrator would believe the subagent completed.
const earlierGood = {
type: "message",
message: {
role: "assistant",
content: [{ type: "text", text: "Investigating the bug..." }],
},
};
const overloadError = {
type: "message",
message: {
role: "assistant",
content: [],
stopReason: "error",
errorMessage: "Anthropic 529 Overloaded after 3 retries",
},
};
const entries = [earlierGood, overloadError] as any[];
assert.equal(
findLastAssistantMessage(entries),
"Subagent error: Anthropic 529 Overloaded after 3 retries",
);
});
it("prefers text content even when an error stopReason is set", () => {
// If the model produced text before the error (rare but possible), we
// prefer the actual content over the synthetic error fallback.
const msg = {
type: "message",
message: {
role: "assistant",
content: [{ type: "text", text: "Here is partial output." }],
stopReason: "error",
errorMessage: "stream interrupted",
},
};
assert.equal(findLastAssistantMessage([msg] as any[]), "Here is partial output.");
});
it("does not invent a summary for a stop=error message with no errorMessage", () => {
const msg = {
type: "message",
message: {
role: "assistant",
content: [],
stopReason: "error",
},
};
assert.equal(findLastAssistantMessage([msg] as any[]), null);
});
});
describe("appendBranchSummary", () => {
it("appends valid branch_summary entry", () => {
const file = createSessionFile(dir, [SESSION_HEADER, USER_MSG, ASSISTANT_MSG]);
const id = appendBranchSummary(file, "user-001", "asst-001", "The plan was created.");
assert.ok(id, "should return an id");
assert.equal(typeof id, "string");
// Read back and verify
const lines = readFileSync(file, "utf8").trim().split("\n");
assert.equal(lines.length, 4); // 3 original + 1 summary
const summary = JSON.parse(lines[3]);
assert.equal(summary.type, "branch_summary");
assert.equal(summary.id, id);
assert.equal(summary.parentId, "user-001");
assert.equal(summary.fromId, "asst-001");
assert.equal(summary.summary, "The plan was created.");
assert.ok(summary.timestamp);
});
it("uses branchPointId as fromId fallback", () => {
const file = createSessionFile(dir, [SESSION_HEADER]);
appendBranchSummary(file, "branch-pt", null, "summary");
const lines = readFileSync(file, "utf8").trim().split("\n");
const summary = JSON.parse(lines[1]);
assert.equal(summary.fromId, "branch-pt");
});
});
describe("copySessionFile", () => {
it("creates a copy with different path", () => {
const file = createSessionFile(dir, [SESSION_HEADER, USER_MSG]);
const copyDir = join(dir, "copies");
mkdirSync(copyDir, { recursive: true });
const copy = copySessionFile(file, copyDir);
assert.notEqual(copy, file);
assert.ok(copy.endsWith(".jsonl"));
assert.equal(readFileSync(copy, "utf8"), readFileSync(file, "utf8"));
});
});
describe("seedSubagentSessionFile", () => {
it("creates a lineage-only child session with parent linkage and no copied turns", () => {
const parentFile = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE, USER_MSG, ASSISTANT_MSG]);
const childFile = join(dir, "lineage-child.jsonl");
seedSubagentSessionFile({
mode: "lineage-only",
parentSessionFile: parentFile,
childSessionFile: childFile,
childCwd: "/tmp/child-cwd",
});
const lines = readFileSync(childFile, "utf8").trim().split("\n");
assert.equal(lines.length, 1);
const header = JSON.parse(lines[0]);
assert.equal(header.type, "session");
assert.equal(header.parentSession, parentFile);
assert.equal(header.cwd, "/tmp/child-cwd");
});
it("creates a forked child session with copied context before the triggering user turn", () => {
const parentFile = createSessionFile(dir, [SESSION_HEADER, MODEL_CHANGE, USER_MSG, ASSISTANT_MSG]);
const childFile = join(dir, "fork-child.jsonl");
seedSubagentSessionFile({
mode: "fork",
parentSessionFile: parentFile,
childSessionFile: childFile,
childCwd: "/tmp/fork-child-cwd",
});
const entries = readFileSync(childFile, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line));
assert.equal(entries.length, 2);
assert.equal(entries[0].type, "session");
assert.equal(entries[0].parentSession, parentFile);
assert.equal(entries[0].cwd, "/tmp/fork-child-cwd");
assert.equal(entries[1].type, "model_change");
assert.equal(entries.some((entry) => entry.type === "session" && entry.parentSession !== parentFile), false);
assert.equal(entries.some((entry) => entry.type === "message"), false);
});
});
describe("mergeNewEntries", () => {
it("appends new entries from source to target", () => {
// Source starts with same base (2 entries), then has 1 new entry
const sourceFile = join(dir, "merge-source.jsonl");
const targetFile = join(dir, "merge-target.jsonl");
writeFileSync(
sourceFile,
[SESSION_HEADER, USER_MSG, ASSISTANT_MSG].map((e) => JSON.stringify(e)).join("\n") + "\n",
);
writeFileSync(
targetFile,
[SESSION_HEADER, USER_MSG].map((e) => JSON.stringify(e)).join("\n") + "\n",
);
// Merge entries after line 2 (the shared base)
const merged = mergeNewEntries(sourceFile, targetFile, 2);
assert.equal(merged.length, 1);
assert.equal(merged[0].id, "asst-001");
// Target should now have 3 entries
const targetLines = readFileSync(targetFile, "utf8").trim().split("\n");
assert.equal(targetLines.length, 3);
});
});
describe("summarizeSessionStats", () => {
const asstWithUsage = (id: string, opts: {
model?: string;
tools?: string[];
usage?: Record<string, unknown>;
}) => ({
type: "message",
id,
parentId: "user-001",
message: {
role: "assistant",
...(opts.model ? { model: opts.model } : {}),
content: [
{ type: "text", text: "ok" },
...(opts.tools ?? []).map((name, i) => ({ type: "toolCall", name, id: `${id}-tc${i}` })),
],
...(opts.usage ? { usage: opts.usage } : {}),
},
});
it("aggregates tokens/cost cumulatively and tracks last context size", () => {
const file = createSessionFile(dir, [
SESSION_HEADER,
{ type: "model_change", id: "mc-001", parentId: null, modelId: "claude-sonnet-4-6" },
USER_MSG,
asstWithUsage("a1", {
tools: ["read", "grep"],
usage: { input: 100, output: 50, cacheRead: 1000, cacheWrite: 200, totalTokens: 1350, cost: { total: 0.01 } },
}),
asstWithUsage("a2", {
tools: ["write"],
usage: { input: 30, output: 70, cacheRead: 2000, cacheWrite: 0, totalTokens: 3500, cost: { total: 0.02 } },
}),
]);
const stats = summarizeSessionStats(file)!;
assert.equal(stats.model, "claude-sonnet-4-6");
assert.equal(stats.toolCount, 3);
assert.equal(stats.inputTokens, 130);
assert.equal(stats.outputTokens, 120);
assert.equal(stats.cacheReadTokens, 3000);
assert.equal(stats.cacheWriteTokens, 200);
// contextTokens is the LAST assistant turn's totalTokens, not the sum.
assert.equal(stats.contextTokens, 3500);
assert.ok(Math.abs(stats.cost - 0.03) < 1e-9);
});
it("prefers per-message model over model_change", () => {
const file = createSessionFile(dir, [
SESSION_HEADER,
{ type: "model_change", id: "mc-001", parentId: null, modelId: "claude-haiku-4-5" },
asstWithUsage("a1", { model: "claude-sonnet-4-6", usage: { totalTokens: 10, cost: { total: 0 } } }),
]);
assert.equal(summarizeSessionStats(file)!.model, "claude-sonnet-4-6");
});
it("handles missing usage gracefully", () => {
const file = createSessionFile(dir, [SESSION_HEADER, USER_MSG, ASSISTANT_MSG]);
const stats = summarizeSessionStats(file)!;
assert.equal(stats.toolCount, 0);
assert.equal(stats.inputTokens, 0);
assert.equal(stats.cost, 0);
assert.equal(stats.contextTokens, 0);
});
it("returns null for an unreadable file", () => {
assert.equal(summarizeSessionStats(join(dir, "does-not-exist.jsonl")), null);
});
});
});
describe("status.ts", () => {
it("parses strict config objects", () => {
const disabled = parseStatusConfig({ status: { enabled: false } });
assert.deepEqual(disabled, {
enabled: false,
lineLimit: 4,
});
});
it("loads a valid config file", () => {
const examplePath = fileURLToPath(new URL("../config.json.example", import.meta.url));
const config = loadStatusConfig(examplePath);
assert.deepEqual(config, {
enabled: true,
lineLimit: 4,
});
});
it("loads the shared example when local config is absent", () => {
withTempDir((dir) => {
const examplePath = join(dir, "config.json.example");
writeFileSync(
examplePath,
JSON.stringify({ status: { enabled: true } }, null, 2) + "\n",
);
const config = loadStatusConfig(join(dir, "config.json"), examplePath);
assert.deepEqual(config, {
enabled: true,
lineLimit: 4,
});
});
});
it("fails fast for invalid config shapes", () => {
assert.throws(
() => parseStatusConfig({ status: { enabled: "false" } }),
/status\.enabled must be a boolean/,
);
assert.throws(
() => parseStatusConfig({ status: { enabled: true, defaultCadenceSeconds: 60 } }),
/status has unsupported key\(s\): defaultCadenceSeconds/,
);
});
it("reports when neither local nor shared config exists", () => {
withTempDir((dir) => {
assert.throws(
() => loadStatusConfig(join(dir, "config.json"), join(dir, "config.json.example")),
/Missing subagent status config\. Expected .*config\.json.*or.*config\.json\.example/,
);
});
});
it("reports invalid JSON from the shared example path", () => {
withTempDir((dir) => {
const examplePath = join(dir, "config.json.example");
writeFileSync(examplePath, "{\n");
assert.throws(
() => loadStatusConfig(join(dir, "config.json"), examplePath),
/Invalid JSON in subagent config .*config\.json\.example/,
);
});
});
it("fails on invalid local config instead of falling back to the shared example", () => {
withTempDir((dir) => {
const configPath = join(dir, "config.json");
const examplePath = join(dir, "config.json.example");
writeFileSync(configPath, "{\n");
writeFileSync(
examplePath,
JSON.stringify({ status: { enabled: true } }, null, 2) + "\n",
);
assert.throws(
() => loadStatusConfig(configPath, examplePath),
/Invalid JSON in subagent config .*config\.json/,
);
});
});
it("keeps a missing snapshot as starting until the fixed watchdog threshold", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, { snapshot: "missing" }, 1_000);
assert.equal(classifyStatus(state, 60_999).kind, "starting");
const stalled = classifyStatus(state, 61_000);
assert.equal(stalled.kind, "stalled");
assert.equal(stalled.statusLabel, null);
});
it("classifies active snapshots without aging into stalled", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
latestEvent: "tool_execution_start",
}, 5_000);
const snapshot = classifyStatus(state, 240_000);
assert.equal(snapshot.kind, "active");
assert.equal(snapshot.activityLabel, "bash");
assert.equal(snapshot.activeDurationText, "3m");
});
it("classifies waiting snapshots as healthy idle without becoming stalled", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 10_000,
sequence: 1,
phase: "waiting",
waitingSince: 10_000,
latestEvent: "agent_end",
}, 10_000);
const snapshot = classifyStatus(state, 240_000);
assert.equal(snapshot.kind, "waiting");
assert.equal(snapshot.waitingDurationText, "3m");
});
it("uses elapsed-only fallback for claude-backed subagents", () => {
const state = createStatusState({ source: "claude", startTimeMs: 0 });
const snapshot = classifyStatus(state, 125_000);
assert.equal(snapshot.kind, "running");
assert.equal(snapshot.elapsedText, "2m");
});
it("detects stalled transitions and recovery", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, { snapshot: "missing" }, 1_000);
let advanced = advanceStatusState(state, 95_000);
assert.equal(advanced.transition, "stalled");
assert.equal(advanced.snapshot.kind, "stalled");
state = observeStatus(advanced.nextState, {
snapshot: "present",
updatedAt: 96_000,
sequence: 1,
phase: "waiting",
waitingSince: 96_000,
latestEvent: "agent_end",
}, 96_000);
advanced = advanceStatusState(state, 97_000);
assert.equal(advanced.transition, "recovered");
assert.equal(advanced.snapshot.kind, "waiting");
});
it("keeps the last healthy kind during transient snapshot loss", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "streaming",
activeSince: 5_000,
}, 5_000);
state = advanceStatusState(state, 6_000).nextState;
state = observeStatus(state, { snapshot: "missing" }, 10_000);
const snapshot = classifyStatus(state, 20_000);
assert.equal(snapshot.kind, "active");
assert.equal(snapshot.statusLabel, null);
});
it("forces an active state to waiting after interrupt", () => {
const now = 20_000;
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
}, 5_000);
assert.equal(classifyStatus(state, now).kind, "active");
const forced = forceStatusAfterInterrupt(state, now);
const snapshot = classifyStatus(forced, now);
assert.equal(snapshot.kind, "waiting");
assert.equal(snapshot.activityLabel, "interrupted");
assert.equal(snapshot.waitingDurationText, "0s");
assert.equal(forced.activeNow, false);
});
it("orders same-millisecond snapshots by sequence", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 10_000,
sequence: 2,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 10_000,
activityLabel: "bash",
}, 10_000);
state = observeStatus(state, {
snapshot: "present",
updatedAt: 10_000,
sequence: 3,
phase: "waiting",
waitingSince: 10_000,
latestEvent: "agent_end",
}, 10_001);
const snapshot = classifyStatus(state, 11_000);
assert.equal(snapshot.kind, "waiting");
assert.equal(snapshot.latestEvent, "agent_end");
});
it("recovers from a transient snapshot read failure with the same valid snapshot", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 2,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
}, 5_000);
state = observeStatus(state, { snapshot: "missing" }, 10_000);
assert.equal(classifyStatus(state, 10_000).statusLabel, null);
state = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 2,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
}, 11_000);
const snapshot = classifyStatus(state, 11_000);
assert.equal(snapshot.kind, "active");
assert.equal(snapshot.statusLabel, null);
});
it("ignores stale and exact old snapshots after interrupt and accepts newer snapshots", () => {
let state = createStatusState({ source: "pi", startTimeMs: 0 });
state = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
}, 5_000);
state = forceStatusAfterInterrupt(state, 20_000);
const stale = observeStatus(state, {
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
}, 21_000);
let snapshot = classifyStatus(stale, 21_000);
assert.equal(snapshot.kind, "waiting");
assert.equal(snapshot.activityLabel, "interrupted");
const sameTimestamp = observeStatus(stale, {
snapshot: "present",
updatedAt: 20_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 20_000,
activityLabel: "bash",
}, 22_000);
snapshot = classifyStatus(sameTimestamp, 22_000);
assert.equal(snapshot.kind, "waiting");
assert.equal(snapshot.activityLabel, "interrupted");
const resumed = observeStatus(sameTimestamp, {
snapshot: "present",
sequence: 2,
updatedAt: 25_000,
phase: "active",
active: true,
activeScope: "streaming",
activeSince: 25_000,
activityLabel: "streaming",
}, 25_000);
snapshot = classifyStatus(resumed, 25_000);
assert.equal(snapshot.kind, "active");
assert.equal(resumed.activeScope, "streaming");
});
it("normalizes and truncates long newline-heavy names", () => {
const longName = `Worker\n\n${"very-long-name-".repeat(12)}`;
const stalledState = observeStatus(
createStatusState({ source: "pi", startTimeMs: 0 }),
{ snapshot: "missing" },
1_000,
);
const activeState = observeStatus(
createStatusState({ source: "pi", startTimeMs: 0 }),
{
snapshot: "present",
updatedAt: 299_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 299_000,
activityLabel: "write",
},
299_000,
);
const line = formatStatusLine(longName, classifyStatus(stalledState, 240_000));
const recovered = formatTransitionLine(longName, classifyStatus(activeState, 300_000), "recovered");
assert.doesNotMatch(line, /\n/);
assert.doesNotMatch(recovered, /\n/);
assert.ok(line.length <= 120, `expected bounded line length, got ${line.length}`);
assert.ok(recovered.length <= 120, `expected bounded line length, got ${recovered.length}`);
});
it("caps visible status lines and reports overflow consistently", () => {
const waitingState = observeStatus(
createStatusState({ source: "pi", startTimeMs: 0 }),
{ snapshot: "present", updatedAt: 180_000, sequence: 1, phase: "waiting", waitingSince: 180_000 },
180_000,
);
const activeState = observeStatus(
createStatusState({ source: "pi", startTimeMs: 0 }),
{
snapshot: "present",
updatedAt: 419_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 419_000,
activityLabel: "bash",
},
419_000,
);
const waitingLine = formatStatusLine("Worker", classifyStatus(waitingState, 300_000));
const recoveredLine = formatTransitionLine("Worker", classifyStatus(activeState, 420_000), "recovered");
const lines = [waitingLine, recoveredLine, "Scout running 2m.", "Reviewer running 4m.", "Planner running 6m."];
const capped = capStatusLines(lines, 3);
const aggregate = formatStatusAggregate(lines, 3);
assert.equal(waitingLine, "Worker running 5m, waiting 2m.");
assert.equal(recoveredLine, "Worker running 7m, recovered; active (bash 1s).");
assert.deepEqual(capped.visibleLines, [waitingLine, recoveredLine, "Scout running 2m."]);
assert.equal(capped.overflow, 2);
assert.match(aggregate, /^Subagent status:/);
assert.match(aggregate, /\+2 more running\./);
assert.doesNotMatch(aggregate, /\/tmp|\.jsonl/);
});
});
describe("subagent discovery", () => {
const testApi = (subagentsModule as any).__test__;
it("loads session-mode from frontmatter", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
projectAgentsDir,
"lineage-mode-test-agent",
[
"name: lineage-mode-test-agent",
"model: anthropic/test-lineage",
"session-mode: lineage-only",
].join("\n"),
);
const loaded = testApi.loadAgentDefaults("lineage-mode-test-agent");
assert.ok(loaded, "expected agent to load");
assert.equal(loaded.sessionMode, "lineage-only");
});
});
it("loads explicit interactive flag from frontmatter", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
projectAgentsDir,
"interactive-true-test-agent",
[
"name: interactive-true-test-agent",
"model: anthropic/test-interactive-true",
"interactive: true",
].join("\n"),
);
writeAgentFile(
projectAgentsDir,
"interactive-false-test-agent",
[
"name: interactive-false-test-agent",
"model: anthropic/test-interactive-false",
"interactive: false",
].join("\n"),
);
const loadedTrue = testApi.loadAgentDefaults("interactive-true-test-agent");
assert.equal(loadedTrue?.interactive, true);
const loadedFalse = testApi.loadAgentDefaults("interactive-false-test-agent");
assert.equal(loadedFalse?.interactive, false);
});
});
it("leaves interactive undefined when not set in frontmatter", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
projectAgentsDir,
"interactive-unset-test-agent",
[
"name: interactive-unset-test-agent",
"model: anthropic/test-interactive-unset",
].join("\n"),
);
const loaded = testApi.loadAgentDefaults("interactive-unset-test-agent");
assert.equal(loaded?.interactive, undefined);
});
});
it("resolveEffectiveInteractive defaults to the inverse of auto-exit", () => {
// Autonomous agents (auto-exit: true) are NOT interactive — parent gets stall pings.
assert.equal(
testApi.resolveEffectiveInteractive({ name: "A", task: "T" }, { autoExit: true }),
false,
);
// Agents without auto-exit ARE interactive — parent does not receive status transition pings.
assert.equal(
testApi.resolveEffectiveInteractive({ name: "A", task: "T" }, { autoExit: false }),
true,
);
assert.equal(
testApi.resolveEffectiveInteractive({ name: "A", task: "T" }, {}),
true,
);
// Bare spawn with no agent defs (e.g. /iterate fork) is interactive by default.
assert.equal(
testApi.resolveEffectiveInteractive({ name: "A", task: "T" }, null),
true,
);
});
it("resolveEffectiveInteractive honors explicit frontmatter over the auto-exit default", () => {
// Autonomous agent that still wants to be treated as interactive.
assert.equal(
testApi.resolveEffectiveInteractive(
{ name: "A", task: "T" },
{ autoExit: true, interactive: true },
),
true,
);
// Non-auto-exit agent that opts back into stall pings.
assert.equal(
testApi.resolveEffectiveInteractive(
{ name: "A", task: "T" },
{ interactive: false },
),
false,
);
});
it("bundled scout/researcher/worker use the configured model and resolve as non-interactive", () => {
for (const name of ["scout", "researcher", "worker"]) {
const defs = testApi.loadAgentDefaults(name);
assert.ok(defs, `expected bundled agent ${name} to be discoverable`);
assert.equal(defs.model, "openai-codex/gpt-5.6-luna");
assert.equal(defs.thinking, "xhigh");
assert.equal(
testApi.resolveEffectiveInteractive({ name, task: "" }, defs),
false,
`${name} should resolve as non-interactive (autonomous, auto-exit)`,
);
}
});
it("worker is granted the spawning toolset restricted to scout and researcher", () => {
const worker = testApi.loadAgentDefaults("worker");
assert.ok(worker, "expected bundled worker to be discoverable");
assert.deepEqual(worker.subagentAgents, ["scout", "researcher"]);
const allowlist = testApi.buildSubagentToolAllowlist(worker.tools, { grantSpawning: true });
assert.ok(allowlist, "expected an allowlist");
const tools = new Set(allowlist!.split(","));
for (const t of ["subagent", "subagent_message", "subagents_list"]) {
assert.ok(tools.has(t), `expected spawning tool ${t} in worker allowlist`);
}
assert.ok(tools.has("bash"), "expected worker to keep bash");
});
it("scout and researcher are not granted spawning tools", () => {
for (const name of ["scout", "researcher"]) {
const defs = testApi.loadAgentDefaults(name);
assert.ok(defs, `expected bundled agent ${name} to be discoverable`);
assert.equal(defs.subagentAgents, undefined, `${name} should not declare subagent_agents`);
}
});
it("getToolExtensionPath maps custom tools and skips built-ins", () => {
assert.equal(testApi.getToolExtensionPath("read"), undefined);
assert.equal(testApi.getToolExtensionPath("bash"), undefined);
assert.ok(testApi.getToolExtensionPath("safe_bash")?.endsWith("tools/safe-bash.ts"));
// Spawning tools are registered by this extension itself.
assert.ok(testApi.getToolExtensionPath("subagent")?.endsWith("index.ts"));
withTempDir((dir) => {
const extensionPath = join(dir, "portable-test-tool.ts");
writeFileSync(extensionPath, "export default function () {}\n");
subagentsModule.registerToolExtension("portable_test_tool", extensionPath);
assert.equal(testApi.getToolExtensionPath("portable_test_tool"), extensionPath);
});
});
it("ignores invalid session-mode values", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
projectAgentsDir,
"invalid-mode-test-agent",
[
"name: invalid-mode-test-agent",
"model: anthropic/test-invalid",
"session-mode: sideways",
].join("\n"),
);
const loaded = testApi.loadAgentDefaults("invalid-mode-test-agent");
assert.ok(loaded, "expected agent to load");
assert.equal(loaded.sessionMode, undefined);
});
});
it("resolves session mode from frontmatter (standalone default)", () => {
assert.equal(testApi.resolveEffectiveSessionMode({ name: "A", task: "T" }, null), "standalone");
assert.equal(
testApi.resolveEffectiveSessionMode({ name: "A", task: "T" }, { sessionMode: "lineage-only" }),
"lineage-only",
);
assert.equal(
testApi.resolveEffectiveSessionMode({ name: "A", task: "T" }, { sessionMode: "fork" }),
"fork",
);
});
it("resolves launch behavior for standalone, lineage-only, and fork modes", () => {
assert.deepEqual(testApi.resolveLaunchBehavior({ name: "A", task: "T" }, null), {
sessionMode: "standalone",
seededSessionMode: null,
inheritsConversationContext: false,
taskDelivery: "artifact",
});
assert.deepEqual(
testApi.resolveLaunchBehavior({ name: "A", task: "T" }, { sessionMode: "lineage-only" }),
{
sessionMode: "lineage-only",
seededSessionMode: "lineage-only",
inheritsConversationContext: false,
taskDelivery: "artifact",
},
);
assert.deepEqual(
testApi.resolveLaunchBehavior({ name: "A", task: "T" }, { sessionMode: "fork" }),
{
sessionMode: "fork",
seededSessionMode: "fork",
inheritsConversationContext: true,
taskDelivery: "direct",
},
);
});
it("buildSubagentToolAllowlist preserves requested tools and adds child control tools", () => {
assert.equal(
testApi.buildSubagentToolAllowlist("read,bash,web_search"),
"read,bash,web_search,ask_question",
);
});
it("buildSubagentToolAllowlist returns null without an explicit tool restriction", () => {
assert.equal(testApi.buildSubagentToolAllowlist(undefined), null);
assert.equal(testApi.buildSubagentToolAllowlist(""), null);
});
it("applySandboxToParts replays model, identity, and default-deny tool restriction", () => {
withTempDir((d) => {
const parts: string[] = [];
testApi.applySandboxToParts(
parts,
{
agent: "worker",
toolAllowlist: "read,write,safe_bash",
model: "openrouter/z-ai/glm-5.2",
thinking: "medium",
systemPromptMode: "append",
identity: "You are a worker.",
spawnable: ["scout"],
autoExit: true,
cwd: null,
agentDir: null,
},
{ artifactDir: d, name: "worker" },
);
const joined = parts.join(" ");
// Model with thinking suffix.
assert.ok(joined.includes("--model"), "expected --model");
assert.ok(joined.includes("openrouter/z-ai/glm-5.2:medium"), "expected model:thinking");
// Identity written to a file and appended.
assert.ok(joined.includes("--append-system-prompt"), "expected --append-system-prompt");
// Default-deny restriction.
assert.ok(parts.includes("--no-extensions"), "expected --no-extensions");
const toolsIdx = parts.indexOf("--tools");
assert.ok(toolsIdx >= 0, "expected --tools");
// The value is shell-escaped (single-quoted) before joining.
assert.ok(
parts[toolsIdx + 1].includes("read,write,safe_bash"),
"expected the tool allowlist as the --tools value",
);
});
});
it("applySandboxToParts omits restriction flags when the loadout was unrestricted", () => {
withTempDir((d) => {
const parts: string[] = [];
testApi.applySandboxToParts(
parts,
{
agent: null,
toolAllowlist: null,
model: null,
thinking: null,
systemPromptMode: null,
identity: null,
spawnable: null,
autoExit: false,
cwd: null,
agentDir: null,
},
{ artifactDir: d, name: "fork" },
);
assert.deepEqual(parts, []);
});
});
it("buildPiPromptArgs inserts separator for artifact-backed launches with skills", () => {
assert.deepEqual(
testApi.buildPiPromptArgs({ effectiveSkills: "review,lint", taskDelivery: "artifact", taskArg: "@artifact.md" }),
["", "/skill:review", "/skill:lint", "@artifact.md"],
);
});
it("buildPiPromptArgs omits separator for artifact-backed launches without skills", () => {
assert.deepEqual(
testApi.buildPiPromptArgs({ effectiveSkills: undefined, taskDelivery: "artifact", taskArg: "@artifact.md" }),
["@artifact.md"],
);
});
it("buildPiPromptArgs omits separator for direct launches with skills", () => {
assert.deepEqual(
testApi.buildPiPromptArgs({ effectiveSkills: "review", taskDelivery: "direct", taskArg: "do the task" }),
["/skill:review", "do the task"],
);
});
it("lists visible agents from discovery", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
projectAgentsDir,
"visible-discovery-test-agent",
[
"name: visible-discovery-test-agent",
"description: Visible test agent",
"model: anthropic/test-visible",
].join("\n"),
);
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const tool = registeredTools.find((tool) => tool.name === "subagents_list");
assert.ok(tool, "expected subagents_list to be registered");
const result = await tool.execute();
const agents = result.details?.agents ?? [];
assert.ok(agents.some((agent: any) => agent.name === "visible-discovery-test-agent"));
assert.match(result.content[0].text, /visible-discovery-test-agent/);
});
});
it("hides disable-model-invocation agents from listings but keeps direct loading", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir }) => {
writeAgentFile(
projectAgentsDir,
"hidden-discovery-test-agent",
[
"name: hidden-discovery-test-agent",
"description: Hidden test agent",
"model: anthropic/test-hidden",
"disable-model-invocation: true",
].join("\n"),
"You are the hidden agent.",
);
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const tool = registeredTools.find((tool) => tool.name === "subagents_list");
assert.ok(tool, "expected subagents_list to be registered");
const result = await tool.execute();
const agents = result.details?.agents ?? [];
assert.equal(agents.some((agent: any) => agent.name === "hidden-discovery-test-agent"), false);
assert.doesNotMatch(result.content[0].text, /hidden-discovery-test-agent/);
const loaded = testApi.loadAgentDefaults("hidden-discovery-test-agent");
assert.ok(loaded, "expected hidden agent to remain directly loadable");
assert.equal(loaded.model, "anthropic/test-hidden");
assert.equal(loaded.body, "You are the hidden agent.");
assert.equal(loaded.disableModelInvocation, true);
});
});
it("lets a hidden project agent shadow a visible global agent", async () => {
await withIsolatedAgentEnv(async ({ projectAgentsDir, globalAgentsDir }) => {
writeAgentFile(
globalAgentsDir,
"shadowed-discovery-test-agent",
[
"name: shadowed-discovery-test-agent",
"description: Global visible agent",
"model: anthropic/test-global",
].join("\n"),
"You are the global visible agent.",
);
writeAgentFile(
projectAgentsDir,
"shadowed-discovery-test-agent",
[
"name: shadowed-discovery-test-agent",
"description: Project hidden agent",
"model: anthropic/test-project",
"disable-model-invocation: true",
].join("\n"),
"You are the project hidden agent.",
);
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const tool = registeredTools.find((tool) => tool.name === "subagents_list");
assert.ok(tool, "expected subagents_list to be registered");
const result = await tool.execute();
const agents = result.details?.agents ?? [];
assert.equal(agents.some((agent: any) => agent.name === "shadowed-discovery-test-agent"), false);
assert.doesNotMatch(result.content[0].text, /shadowed-discovery-test-agent/);
const loaded = testApi.loadAgentDefaults("shadowed-discovery-test-agent");
assert.ok(loaded, "expected project override to remain directly loadable");
assert.equal(loaded.model, "anthropic/test-project");
assert.equal(loaded.body, "You are the project hidden agent.");
assert.equal(loaded.disableModelInvocation, true);
});
});
});
describe("subagent-done.ts", () => {
describe("shouldMarkUserTookOver", () => {
it("ignores the initial injected task before the first agent run", () => {
assert.equal(shouldMarkUserTookOver(false), false);
});
it("treats later input as manual takeover", () => {
assert.equal(shouldMarkUserTookOver(true), true);
});
});
describe("shouldAutoExitOnAgentEnd", () => {
it("auto-exits after normal completion when there was no takeover", () => {
const messages = [{ role: "assistant", stopReason: "stop" }];
assert.equal(shouldAutoExitOnAgentEnd(false, messages), true);
});
it("auto-exits after normal completion even when the user sent the prompt", () => {
const messages = [{ role: "assistant", stopReason: "stop" }];
assert.equal(shouldAutoExitOnAgentEnd(true, messages), true);
});
it("stays open after Escape aborts the run", () => {
const messages = [{ role: "assistant", stopReason: "aborted" }];
assert.equal(shouldAutoExitOnAgentEnd(false, messages), false);
});
it("still exits when the latest turn ended with stopReason=error", () => {
// Auto-exit subagents must shut down on retry-exhaustion errors so the
// parent is woken. The error sidecar (written separately) carries the
// failure detail; staying open would just strand the worker.
const messages = [{ role: "assistant", stopReason: "error", errorMessage: "529 overloaded" }];
assert.equal(shouldAutoExitOnAgentEnd(false, messages), true);
});
});
describe("findLatestAssistantError", () => {
it("returns the error info from a stopReason=error message", () => {
const messages = [
{ role: "assistant", stopReason: "stop", content: [{ type: "text", text: "ok" }] },
{ role: "toolResult", content: [] },
{ role: "assistant", stopReason: "error", errorMessage: "Anthropic 529 Overloaded" },
];
assert.deepEqual(findLatestAssistantError(messages), {
errorMessage: "Anthropic 529 Overloaded",
stopReason: "error",
});
});
it("returns null when the latest assistant turn completed normally", () => {
const messages = [
{ role: "assistant", stopReason: "error", errorMessage: "old failure" },
{ role: "user", content: [] },
{ role: "assistant", stopReason: "stop", content: [{ type: "text", text: "done" }] },
];
assert.equal(findLatestAssistantError(messages), null);
});
it("returns null when the latest assistant turn was aborted by the user", () => {
const messages = [{ role: "assistant", stopReason: "aborted" }];
assert.equal(findLatestAssistantError(messages), null);
});
it("falls back to a placeholder when stopReason=error has no errorMessage field", () => {
const messages = [{ role: "assistant", stopReason: "error" }];
const info = findLatestAssistantError(messages);
assert.ok(info);
assert.equal(info!.stopReason, "error");
assert.match(info!.errorMessage, /stopReason=error/);
});
it("returns null when messages is undefined or empty", () => {
assert.equal(findLatestAssistantError(undefined), null);
assert.equal(findLatestAssistantError([]), null);
});
});
describe("runningChildrenCount", () => {
const KEY = Symbol.for("pi-subagents/running-children-count");
function withGlobal(value: unknown, run: () => void) {
const prev = (globalThis as any)[KEY];
(globalThis as any)[KEY] = value;
try {
run();
} finally {
(globalThis as any)[KEY] = prev;
}
}
it("returns 0 when the spawning tools aren't loaded (no global)", () => {
withGlobal(undefined, () => {
assert.equal(runningChildrenCount(), 0);
});
});
it("reflects the live child count published by index.ts", () => {
withGlobal(() => 3, () => {
assert.equal(runningChildrenCount(), 3);
});
});
it("treats zero/negative/non-number/throwing getters as 0", () => {
withGlobal(() => 0, () => assert.equal(runningChildrenCount(), 0));
withGlobal(() => -1, () => assert.equal(runningChildrenCount(), 0));
withGlobal(() => "two", () => assert.equal(runningChildrenCount(), 0));
withGlobal(() => { throw new Error("boom"); }, () => assert.equal(runningChildrenCount(), 0));
});
});
describe("ask_question tool", () => {
function setupSubagentExtension(sessionFile: string) {
const saved = {
session: process.env.PI_SUBAGENT_SESSION,
name: process.env.PI_SUBAGENT_NAME,
agent: process.env.PI_SUBAGENT_AGENT,
autoExit: process.env.PI_SUBAGENT_AUTO_EXIT,
};
process.env.PI_SUBAGENT_SESSION = sessionFile;
process.env.PI_SUBAGENT_NAME = "scout-2";
process.env.PI_SUBAGENT_AGENT = "scout";
process.env.PI_SUBAGENT_AUTO_EXIT = "1";
const mock = createMockExtensionApi();
subagentDoneExtension(mock.api);
const restore = () => {
restoreEnvVar("PI_SUBAGENT_SESSION", saved.session);
restoreEnvVar("PI_SUBAGENT_NAME", saved.name);
restoreEnvVar("PI_SUBAGENT_AGENT", saved.agent);
restoreEnvVar("PI_SUBAGENT_AUTO_EXIT", saved.autoExit);
};
return { mock, restore };
}
it("registers ask_question (and no caller_ping) with a single freeform question param", () => {
const dir = createTestDir();
const { mock, restore } = setupSubagentExtension(join(dir, "s.jsonl"));
try {
const names = mock.registeredTools.map((t) => t.name);
assert.ok(names.includes("ask_question"));
assert.ok(!names.includes("caller_ping"));
const tool = mock.registeredTools.find((t) => t.name === "ask_question");
assert.deepEqual(Object.keys(tool.parameters.properties), ["question"]);
assert.match(tool.description, /orchestrator/i);
} finally {
restore();
rmSync(dir, { recursive: true, force: true });
}
});
it("writes a .ask signal with name/agent/question and does NOT shut the session down", async () => {
const dir = createTestDir();
const sessionFile = join(dir, "s.jsonl");
const { mock, restore } = setupSubagentExtension(sessionFile);
try {
const tool = mock.registeredTools.find((t) => t.name === "ask_question");
let shutdownCalled = false;
const ctx = { shutdown() { shutdownCalled = true; } } as any;
const out = await tool.execute("call-1", { question: "Which API base URL?" }, undefined, undefined, ctx);
assert.equal(shutdownCalled, false, "ask_question must keep the session open");
assert.match(out.content[0].text, /wait/i);
const askFile = `${sessionFile}.ask`;
assert.ok(existsSync(askFile), ".ask signal file should be written");
const payload = JSON.parse(readFileSync(askFile, "utf-8"));
assert.equal(payload.question, "Which API base URL?");
assert.equal(payload.name, "scout-2");
assert.equal(payload.agent, "scout");
// No .exit sidecar — the session is not exiting.
assert.ok(!existsSync(`${sessionFile}.exit`));
} finally {
restore();
rmSync(dir, { recursive: true, force: true });
}
});
// Regression tests for the mid-run reply race: a reply steered in while the
// asking run is still open fires `input` but NOT `agent_start`, so the flag
// must be cleared on `input` or the session parks forever.
function setupCapturingExtension(sessionFile: string) {
const handlers = new Map<string, Array<(...args: any[]) => void>>();
const tools: any[] = [];
const api = {
on(event: string, handler: (...args: any[]) => void) {
if (!handlers.has(event)) handlers.set(event, []);
handlers.get(event)!.push(handler);
},
registerTool(t: any) { tools.push(t); },
registerCommand() {}, registerMessageRenderer() {}, registerShortcut() {},
sendUserMessage() {}, sendMessage() {}, getAllTools() { return []; },
} as any;
const saved = {
session: process.env.PI_SUBAGENT_SESSION,
name: process.env.PI_SUBAGENT_NAME,
agent: process.env.PI_SUBAGENT_AGENT,
autoExit: process.env.PI_SUBAGENT_AUTO_EXIT,
};
process.env.PI_SUBAGENT_SESSION = sessionFile;
process.env.PI_SUBAGENT_NAME = "scout-2";
process.env.PI_SUBAGENT_AGENT = "scout";
process.env.PI_SUBAGENT_AUTO_EXIT = "1";
subagentDoneExtension(api);
const emit = (event: string, ...args: any[]) =>
(handlers.get(event) ?? []).forEach((h) => h(...args));
const restore = () => {
restoreEnvVar("PI_SUBAGENT_SESSION", saved.session);
restoreEnvVar("PI_SUBAGENT_NAME", saved.name);
restoreEnvVar("PI_SUBAGENT_AGENT", saved.agent);
restoreEnvVar("PI_SUBAGENT_AUTO_EXIT", saved.autoExit);
};
const ask = async () => {
const tool = tools.find((t) => t.name === "ask_question");
await tool.execute("c1", { question: "v1 or v2?" }, undefined, undefined, { shutdown() {} });
};
return { emit, ask, restore };
}
it("exits (does not park) when the reply arrives mid-run via input", async () => {
const dir = createTestDir();
const { emit, ask, restore } = setupCapturingExtension(join(dir, "s.jsonl"));
try {
emit("agent_start");
await ask(); // sets awaitingAnswer mid-run
// Reply arrives MID-RUN as a steer: input fires, no new agent_start.
emit("input");
let shutdown = false;
emit("agent_end", { messages: [] }, { shutdown() { shutdown = true; } });
assert.equal(shutdown, true, "reply consumed mid-run → agent_end should exit, not park");
} finally {
restore();
rmSync(dir, { recursive: true, force: true });
}
});
it("parks as waiting at agent_end while the reply is still pending (no input yet)", async () => {
const dir = createTestDir();
const { emit, ask, restore } = setupCapturingExtension(join(dir, "s.jsonl"));
try {
emit("agent_start");
await ask();
// No input yet — the orchestrator has not replied.
let shutdown = false;
emit("agent_end", { messages: [] }, { shutdown() { shutdown = true; } });
assert.equal(shutdown, false, "pending question with no reply must park, not exit");
} finally {
restore();
rmSync(dir, { recursive: true, force: true });
}
});
it("exits when the reply arrives as a new turn (agent_start also clears the flag)", async () => {
const dir = createTestDir();
const { emit, ask, restore } = setupCapturingExtension(join(dir, "s.jsonl"));
try {
emit("agent_start");
await ask();
let shutdown1 = false;
emit("agent_end", { messages: [] }, { shutdown() { shutdown1 = true; } });
assert.equal(shutdown1, false, "parks while waiting");
// Reply arrives as a fresh turn after the subagent had parked.
emit("input");
emit("agent_start");
let shutdown2 = false;
emit("agent_end", { messages: [] }, { shutdown() { shutdown2 = true; } });
assert.equal(shutdown2, true, "after the reply turn, agent_end should exit");
} finally {
restore();
rmSync(dir, { recursive: true, force: true });
}
});
});
});
describe("surface exit signals", () => {
const { interpretExitSidecar, readCompletionFile } = __pollForExitTest__;
it("does not poll the terminal while waiting for a filesystem completion signal", async () => {
const dir = createTestDir();
const completionFile = join(dir, "child.exit-code");
let surfaceReads = 0;
try {
const result = await pollForExit(
"unused-surface",
new AbortController().signal,
{
interval: 1,
completionFile,
onTick() {
writeFileSync(completionFile, "7\n");
},
},
async () => {
surfaceReads += 1;
return "__SUBAGENT_DONE_0__";
},
);
assert.deepEqual(result, { reason: "sentinel", exitCode: 7 });
assert.equal(surfaceReads, 0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("bounds an asynchronous Orca screen read and kills its CLI process", async () => {
const dir = createTestDir();
const pidFile = join(dir, "reader.pid");
const cli = join(dir, "fake-orca");
writeFileSync(
cli,
`#!/usr/bin/env node\nrequire("node:fs").writeFileSync(${JSON.stringify(pidFile)}, String(process.pid));\nsetInterval(() => {}, 1000);\n`,
);
chmodSync(cli, 0o755);
try {
await assert.rejects(__orcaTest__.readScreenWithCli(cli, "unused-surface", 5, 500));
const pid = Number.parseInt(readFileSync(pidFile, "utf8"), 10);
let running = true;
try {
process.kill(pid, 0);
} catch {
running = false;
}
assert.equal(running, false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("reads and removes a shell completion file", () => {
withTempDir((dir) => {
const completionFile = join(dir, "child.exit-code");
writeFileSync(completionFile, "7\n");
assert.deepEqual(readCompletionFile(completionFile), {
reason: "sentinel",
exitCode: 7,
});
assert.equal(existsSync(completionFile), false);
assert.equal(readCompletionFile(completionFile), null);
});
});
it("no longer decodes ping payloads (ask_question keeps the session open instead)", () => {
// ask_question writes a `.ask` signal, not a `.exit` ping sidecar, so an
// unknown `type: "ping"` payload now falls through to a clean done.
assert.deepEqual(
interpretExitSidecar({ type: "ping", name: "Worker", message: "need help" }),
{ reason: "done", exitCode: 0 },
);
});
it("decodes done payloads", () => {
assert.deepEqual(interpretExitSidecar({ type: "done" }), {
reason: "done",
exitCode: 0,
});
});
it("decodes error payloads and propagates the message with a non-zero exit code", () => {
assert.deepEqual(
interpretExitSidecar({
type: "error",
errorMessage: "Anthropic 529 Overloaded after 3 retries",
stopReason: "error",
}),
{
reason: "error",
exitCode: 1,
errorMessage: "Anthropic 529 Overloaded after 3 retries",
},
);
});
it("falls back to a placeholder when error payload has no errorMessage", () => {
const result = interpretExitSidecar({ type: "error" });
assert.equal(result.reason, "error");
assert.equal(result.exitCode, 1);
assert.match(result.errorMessage ?? "", /no errorMessage/);
});
it("treats unknown payload shapes as done", () => {
assert.deepEqual(interpretExitSidecar({}), { reason: "done", exitCode: 0 });
assert.deepEqual(interpretExitSidecar(null), { reason: "done", exitCode: 0 });
});
});
describe("commands", () => {
it("/subagent emits a spawn tool call for a known agent", () => {
const { api, registeredCommands, sentUserMessages } = createMockExtensionApi();
(subagentsModule as any).default(api);
const subagent = registeredCommands.find((command) => command.name === "subagent");
assert.ok(subagent, "expected /subagent to be registered");
subagent.handler("scout map the auth code", {
ui: { notify() {} },
});
assert.equal(sentUserMessages.length, 1);
assert.match(sentUserMessages[0], /agent: "scout"/);
assert.match(sentUserMessages[0], /map the auth code/);
});
it("does not register the removed /iterate or /plan commands", () => {
const { api, registeredCommands } = createMockExtensionApi();
(subagentsModule as any).default(api);
assert.equal(registeredCommands.find((c) => c.name === "iterate"), undefined);
assert.equal(registeredCommands.find((c) => c.name === "plan"), undefined);
});
});
describe("reload lifecycle", () => {
const testApi = (subagentsModule as any).__test__;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
const ctx = { hasUI: false, ui: { setWidget() {} } };
beforeEach(() => resetSharedRuntimeForTest(shared));
after(() => resetSharedRuntimeForTest(shared));
it("preserves a live subagent and delivers its queued result through the fresh runtime", async () => {
const oldRuntime = createMockExtensionApi();
(subagentsModule as any).default(oldRuntime.api);
await emitMockEvent(oldRuntime, "session_start", { reason: "startup" }, ctx);
const abortController = new AbortController();
const running: {
id: string;
name: string;
task: string;
surface: string;
startTime: number;
sessionFile: string;
statusState: ReturnType<typeof createStatusState>;
interactive: boolean;
abortController: AbortController;
suppressDelivery?: boolean;
} = {
id: "reload-child",
name: "reload-child",
task: "keep running",
surface: "surface-1",
startTime: Date.now(),
sessionFile: "/tmp/reload-child.jsonl",
statusState: createStatusState({ source: "pi", startTimeMs: Date.now() }),
interactive: false,
abortController,
};
shared.runningSubagents.set(running.id, running);
await emitMockEvent(oldRuntime, "session_shutdown", { reason: "reload" }, ctx);
assert.equal(abortController.signal.aborted, false, "reload must not cancel the child");
assert.equal(shared.runningSubagents.get(running.id), running);
testApi.sendWhenRuntimeReady((pi: any) => {
pi.sendMessage({ customType: "subagent_result", content: "done", display: true });
});
assert.equal(oldRuntime.sentMessages.length, 0, "the stale runtime must not receive completion");
assert.equal(shared.pendingDeliveries.length, 1);
const freshRuntime = createMockExtensionApi();
(subagentsModule as any).default(freshRuntime.api);
await emitMockEvent(freshRuntime, "session_start", { reason: "reload" }, ctx);
await new Promise<void>((resolve) => setImmediate(resolve));
assert.equal(freshRuntime.sentMessages.length, 1);
assert.equal(freshRuntime.sentMessages[0].message.customType, "subagent_result");
assert.equal(oldRuntime.sentMessages.length, 0);
assert.equal(shared.pendingDeliveries.length, 0);
assert.equal(shared.runningSubagents.get(running.id), running);
testApi.flushPendingDeliveries();
assert.equal(freshRuntime.sentMessages.length, 1, "a queued completion must flush exactly once");
await emitMockEvent(oldRuntime, "session_shutdown", { reason: "quit" }, ctx);
assert.equal(abortController.signal.aborted, false, "a stale runtime cannot cancel rebound state");
assert.equal(shared.runningSubagents.get(running.id), running);
await emitMockEvent(freshRuntime, "session_shutdown", { reason: "quit" }, ctx);
assert.equal(abortController.signal.aborted, true, "real shutdown still cancels the child");
assert.equal(running.suppressDelivery, true);
assert.equal(shared.runningSubagents.size, 0);
});
});
describe("tool registration", () => {
it("always resumes subagents as autonomous (auto-exit, non-interactive tracking)", () => {
const testApi = (subagentsModule as any).__test__;
assert.deepEqual(testApi.resolveResumeLaunchBehavior(), {
autoExit: true,
interactive: false,
});
});
it("rejects a top-level spawn with no agent and no fork", async () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const subagentTool = registeredTools.find((tool) => tool.name === "subagent");
assert.ok(subagentTool, "expected subagent tool to be registered");
const result = await subagentTool.execute("call-1", { name: "x", task: "do it" });
assert.equal(result.details?.error, "agent required");
assert.match(result.content[0].text, /specify which agent/i);
});
it("rejects a top-level spawn naming an unknown agent", async () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const subagentTool = registeredTools.find((tool) => tool.name === "subagent");
assert.ok(subagentTool, "expected subagent tool to be registered");
const result = await subagentTool.execute("call-1", {
name: "x",
task: "do it",
agent: "wizard",
});
assert.equal(result.details?.error, "unknown agent");
assert.match(result.content[0].text, /not a known agent/i);
});
it("exposes a debloated schema: agent+task required, name/model/cwd optional, no override knobs", () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const subagentTool = registeredTools.find((tool) => tool.name === "subagent");
assert.ok(subagentTool, "expected subagent tool to be registered");
const props = subagentTool.parameters.properties;
assert.deepEqual(
Object.keys(props).sort(),
["agent", "cwd", "model", "name", "task"],
"only agent/task/name/model/cwd should remain",
);
assert.deepEqual(
[...(subagentTool.parameters.required ?? [])].sort(),
["agent", "task"],
"agent and task must be required",
);
// `name` is now optional and purely cosmetic.
assert.match(props.name.description, /cosmetic/i);
// The removed override knobs must be gone.
for (const gone of ["tools", "skills", "systemPrompt", "fork", "interactive", "resumeSessionId"]) {
assert.equal(props[gone], undefined, `expected ${gone} param to be removed`);
}
});
it("renders partial subagent tool-call args without throwing", () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const subagentTool = registeredTools.find((tool) => tool.name === "subagent");
assert.ok(subagentTool, "expected subagent tool to be registered");
const theme = {
fg(_color: string, text: string) {
return text;
},
bold(text: string) {
return text;
},
};
const rendered = subagentTool.renderCall({}, theme);
const output = rendered.render(80).join("\n");
assert.match(output, /\(unnamed\)/);
});
it("registers subagent_message with name + message both required (name-only addressing)", () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const messageTool = registeredTools.find((tool) => tool.name === "subagent_message");
assert.ok(messageTool, "expected subagent_message tool to be registered");
const props = messageTool.parameters.properties;
assert.deepEqual(
Object.keys(props).sort(),
["message", "name"],
"only name/message should remain (sessionId dropped)",
);
assert.equal(props.message.type, "string");
assert.equal(props.name.type, "string");
assert.deepEqual(
messageTool.parameters.required?.slice().sort(),
["message", "name"],
"name and message should both be required",
);
assert.equal(props.sessionId, undefined, "sessionId should be removed");
assert.equal(props.autoExit, undefined, "autoExit knob should be removed");
});
it("no longer registers subagent_interrupt or subagent_resume", () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const names = registeredTools.map((tool) => tool.name);
assert.equal(names.includes("subagent_interrupt"), false);
assert.equal(names.includes("subagent_resume"), false);
});
});
describe("subagent activity snapshots", () => {
function validActivity(overrides: Record<string, unknown> = {}) {
return {
version: 1,
runningChildId: "child-1",
createdAt: 1_000,
updatedAt: 1_000,
sequence: 1,
latestEvent: "session_start",
phase: "starting",
agentActive: false,
turnActive: false,
providerActive: false,
toolActive: false,
...overrides,
};
}
it("writes and validates activity files by running child id", () => {
withTempDir((dir) => {
const activityFile = getSubagentActivityFile(dir, "child-1");
const recorder = createSubagentActivityRecorder({
runningChildId: "child-1",
activityFile,
now: () => 1_000,
});
recorder.sessionStart();
recorder.toolExecutionStart("tool-1", "bash");
const read = readSubagentActivityFile(activityFile, "child-1");
assert.ok(read.ok);
assert.equal(read.activity.phase, "active");
assert.equal(read.activity.activeScope, "tool");
assert.equal(read.activity.toolName, "bash");
assert.deepEqual(readSubagentActivityFile(activityFile, "other-child"), {
ok: false,
reason: "wrong-id",
});
});
});
it("records waiting and final done states", () => {
withTempDir((dir) => {
let currentNow = 2_000;
const activityFile = getSubagentActivityFile(dir, "child-2");
const recorder = createSubagentActivityRecorder({
runningChildId: "child-2",
activityFile,
now: () => currentNow,
});
recorder.sessionStart();
currentNow = 3_000;
recorder.agentEndWaiting();
let read = readSubagentActivityFile(activityFile, "child-2");
assert.ok(read.ok);
assert.equal(read.activity.phase, "waiting");
assert.equal(read.activity.waitingSince, 3_000);
currentNow = 4_000;
recorder.agentEndDone();
read = readSubagentActivityFile(activityFile, "child-2");
assert.ok(read.ok);
assert.equal(read.activity.phase, "done");
assert.equal(read.activity.agentActive, false);
});
});
it("rejects malformed activity fields used by classification and rendering", () => {
withTempDir((dir) => {
mkdirSync(join(dir, "subagent-activity"), { recursive: true });
const cases = [
{ activeSince: "bad" },
{ waitingSince: "bad" },
{ activeScope: "database" },
{ latestEvent: "unknown" },
{ runningChildId: 42 },
{ toolActive: "yes" },
{ toolName: "bad\nname" },
];
for (const [index, overrides] of cases.entries()) {
const activityFile = getSubagentActivityFile(dir, `child-${index}`);
const activity = validActivity({ runningChildId: `child-${index}`, ...overrides });
writeFileSync(activityFile, `${JSON.stringify(activity)}\n`);
const read = readSubagentActivityFile(activityFile, `child-${index}`);
assert.equal(read.ok, false);
assert.equal((read as { ok: false; reason: string }).reason, "invalid");
}
});
});
it("does not let tool_result resurrect finished tool activity", () => {
withTempDir((dir) => {
let currentNow = 1_000;
const activityFile = getSubagentActivityFile(dir, "child-3");
const recorder = createSubagentActivityRecorder({
runningChildId: "child-3",
activityFile,
now: () => currentNow,
});
recorder.sessionStart();
recorder.agentStart();
recorder.turnStart(1);
currentNow = 2_000;
recorder.toolExecutionStart("tool-1", "bash");
currentNow = 3_000;
recorder.toolExecutionEnd("tool-1", "bash");
currentNow = 4_000;
recorder.toolResult("tool-1", "bash");
const read = readSubagentActivityFile(activityFile, "child-3");
assert.ok(read.ok);
assert.equal(read.activity.toolActive, false);
assert.equal(read.activity.activeScope, "turn");
});
});
it("does not mark reload shutdown as the final done snapshot", () => {
withTempDir((dir) => {
const activityFile = getSubagentActivityFile(dir, "child-4");
const recorder = createSubagentActivityRecorder({
runningChildId: "child-4",
activityFile,
now: () => 1_000,
});
recorder.sessionStart();
recorder.sessionShutdown("reload");
const read = readSubagentActivityFile(activityFile, "child-4");
assert.ok(read.ok);
assert.equal(read.activity.phase, "starting");
assert.equal(read.activity.latestEvent, "session_start");
});
});
it("cancels pending throttled writes on reload shutdown", async () => {
const dir = createTestDir();
try {
await new Promise<void>((resolve) => {
let currentNow = 1_000;
const activityFile = getSubagentActivityFile(dir, "child-5");
const recorder = createSubagentActivityRecorder({
runningChildId: "child-5",
activityFile,
now: () => currentNow,
});
recorder.sessionStart();
currentNow = 1_100;
recorder.messageUpdate("delta");
recorder.sessionShutdown("reload");
setTimeout(() => {
const read = readSubagentActivityFile(activityFile, "child-5");
assert.ok(read.ok);
assert.equal(read.activity.phase, "starting");
assert.equal(read.activity.latestEvent, "session_start");
resolve();
}, 650);
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe("subagent interruption", () => {
function makeRunning(overrides: Record<string, unknown> = {}) {
return {
id: "a1",
name: "Worker",
task: "",
surface: "pane-1",
startTime: 0,
sessionFile: "worker.jsonl",
interactive: false,
statusState: createStatusState({ source: "pi", startTimeMs: 0 }),
...overrides,
};
}
it("registers subagent_message and not the old interrupt/resume tools", () => {
const { api, registeredTools } = createMockExtensionApi();
(subagentsModule as any).default(api);
const names = registeredTools.map((tool) => tool.name);
assert.equal(names.includes("subagent_message"), true);
assert.equal(names.includes("subagent_interrupt"), false);
assert.equal(names.includes("subagent_resume"), false);
});
it("resolves a running subagent by exact name and reports ambiguity", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
runningMap.clear();
try {
runningMap.set("a1", makeRunning({ id: "a1", name: "Worker", surface: "a1", sessionFile: "a1.jsonl" }));
runningMap.set("b2", makeRunning({ id: "b2", name: "Worker", surface: "b2", sessionFile: "b2.jsonl" }));
runningMap.set("c3", makeRunning({ id: "c3", name: "Scout", surface: "c3", sessionFile: "c3.jsonl" }));
const byName = testApi.resolveRunningByName("Scout");
assert.equal(byName.running.id, "c3");
const ambiguous = testApi.resolveRunningByName("Worker");
assert.match(ambiguous.error, /Ambiguous subagent name/);
const missing = testApi.resolveRunningByName("Ghost");
assert.match(missing.error, /No running subagent named "Ghost"/);
} finally {
runningMap.clear();
}
});
it("uniqueRunningName suffixes defaulted names that collide with running subagents", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
runningMap.clear();
try {
// No collision: base name is returned untouched.
assert.equal(testApi.uniqueRunningName("worker"), "worker");
runningMap.set("a1", makeRunning({ id: "a1", name: "worker", surface: "a1" }));
assert.equal(testApi.uniqueRunningName("worker"), "worker-2");
runningMap.set("b2", makeRunning({ id: "b2", name: "worker-2", surface: "b2" }));
assert.equal(testApi.uniqueRunningName("worker"), "worker-3");
// A distinct base is unaffected by the worker collisions.
assert.equal(testApi.uniqueRunningName("scout"), "scout");
} finally {
runningMap.clear();
}
});
it("uniqueRunningName also avoids names already taken in the persistent registry", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
const reserved = testApi.reservedNames as Set<string>;
runningMap.clear();
reserved.clear();
try {
// A finished subagent's name lives in the registry even though nothing is
// running — a fresh default must skip it so names stay unique session-wide.
const registryNames = new Set(["worker", "worker-2"]);
assert.equal(testApi.uniqueRunningName("worker", registryNames), "worker-3");
// A name not in the registry (or running/reserved) is unaffected.
assert.equal(testApi.uniqueRunningName("scout", registryNames), "scout");
// An empty registry behaves like before.
assert.equal(testApi.uniqueRunningName("worker", new Set()), "worker");
} finally {
runningMap.clear();
reserved.clear();
}
});
it("uniqueRunningName also avoids names reserved by in-flight parallel spawns", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
const reserved = testApi.reservedNames as Set<string>;
runningMap.clear();
reserved.clear();
try {
// Simulate the first parallel spawn reserving its default name before it
// has registered in runningSubagents.
reserved.add(testApi.uniqueRunningName("scout")); // "scout"
// The second spawn, running concurrently, must not reuse it.
assert.equal(testApi.uniqueRunningName("scout"), "scout-2");
reserved.add("scout-2");
assert.equal(testApi.uniqueRunningName("scout"), "scout-3");
} finally {
runningMap.clear();
reserved.clear();
}
});
it("steers a running subagent by typing into its pane (newlines flattened)", () => {
const testApi = (subagentsModule as any).__test__;
let sentSurface = "";
let sentText = "";
const running = makeRunning();
const result = testApi.steerSubagent(running, "do this\nthen that", (surface: string, text: string) => {
sentSurface = surface;
sentText = text;
});
assert.deepEqual(result, { ok: true });
assert.equal(sentSurface, "pane-1");
assert.equal(sentText, "do this then that");
});
it("returns an explicit error when steering delivery fails", () => {
const testApi = (subagentsModule as any).__test__;
const running = makeRunning();
const result = testApi.steerSubagent(running, "hi", () => {
throw new Error("mux write failed");
});
assert.match(result.error, /Failed to deliver message/);
});
it("delivers a steer message and forces local status waiting", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
let sentSurface = "";
let sentText = "";
runningMap.clear();
const activeState = observeStatus(
createStatusState({ source: "pi", startTimeMs: 0 }),
{
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
},
5_000,
);
try {
runningMap.set("a1", makeRunning({ statusState: activeState }));
const result = withMockedNow(20_000, () =>
testApi.handleSubagentSteer({ name: "Worker", message: "keep going" }, (surface: string, text: string) => {
sentSurface = surface;
sentText = text;
}),
);
assert.equal(sentSurface, "pane-1");
assert.equal(sentText, "keep going");
assert.equal(result.content[0].text.includes('Message delivered to running subagent "Worker"'), true);
assert.deepEqual(result.details, { id: "a1", name: "Worker", status: "steered" });
const snapshot = classifyStatus(runningMap.get("a1").statusState, 20_000);
assert.equal(snapshot.kind, "waiting");
assert.equal(runningMap.has("a1"), true);
} finally {
runningMap.clear();
}
});
it("requires a message when steering", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
runningMap.clear();
try {
runningMap.set("a1", makeRunning());
const result = testApi.handleSubagentSteer({ name: "Worker", message: " " }, () => {});
assert.match(result.content[0].text, /`message` is required/);
} finally {
runningMap.clear();
}
});
it("leaves status unchanged when steering delivery fails in the tool path", () => {
const testApi = (subagentsModule as any).__test__;
const runningMap = testApi.runningSubagents as Map<string, any>;
runningMap.clear();
const activeState = observeStatus(
createStatusState({ source: "pi", startTimeMs: 0 }),
{
snapshot: "present",
updatedAt: 5_000,
sequence: 1,
phase: "active",
active: true,
activeScope: "tool",
activeSince: 5_000,
activityLabel: "bash",
},
5_000,
);
try {
runningMap.set("a1", makeRunning({ statusState: activeState }));
const result = withMockedNow(20_000, () =>
testApi.handleSubagentSteer({ name: "Worker", message: "go" }, () => {
throw new Error("mux write failed");
}),
);
assert.match(result.content[0].text, /Failed to deliver message/);
assert.equal(classifyStatus(runningMap.get("a1").statusState, 20_000).kind, "active");
} finally {
runningMap.clear();
}
});
it("formats exit code 130 as an ordinary failure", () => {
const testApi = (subagentsModule as any).__test__;
const presentation = testApi.resolveResultPresentation(
{
exitCode: 130,
elapsed: 61,
summary: "Sub-agent exited with code 130",
sessionFile: "/tmp/subagent.jsonl",
sessionId: "019f-abc",
},
"Worker",
);
assert.match(presentation, /failed \(exit code 130\)/);
assert.doesNotMatch(presentation, /interrupted/);
// Follow-ups reference the name (not the session id).
assert.match(presentation, /subagent_message\(\{ name: "Worker"/);
assert.doesNotMatch(presentation, /Session id:/);
});
it("renders a clear provider/agent error when errorMessage is set", () => {
// Previously, an overload retry-exhaustion produced exitCode 0 with a
// stale summary — the orchestrator thought the subagent finished
// quickly. With the error sidecar plumbed through, the presentation
// must call out the failure, include the underlying error, and tell the
// orchestrator how to recover.
const testApi = (subagentsModule as any).__test__;
const presentation = testApi.resolveResultPresentation(
{
exitCode: 1,
elapsed: 14,
summary: "ignored when errorMessage is present",
sessionFile: "/tmp/subagent.jsonl",
sessionId: "019f-xyz",
errorMessage: "Anthropic 529 Overloaded after 3 retries",
},
"Worker",
);
assert.match(presentation, /Sub-agent "Worker" failed/);
assert.match(presentation, /provider\/agent error — auto-retry exhausted/);
assert.match(presentation, /Error: Anthropic 529 Overloaded after 3 retries/);
assert.match(presentation, /subagent_message\(\{ name: "Worker"/);
assert.doesNotMatch(presentation, /Session id:/);
assert.doesNotMatch(presentation, /ignored when errorMessage is present/);
});
});
describe("subagent status renderer", () => {
function createTheme() {
return {
fg(_color: string, text: string) {
return text;
},
bg(_color: string, text: string) {
return text;
},
bold(text: string) {
return text;
},
};
}
it("renders only capped lines plus overflow", () => {
const { api, registeredMessageRenderers } = createMockExtensionApi();
(subagentsModule as any).default(api);
const rendererEntry = registeredMessageRenderers.find((entry) => entry.name === "subagent_status");
assert.ok(rendererEntry, "expected subagent_status renderer to be registered");
const visibleLines = [
"Worker running 5m, active (bash 2m).",
"Scout running 3m, waiting 1m.",
"Reviewer running 2m, active (streaming 30s).",
"Planner running 4m, waiting 2m.",
];
const rendered = rendererEntry.renderer(
{
customType: "subagent_status",
content: "Subagent status:\n• Worker running 5m, active (bash 2m).",
details: {
lines: visibleLines,
overflow: 2,
},
},
{ expanded: true },
createTheme(),
);
const output = rendered.render(80).join("\n");
assert.match(output, /Subagent status/);
for (const line of visibleLines) {
assert.match(output, new RegExp(line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
}
assert.match(output, /\+2 more running\./);
});
it("stays within narrow widths", () => {
const { api, registeredMessageRenderers } = createMockExtensionApi();
(subagentsModule as any).default(api);
const rendererEntry = registeredMessageRenderers.find((entry) => entry.name === "subagent_status");
assert.ok(rendererEntry, "expected subagent_status renderer to be registered");
const rendered = rendererEntry.renderer(
{
customType: "subagent_status",
content: "Subagent status:\n• Worker running 5m, active (bash 2m).",
details: { lines: ["Worker running 5m, active (bash 2m)."], overflow: 0 },
},
{ expanded: true },
createTheme(),
);
for (const width of [4, 5, 6]) {
for (const line of rendered.render(width)) {
assert.ok(
visibleWidth(line) <= width,
`expected line width <= ${width}, got ${visibleWidth(line)} for ${JSON.stringify(line)}`,
);
}
}
});
});
describe("subagent startup delay", () => {
it("uses backend-specific defaults", () => {
const testApi = (subagentsModule as any).__test__;
assert.equal(testApi.defaultShellReadyDelayMs("tmux"), 500);
assert.equal(testApi.defaultShellReadyDelayMs("herdr"), 1000);
assert.equal(testApi.defaultShellReadyDelayMs("orca"), 500);
assert.equal(testApi.defaultShellReadyDelayMs(null), 500);
});
it("uses PI_SUBAGENT_SHELL_READY_DELAY_MS when it is set", () => {
const testApi = (subagentsModule as any).__test__;
assert.ok(testApi, "expected subagents test helpers to be exported");
assert.equal(typeof testApi.getShellReadyDelayMs, "function");
const original = process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS;
process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS = "2500";
try {
assert.equal(testApi.getShellReadyDelayMs(), 2500);
} finally {
if (original == null) delete process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS;
else process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS = original;
}
});
});
describe("subagents widget rendering", () => {
function createThemeSpy() {
const calls: Array<{ color: string; text: string }> = [];
return {
calls,
theme: {
fg(color: string, text: string) {
calls.push({ color, text });
return `\x1b[38;5;${calls.length}m${text}\x1b[0m`;
},
},
};
}
function createWidgetAgent(
id: string,
kind: "active" | "running" | "stalled" | "starting" | "waiting",
) {
const startTime = kind === "starting" ? 1_061_000 - 5_000 : 1_000_000 - 5_000;
let statusState = createStatusState({
source: kind === "running" ? "claude" : "pi",
startTimeMs: startTime,
});
if (kind === "active" || kind === "waiting") {
statusState = observeStatus(
statusState,
{
snapshot: "present",
updatedAt: startTime + 1_000,
sequence: 1,
phase: kind,
...(kind === "active"
? {
active: true,
activeScope: "tool",
activeSince: startTime + 1_000,
activityLabel: "bash",
}
: { waitingSince: startTime + 1_000 }),
},
startTime + 1_000,
);
} else if (kind === "stalled") {
statusState = observeStatus(
statusState,
{ snapshot: "missing" },
startTime + 1_000,
);
}
return {
id,
name: id,
agent: "worker",
task: "",
surface: "surface",
startTime,
sessionFile: `${id}.jsonl`,
statusState,
};
}
function createActiveWidgetAgent(id: string) {
return createWidgetAgent(id, "active");
}
it("keeps every rendered line within a very narrow width", () => {
const testApi = (subagentsModule as any).__test__;
assert.ok(testApi, "expected subagents test helpers to be exported");
assert.equal(typeof testApi.renderSubagentWidgetLines, "function");
const originalNow = Date.now;
Date.now = () => 1_000_000;
try {
const lines = testApi.renderSubagentWidgetLines([
{
id: "a1",
name: "A",
task: "",
surface: "s1",
startTime: 1_000_000 - 13_000,
sessionFile: "sess1",
statusState: createStatusState({ source: "pi", startTimeMs: 1_000_000 - 13_000 }),
},
{
id: "a2",
name: "B",
task: "",
surface: "s2",
startTime: 1_000_000 - 21_000,
sessionFile: "sess2",
statusState: createStatusState({ source: "pi", startTimeMs: 1_000_000 - 21_000 }),
},
{
id: "a3",
name: "C",
task: "",
surface: "s3",
startTime: 1_000_000 - 27_000,
sessionFile: "sess3",
statusState: createStatusState({ source: "pi", startTimeMs: 1_000_000 - 27_000 }),
},
], 16);
assert.deepEqual(
lines.map((line: string) => visibleWidth(line)),
[16, 16, 16, 16],
);
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
} finally {
Date.now = originalNow;
}
});
it("sanitizes whitespace in agent names before rendering rows", () => {
const testApi = (subagentsModule as any).__test__;
const startTime = Date.now() - 5_000;
const lines = testApi.renderSubagentWidgetLines([
{
id: "unsafe",
name: "worker\nwith\ttabs",
task: "",
surface: "s1",
startTime,
sessionFile: "sess1",
statusState: createStatusState({ source: "pi", startTimeMs: startTime }),
},
], 80);
assert.ok(lines.every((line: string) => !/[\r\n]/.test(line)));
assert.match(lines[1] ?? "", /worker with tabs/);
});
it("truncates the right-hand status instead of overflowing when it alone is too wide", () => {
const testApi = (subagentsModule as any).__test__;
assert.ok(testApi, "expected subagents test helpers to be exported");
assert.equal(typeof testApi.borderLine, "function");
const line = testApi.borderLine(" A ", " 999 msgs (999.9KB) ", 16);
assert.equal(visibleWidth(line), 16);
});
it("handles ultra-narrow widths without exceeding the width contract", () => {
const testApi = (subagentsModule as any).__test__;
assert.ok(testApi, "expected subagents test helpers to be exported");
assert.equal(typeof testApi.renderSubagentWidgetLines, "function");
const widths = [0, 1, 2];
for (const width of widths) {
const startTime = Date.now() - 5_000;
const lines = testApi.renderSubagentWidgetLines([
{
id: "a1",
name: "A",
task: "",
surface: "s1",
startTime,
sessionFile: "sess1",
statusState: createStatusState({ source: "pi", startTimeMs: startTime }),
},
], width);
for (const line of lines) {
assert.ok(
visibleWidth(line) <= width,
`expected line width <= ${width}, got ${visibleWidth(line)} for ${JSON.stringify(line)}`,
);
}
}
});
it("uses the standalone widget factory theme for semantic status colors", () => {
const testApi = (subagentsModule as any).__test__;
const widgetCalls: any[] = [];
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
shared.runningSubagents.set("active", createActiveWidgetAgent("active"));
testApi.updateWidget();
const { theme, calls } = createThemeSpy();
const component = widgetCalls[0][1](undefined, theme);
const lines = withMockedNow(1_000_000, () => component.render(80));
assert.equal(lines.length, 2, "header plus one body row");
assert.equal(calls.find((call) => call.text === "Subagents")?.color, "accent");
assert.equal(calls.find((call) => call.text === "1 running")?.color, "muted");
assert.ok(calls.some((call) => call.color === "warning" && call.text.includes("active")));
assert.equal(calls.find((call) => call.text === " (worker)")?.color, "dim");
assert.ok(calls.some((call) => call.color === "dim" && call.text === "00:05"));
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
});
it("uses the optional theme passed to hosted rendering", () => {
const testApi = (subagentsModule as any).__test__;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
shared.runningSubagents.set("active", createActiveWidgetAgent("active"));
const { theme, calls } = createThemeSpy();
const lines = withMockedNow(1_000_000, () => testApi.statusWidget.render(80, undefined, theme));
assert.equal(lines.length, 2, "header plus one body row");
assert.ok(calls.some((call) => call.color === "accent" && call.text === "Subagents"));
assert.ok(calls.some((call) => call.color === "muted" && call.text === "1 running"));
assert.ok(calls.some((call) => call.color === "warning" && call.text.includes("active")));
assert.ok(calls.some((call) => call.color === "dim" && call.text === "00:05"));
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
});
it("maps every status kind to a semantic row color", () => {
const testApi = (subagentsModule as any).__test__;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
for (const [id, kind] of [
["active", "active"],
["running", "running"],
["stalled", "stalled"],
["starting", "starting"],
["waiting", "waiting"],
] as const) {
shared.runningSubagents.set(id, createWidgetAgent(id, kind));
}
const { theme, calls } = createThemeSpy();
withMockedNow(1_061_000, () => testApi.statusWidget.render(120, undefined, theme));
const expected = {
active: "warning",
running: "warning",
stalled: "error",
starting: "dim",
waiting: "accent",
} as const;
for (const [id, kind] of [
["active", "active"],
["running", "running"],
["stalled", "stalled"],
["starting", "starting"],
["waiting", "waiting"],
] as const) {
assert.ok(
calls.some((call) => call.text === id && call.color === expected[kind]),
`${id} should use ${expected[kind]}`,
);
}
});
});
describe("status widget host integration", () => {
const testApi = (subagentsModule as any).__test__;
const statusWidget = (globalThis as any).__pi_interactive_subagents.statusWidget;
const shared = testApi.sharedRuntime as SharedRuntimeTestState;
function addRunning(id: string) {
const startTime = Date.now();
shared.runningSubagents.set(id, {
id,
name: id,
task: "",
surface: "surface",
startTime,
sessionFile: `${id}.jsonl`,
statusState: createStatusState({ source: "pi", startTimeMs: startTime }),
});
}
beforeEach(() => resetSharedRuntimeForTest(shared));
after(() => resetSharedRuntimeForTest(shared));
it("falls back to the standalone widget when unclaimed", () => {
const widgetCalls: any[] = [];
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
addRunning("worker");
testApi.updateWidget();
assert.equal(widgetCalls.length, 1);
assert.equal(widgetCalls[0][0], "subagent-status");
assert.equal(typeof widgetCalls[0][1], "function");
});
it("suppresses, notifies, and restores the standalone widget around a claim", async () => {
const widgetCalls: any[] = [];
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
addRunning("worker");
testApi.updateWidget();
let changes = 0;
const onChange = () => changes++;
const release = statusWidget.claim(onChange);
assert.equal(statusWidget.isClaimed(onChange), true);
assert.equal(statusWidget.isClaimed(() => {}), false);
assert.equal(typeof widgetCalls.at(-1)[1], "function", "claim sync is deferred");
await Promise.resolve();
assert.equal(widgetCalls.at(-1)[1], undefined);
testApi.updateWidget();
assert.equal(changes, 1);
release();
assert.equal(statusWidget.isClaimed(onChange), false);
assert.equal(widgetCalls.at(-1)[1], undefined, "release sync is deferred");
await Promise.resolve();
assert.equal(typeof widgetCalls.at(-1)[1], "function");
release();
assert.equal(widgetCalls.length, 4, "release is idempotent");
});
it("does not restore standalone UI between an immediate release and re-claim", async () => {
const widgetCalls: any[] = [];
shared.latestCtx = {
hasUI: true,
ui: { setWidget: (...args: any[]) => widgetCalls.push(args) },
};
addRunning("worker");
testApi.updateWidget();
const firstChange = () => {};
const releaseFirst = statusWidget.claim(firstChange);
releaseFirst();
const secondChange = () => {};
const releaseSecond = statusWidget.claim(secondChange);
await Promise.resolve();
assert.equal(statusWidget.isClaimed(firstChange), false);
assert.equal(statusWidget.isClaimed(secondChange), true);
assert.equal(widgetCalls.at(-1)[1], undefined);
releaseSecond();
await Promise.resolve();
assert.equal(typeof widgetCalls.at(-1)[1], "function");
});
it("caps body rows with an honest overflow row", () => {
addRunning("worker");
addRunning("scout");
addRunning("reviewer");
const lines = statusWidget.render(40, 2);
assert.equal(lines.length, 3, "header + one agent + overflow");
assert.ok(lines.every((line: string) => !/[╭╮╰╯│]/.test(line)));
assert.match(lines[2], /\+2 more running\./);
assert.equal(statusWidget.getBodyRowCount(), 3);
assert.equal(statusWidget.hasContent(), true);
});
it("keeps capped output within narrow widths", () => {
addRunning("worker");
addRunning("scout");
for (const width of [0, 1, 2, 4, 8]) {
for (const line of statusWidget.render(width, 1)) {
assert.ok(
visibleWidth(line) <= width,
`expected line width <= ${width}, got ${visibleWidth(line)}`,
);
}
}
});
it("drops old claims during reload and allows a fresh runtime to claim", async () => {
const oldRuntime = createMockExtensionApi();
(subagentsModule as any).default(oldRuntime.api);
const oldCtx = {
hasUI: true,
ui: { setWidget() {} },
};
await emitMockEvent(oldRuntime, "session_start", { reason: "startup" }, oldCtx);
addRunning("worker");
let oldChanges = 0;
const oldOnChange = () => oldChanges++;
const oldRelease = statusWidget.claim(oldOnChange);
// Bind the replacement before the old runtime emits shutdown. The old
// shutdown must not undo the fresh binding or retain its old callback.
const freshRuntime = createMockExtensionApi();
(subagentsModule as any).default(freshRuntime.api);
await emitMockEvent(freshRuntime, "session_start", { reason: "reload" }, oldCtx);
// A delayed old start must also be ignored once the fresh runtime is bound.
await emitMockEvent(oldRuntime, "session_start", { reason: "reload" }, oldCtx);
assert.equal(shared.latestPi, freshRuntime.api);
assert.equal(statusWidget.isClaimed(oldOnChange), false);
await emitMockEvent(oldRuntime, "session_shutdown", { reason: "reload" }, oldCtx);
testApi.updateWidget();
assert.equal(oldChanges, 0);
assert.equal(shared.statusWidgetClaim, null);
assert.equal(shared.runningSubagents.size, 1);
let freshChanges = 0;
const freshRelease = statusWidget.claim(() => freshChanges++);
oldRelease();
testApi.updateWidget();
assert.equal(freshChanges, 1, "a stale release cannot affect the fresh claim");
freshRelease();
});
it("replaces malformed and old widget contracts on a duplicate load", async () => {
const globalApi = (globalThis as any).__pi_interactive_subagents;
(globalThis as any).__pi_interactive_subagents = "old-api";
shared.statusWidgetClaim = { id: 1, onChange: () => {} };
await import(`../pi-extension/subagents/index.ts?malformed-global=${Date.now()}`);
const repairedApi = (globalThis as any).__pi_interactive_subagents;
assert.equal(typeof repairedApi.registerToolExtension, "function");
assert.equal(repairedApi.statusWidget, globalApi.statusWidget);
shared.statusWidgetClaim = { id: 1, onChange: () => {} };
await import(`../pi-extension/subagents/index.ts?old-widget-claim=${Date.now()}`);
assert.equal(shared.statusWidgetClaim, null);
const oldContract = {
hasContent: () => true,
getBodyRowCount: () => 1,
render: () => [],
claim: () => () => {},
};
shared.statusWidget = oldContract;
(globalThis as any).__pi_interactive_subagents.statusWidget = oldContract;
await import(`../pi-extension/subagents/index.ts?old-widget-contract=${Date.now()}`);
const currentApi = (globalThis as any).__pi_interactive_subagents;
const current = currentApi.statusWidget;
assert.equal(currentApi, repairedApi);
assert.notEqual(current, oldContract);
assert.equal(typeof current.hasContent, "function");
assert.equal(typeof current.getBodyRowCount, "function");
assert.equal(typeof current.render, "function");
assert.equal(typeof current.claim, "function");
assert.equal(typeof current.isClaimed, "function");
assert.equal(shared.statusWidget, current);
});
});
describe("subagent display helpers", () => {
const testApi = (subagentsModule as any).__test__;
describe("formatTokens", () => {
it("renders raw counts below 1k, 1 decimal below 10k, rounded k above", () => {
assert.equal(testApi.formatTokens(850), "850");
assert.equal(testApi.formatTokens(3200), "3.2k");
assert.equal(testApi.formatTokens(45000), "45k");
});
});
describe("contextWindowFor", () => {
it("maps known model families and returns undefined otherwise", () => {
assert.equal(testApi.contextWindowFor("claude-sonnet-4-6"), 200_000);
assert.equal(testApi.contextWindowFor("gemini-2.5-pro"), 1_000_000);
assert.equal(testApi.contextWindowFor("some-unknown-model"), undefined);
assert.equal(testApi.contextWindowFor(null), undefined);
});
});
describe("formatContextUsage", () => {
it("shows a percent gauge when the window is known", () => {
assert.equal(testApi.formatContextUsage(36_000, 200_000), "18.0%/200k");
assert.equal(testApi.formatContextUsage(500_000, 1_000_000), "50.0%/1.0M");
});
it("falls back to a window-less ctx label when unknown", () => {
assert.equal(testApi.formatContextUsage(37_000, undefined), "37k ctx");
});
});
describe("formatUsageSegments", () => {
it("emits arrow/cache/cost segments, skipping zero fields", () => {
const segs = testApi.formatUsageSegments({
model: "claude-sonnet-4-6",
toolCount: 3,
inputTokens: 3200,
outputTokens: 890,
cacheReadTokens: 45000,
cacheWriteTokens: 0,
contextTokens: 7000,
cost: 0.042,
});
assert.deepEqual(segs, ["↑3.2k", "↓890", "R45k", "$0.042"]);
});
it("returns an empty list when there is no usage", () => {
assert.deepEqual(
testApi.formatUsageSegments({
model: null,
toolCount: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
contextTokens: 0,
cost: 0,
}),
[],
);
});
});
describe("widgetIcon", () => {
it("maps active/running to a glyph and waiting/starting to another", () => {
const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
assert.equal(strip(testApi.widgetIcon("active")), "⟳");
assert.equal(strip(testApi.widgetIcon("running")), "⟳");
assert.equal(strip(testApi.widgetIcon("stalled")), "⟳");
assert.equal(strip(testApi.widgetIcon("waiting")), "○");
assert.equal(strip(testApi.widgetIcon("starting")), "○");
});
});
});
describe("surface backend selection", () => {
it("prefers Herdr, then Orca, and otherwise falls back to tmux", () => {
assert.equal(selectMuxBackend({ herdr: true, orca: true, tmux: true }), "herdr");
assert.equal(selectMuxBackend({ herdr: true, orca: false, tmux: false }), "herdr");
assert.equal(selectMuxBackend({ herdr: false, orca: true, tmux: true }), "orca");
assert.equal(selectMuxBackend({ herdr: false, orca: true, tmux: false }), "orca");
assert.equal(selectMuxBackend({ herdr: false, orca: false, tmux: true }), "tmux");
assert.equal(selectMuxBackend({ herdr: false, orca: false, tmux: false }), null);
});
it("parses Herdr tab and pane creation responses", () => {
assert.deepEqual(
parseCreatedTab(JSON.stringify({
result: {
root_pane: { pane_id: "w3:p9" },
tab: { tab_id: "w3:t6" },
},
})),
{ paneId: "w3:p9", tabId: "w3:t6" },
);
assert.equal(
parseCreatedPane(JSON.stringify({ result: { pane: { pane_id: "w3:p10" } } })),
"w3:p10",
);
assert.throws(() => parseCreatedTab("{}"), /result\.root_pane\.pane_id/);
});
it("parses Orca terminal create, split, and read responses", () => {
assert.deepEqual(
parseCreatedTerminal(JSON.stringify({
ok: true,
result: { terminal: { handle: "term_abc", tabId: "tab-1" } },
})),
{ handle: "term_abc", tabId: "tab-1" },
);
assert.equal(
parseCreatedSplit(JSON.stringify({ ok: true, result: { split: { handle: "term_def" } } })),
"term_def",
);
assert.equal(
parseReadTail(JSON.stringify({
ok: true,
result: { terminal: { status: "running", tail: ["line one", "line two"] } },
})),
"line one\nline two",
);
assert.throws(
() => parseReadTail(JSON.stringify({
ok: true,
result: { terminal: { status: "exited", tail: [] } },
})),
/not running \(status: exited\)/,
);
assert.throws(() => parseCreatedTerminal("{}"), /result\.terminal\.handle/);
assert.throws(
() => parseCreatedTerminal(JSON.stringify({ ok: false, error: { message: "nope" } })),
/terminal create failed: nope/,
);
});
});
describe("subagent completion wrapper", () => {
it("records the child exit code without losing the terminal sentinel", () => {
const wrapped = (subagentsModule as any).__test__.wrapWithCompletionFile(
"pi --session test.jsonl",
"/tmp/subagent exit",
);
assert.match(wrapped, /^pi --session test\.jsonl; exit_code=\$\?/);
assert.match(wrapped, /printf '%s\\n' "\$exit_code" > '\/tmp\/subagent exit'/);
assert.match(wrapped, /__SUBAGENT_DONE_\$\{exit_code\}__/);
});
});
describe("surface.ts", () => {
describe("shellEscape", () => {
it("wraps in single quotes", () => {
assert.equal(shellEscape("hello"), "'hello'");
});
it("escapes single quotes", () => {
assert.equal(shellEscape("it's"), "'it'\\''s'");
});
it("handles empty string", () => {
assert.equal(shellEscape(""), "''");
});
it("handles special characters", () => {
const input = 'echo "hello $world" && rm -rf /';
const escaped = shellEscape(input);
assert.ok(escaped.startsWith("'"));
assert.ok(escaped.endsWith("'"));
// Inside single quotes, everything is literal
assert.ok(escaped.includes("$world"));
});
});
});