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 = []; const registeredCommands: Array = []; const registeredMessageRenderers: Array = []; const sentUserMessages: string[] = []; const sentMessages: Array = []; 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(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, ) { 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; }) => ({ 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 all resolve as non-interactive (auto-exit)", () => { for (const name of ["scout", "researcher", "worker"]) { 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)`, ); } }); 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 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,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("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 only name, sessionId, and required message (no autoExit knob)", () => { 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", "sessionId"], "only name/sessionId/message should remain", ); assert.equal(props.message.type, "string"); assert.equal(messageTool.parameters.required?.includes("message"), true); 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 = {}) { 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((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 = {}) { 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; 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; 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 reserved by in-flight parallel spawns", () => { const testApi = (subagentsModule as any).__test__; const runningMap = testApi.runningSubagents as Map; const reserved = testApi.reservedNames as Set; 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; 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; 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; 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"); }); }); });