Files
pi-interactive-subagents/test/test.ts
T

2558 lines
86 KiB
TypeScript

import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, readFileSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { visibleWidth } from "@mariozechner/pi-tui";
import * as subagentsModule from "../pi-extension/subagents/index.ts";
import {
getLeafId,
getNewEntries,
getSessionId,
resolveSessionFileById,
findLastAssistantMessage,
appendBranchSummary,
copySessionFile,
mergeNewEntries,
seedSubagentSessionFile,
summarizeSessionStats,
} from "../pi-extension/subagents/session.ts";
import {
shellEscape,
isCmuxAvailable,
isWezTermAvailable,
parseCmuxFocusedSnapshot,
parseCmuxFocusedSnapshotFromJson,
parseCmuxJson,
parseCmuxPaneRefForSurface,
parseCmuxPaneRefForSurfaceFromJson,
canSplitZellijPane,
predictZellijSplitDirection,
selectZellijPlacement,
selectZellijStackPlacement,
} from "../pi-extension/subagents/cmux.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,
} from "../pi-extension/subagents/subagent-done.ts";
import { __pollForExitTest__ } from "../pi-extension/subagents/cmux.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 sentUserMessages: string[] = [];
const sentMessages: Array<any> = [];
return {
registeredTools,
registeredCommands,
registeredMessageRenderers,
sentUserMessages,
sentMessages,
api: {
on() {},
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 [];
},
} as any,
};
}
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);
});
});
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;
}
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);
});
});
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("resolveEffectiveInteractive honors the explicit tool parameter over all else", () => {
assert.equal(
testApi.resolveEffectiveInteractive(
{ name: "A", task: "T", interactive: false },
{ autoExit: false, interactive: true },
),
false,
);
assert.equal(
testApi.resolveEffectiveInteractive(
{ name: "A", task: "T", interactive: true },
{ autoExit: true, interactive: false },
),
true,
);
});
it("bundled scout/researcher resolve as non-interactive; worker resolves as interactive", () => {
for (const name of ["scout", "researcher"]) {
const defs = testApi.loadAgentDefaults(name);
assert.ok(defs, `expected bundled agent ${name} to be discoverable`);
assert.equal(
testApi.resolveEffectiveInteractive({ name, task: "" }, defs),
false,
`${name} should resolve as non-interactive (autonomous, auto-exit)`,
);
}
const worker = testApi.loadAgentDefaults("worker");
assert.ok(worker, "expected bundled worker to be discoverable");
assert.equal(
testApi.resolveEffectiveInteractive({ name: "worker", task: "" }, worker),
true,
"worker should resolve as interactive (no 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("safe_bash"), "expected worker to keep safe_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("web_search")?.endsWith("web-search/index.ts"));
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"));
});
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 with fork override precedence", () => {
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", fork: true },
{ sessionMode: "lineage-only" },
),
"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",
},
);
assert.deepEqual(
testApi.resolveLaunchBehavior(
{ name: "A", task: "T", fork: true },
{ sessionMode: "lineage-only" },
),
{
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,caller_ping,subagent_done",
);
});
it("buildSubagentToolAllowlist returns null without an explicit tool restriction", () => {
assert.equal(testApi.buildSubagentToolAllowlist(undefined), null);
assert.equal(testApi.buildSubagentToolAllowlist(""), null);
});
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("cmux.ts interpretExitSidecar", () => {
const { interpretExitSidecar } = __pollForExitTest__;
it("decodes ping payloads", () => {
assert.deepEqual(
interpretExitSidecar({ type: "ping", name: "Worker", message: "need help" }),
{
reason: "ping",
exitCode: 0,
ping: { name: "Worker", message: "need help" },
},
);
});
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("tool registration", () => {
it("defaults resumed subagents to auto-exit and non-interactive tracking", () => {
const testApi = (subagentsModule as any).__test__;
assert.deepEqual(testApi.resolveResumeLaunchBehavior({}), {
autoExit: true,
interactive: false,
});
assert.deepEqual(testApi.resolveResumeLaunchBehavior({ autoExit: false }), {
autoExit: false,
interactive: true,
});
});
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("documents that `agent` (not `name`) selects the agent profile", () => {
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;
// `name` must clarify it is cosmetic and does NOT select a profile.
assert.match(props.name.description, /does NOT select/i);
// `agent` must steer the model to set it for a specific profile.
assert.match(props.agent.description, /which agent profile to spawn/i);
});
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, sessionId, message, and an autoExit override", () => {
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.ok(props.name, "expected name param");
assert.ok(props.sessionId, "expected sessionId param");
assert.equal(props.message.type, "string");
assert.equal(messageTool.parameters.required?.includes("message"), true);
const autoExitSchema = props.autoExit;
assert.equal(autoExitSchema.type, "boolean");
assert.match(autoExitSchema.description, /Defaults to true/);
});
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.subagentDone();
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("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/);
assert.match(presentation, /Session id: 019f-abc/);
assert.match(presentation, /subagent_message/);
});
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/);
assert.match(presentation, /Session id: 019f-xyz/);
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("defaults to 500ms when no env var 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;
delete process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS;
try {
assert.equal(testApi.getShellReadyDelayMs(), 500);
} finally {
if (original == null) delete process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS;
else process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS = original;
}
});
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", () => {
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, 16],
);
} finally {
Date.now = originalNow;
}
});
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)}`,
);
}
}
});
});
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("cmux.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"));
});
});
describe("parseCmuxFocusedSnapshot", () => {
it("parses focused surface and pane refs", () => {
assert.deepEqual(
parseCmuxFocusedSnapshot({ focused: { surface_ref: "surface:3", pane_ref: "pane:2" } }),
{ surfaceRef: "surface:3", paneRef: "pane:2" },
);
});
it("does not fall back to caller refs", () => {
assert.equal(
parseCmuxFocusedSnapshot({ caller: { surface_ref: "surface:1", pane_ref: "pane:1" } }),
null,
);
});
it("returns null for malformed values", () => {
assert.equal(parseCmuxFocusedSnapshot(null), null);
assert.equal(parseCmuxFocusedSnapshot({ focused: {} }), null);
});
});
describe("parseCmuxJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxJson("not json"), null);
});
it("parses valid JSON text", () => {
assert.deepEqual(parseCmuxJson('{"ok":true}'), { ok: true });
});
});
describe("parseCmuxFocusedSnapshotFromJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxFocusedSnapshotFromJson("not json"), null);
});
it("returns null when focused is absent or not an object", () => {
assert.equal(
parseCmuxFocusedSnapshotFromJson('{"focused":null,"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"}}'),
null,
);
assert.equal(
parseCmuxFocusedSnapshotFromJson('{"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"}}'),
null,
);
});
it("parses focused refs without falling back to caller refs", () => {
assert.deepEqual(
parseCmuxFocusedSnapshotFromJson(
'{"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"},"focused":{"surface_ref":"surface:2","pane_ref":"pane:3"}}',
),
{ surfaceRef: "surface:2", paneRef: "pane:3" },
);
});
});
describe("parseCmuxPaneRefForSurface", () => {
it("parses top-level pane refs for a surface", () => {
assert.equal(
parseCmuxPaneRefForSurface({ surface_ref: "surface:7", pane_ref: "pane:4" }, "surface:7"),
"pane:4",
);
});
it("parses caller pane refs for identify --surface output", () => {
assert.equal(
parseCmuxPaneRefForSurface(
{ caller: { surface_ref: "surface:7", pane_ref: "pane:4" } },
"surface:7",
),
"pane:4",
);
});
it("returns null when the surface does not match", () => {
assert.equal(
parseCmuxPaneRefForSurface({ surface_ref: "surface:8", pane_ref: "pane:4" }, "surface:7"),
null,
);
});
});
describe("parseCmuxPaneRefForSurfaceFromJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxPaneRefForSurfaceFromJson("not json", "surface:7"), null);
});
it("parses caller refs from cmux identify --surface JSON text", () => {
assert.equal(
parseCmuxPaneRefForSurfaceFromJson(
'{"caller":{"surface_ref":"surface:7","pane_ref":"pane:4"}}',
"surface:7",
),
"pane:4",
);
});
});
describe("zellij placement", () => {
const pane = (overrides: any) => ({
id: 1,
is_plugin: false,
is_floating: false,
is_selectable: true,
exited: false,
pane_rows: 20,
pane_columns: 80,
tab_id: 1,
...overrides,
});
it("matches Zellij direction and minimum split rules", () => {
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 5, pane_columns: 11 })), "right");
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 11, pane_columns: 5 })), "down");
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 5, pane_columns: 10 })), null);
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 4, pane_columns: 80 })), null);
assert.equal(canSplitZellijPane(pane({ pane_rows: 5, pane_columns: 11 })), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 11, pane_columns: 5 })), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 5, pane_columns: 10 })), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 4, pane_columns: 80 })), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 30, pane_columns: 100 }), 80, 20), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 45, pane_columns: 100 }), 80, 20), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 30, pane_columns: 170 }), 80, 20), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 31, pane_columns: 47 }), 50, 10), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 31, pane_columns: 77 }), 50, 10), true);
});
it("uses tab-scoped split only when all Zellij split candidates are safe", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 40, pane_columns: 120 }),
pane({ id: 11, tab_id: 1, pane_rows: 120, pane_columns: 100 }),
pane({ id: 12, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.deepEqual(plan, {
mode: "split",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
splitDirection: "down",
});
});
it("stacks when any Zellij split candidate would fall below Pi's configured minimum", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 100, pane_columns: 47 }),
pane({ id: 11, tab_id: 1, pane_rows: 31, pane_columns: 77 }),
],
10,
50,
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("stacks when Zellij would split a pane below Pi's usable minimum", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 20, pane_columns: 20 }),
pane({ id: 11, tab_id: 1, pane_rows: 18, pane_columns: 60 }),
pane({ id: 12, tab_id: 1, pane_rows: 10, pane_columns: 70 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("never chooses the parent pane as the stack target", () => {
const plan = selectZellijStackPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 60, pane_columns: 200 }),
pane({ id: 11, tab_id: 1, pane_rows: 10, pane_columns: 20 }),
pane({ id: 12, tab_id: 1, pane_rows: 8, pane_columns: 30 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 12,
targetPaneId: 12,
tabId: 1,
});
});
it("does not stack when the only usable pane is the parent", () => {
const plan = selectZellijStackPlacement(
[pane({ id: 10, tab_id: 1, pane_rows: 60, pane_columns: 200 })],
10,
);
assert.equal(plan, null);
});
it("stacks on the largest usable non-parent pane when none can split", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 5, pane_columns: 10 }),
pane({ id: 11, tab_id: 1, pane_rows: 6, pane_columns: 8 }),
pane({ id: 12, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("ignores floating, plugin, exited, unselectable, and other-tab panes", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 5, pane_columns: 10 }),
pane({ id: 11, tab_id: 1, pane_rows: 60, pane_columns: 200, is_floating: true }),
pane({ id: 12, tab_id: 1, pane_rows: 60, pane_columns: 200, is_plugin: true }),
pane({ id: 13, tab_id: 1, pane_rows: 60, pane_columns: 200, exited: true }),
pane({ id: 14, tab_id: 1, pane_rows: 60, pane_columns: 200, is_selectable: false }),
pane({ id: 15, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.equal(plan, null);
});
it("returns null when the parent pane cannot be found", () => {
assert.equal(selectZellijPlacement([pane({ id: 10 })], 99), null);
});
});
describe("isCmuxAvailable", () => {
it("returns boolean based on CMUX_SOCKET_PATH", () => {
// Can't easily mock env in node:test, just verify it returns a boolean
const result = isCmuxAvailable();
assert.equal(typeof result, "boolean");
});
});
describe("isWezTermAvailable", () => {
it("returns boolean based on WEZTERM_UNIX_SOCKET", () => {
const result = isWezTermAvailable();
assert.equal(typeof result, "boolean");
});
});
});