import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleActiveConsoleShortcut, openConsole, SymphonyConsoleComponent } from "./console.ts";
import { startSymphonyEventStream } from "./event-stream.ts";
import type { SymphonyEventEnvelope, SymphonyStateResponse } from "./http-client.ts";
import type { SymphonyRuntime } from "./runtime.ts";
import { createDefaultState } from "./state.ts";
vi.mock("./event-stream.ts", () => ({
startSymphonyEventStream: vi.fn(),
}));
beforeEach(() => {
vi.mocked(startSymphonyEventStream).mockReset();
vi.mocked(startSymphonyEventStream).mockImplementation(() => ({ close: vi.fn() }));
});
function workerStateFixture(): SymphonyStateResponse {
return {
tracker_project_url: "https://linear.app/kata-sh/project/symphony",
running: {
"issue-123": {
issue_id: "issue-123",
issue_identifier: "SIM-123",
issue_title: "Worker one",
attempt: 2,
workspace_path: "/tmp/symphony/issue-123",
started_at: "2026-05-14T12:00:00Z",
status: "running",
worker_host: "worker-a",
tracker_state: "In Progress",
},
"issue-777": {
issue_id: "issue-777",
issue_identifier: "SIM-777",
issue_title: "Worker two",
workspace_path: "/tmp/symphony/issue-777",
started_at: "2026-05-14T12:05:00Z",
status: "running",
error: "usage limit",
tracker_state: "Agent Review",
},
},
running_sessions: {
"issue-123": {
turn_count: 2,
last_activity_at: "2026-05-14T12:03:00Z",
last_event: "tool_call_completed",
last_event_message: "running cargo test",
},
},
running_session_info: {
"issue-123": { turn_count: 3, max_turns: 20, last_activity_ms: Date.parse("2026-05-14T12:04:00Z"), last_error: null },
"issue-777": { turn_count: 1, max_turns: 10, last_activity_ms: null, last_error: "usage limit" },
},
retry_queue: [{ issue_id: "issue-retry", identifier: "SIM-200", attempt: 3, due_in_ms: 90000, error: "rate limit", worker_host: "host-b", workspace_path: "/tmp/retry" }],
blocked: [{ issue_id: "issue-blocked", identifier: "SIM-300", title: "Blocked work", state: "Todo", blocker_identifiers: ["SIM-100", "SIM-101"] }],
pending_escalations: [{ request_id: "esc-1", issue_id: "issue-123", issue_identifier: "SIM-123", method: "approval", preview: "Approve cargo test?", created_at: "2026-05-14T12:06:00Z", timeout_ms: 600000 }],
completed: [{ issue_id: "issue-done", identifier: "SIM-400", title: "Done work", completed_at: "2026-05-14T13:00:00Z" }],
polling: { checking: false, next_poll_in_ms: 1000, poll_interval_ms: 30000 },
shared_context: { total_entries: 0, entries_by_scope: {}, oldest_entry_at: null, newest_entry_at: null },
supervisor: { active: true, steers_issued: 0, conflicts_detected: 0, patterns_detected: 0, escalations_created: 0 },
codex_totals: { input_tokens: 0, output_tokens: 0, total_tokens: 0, event_count: 0, seconds_running: 0 },
codex_rate_limits: null,
};
}
function runtimeEventsFixture(): SymphonyEventEnvelope[] {
return [
{
version: "v1",
sequence: 1,
timestamp: "2026-05-14T12:01:00Z",
kind: "runtime",
severity: "info",
event: "poll_completed",
payload: { summary: "checked tracker" },
},
{
version: "v1",
sequence: 2,
timestamp: "2026-05-14T12:02:00Z",
kind: "worker",
severity: "error",
issue: "SIM-777",
event: "worker_failed",
payload: { error_preview: "usage limit" },
},
];
}
function fakeTheme() {
return {
fg: (color: string, text: string) => `<${color}>${text}${color}>`,
bg: (color: string, text: string) => `${text}`,
bold: (text: string) => `${text}`,
};
}
describe("SymphonyConsoleComponent", () => {
it("renders Slice 1 health fields", () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
state.ownedProcess = { pid: 123, command: "symphony --no-tui", cwd: "/repo", baseUrl: state.attachedBaseUrl, startedAt: "2026-05-14T00:00:00Z" };
state.lastKnownState = {
baseUrl: state.attachedBaseUrl,
trackerProjectUrl: "https://github.com/gannonh/kata/projects/1",
runningCount: 2,
retryCount: 1,
blockedCount: 0,
completedCount: 4,
pollingChecking: false,
nextPollInMs: 5000,
updatedAt: "2026-05-14T00:00:01Z",
};
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => undefined,
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
const output = consoleComponent.render(120).join("\n");
expect(output).toContain("Symphony Console");
expect(output).toContain("dashboard: http://127.0.0.1:8080");
expect(output).toContain("project: https://github.com/gannonh/kata/projects/1");
expect(output).toContain("running: 2");
expect(output).toContain("retry: 1");
expect(output).toContain("owned process: pid 123");
});
it("renders running workers, selected issue details, and recent runtime events", () => {
const state = createDefaultState();
state.console.showDetails = true;
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => runtimeEventsFixture(),
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
const output = consoleComponent.render(160).join("\n");
expect(output).toContain("Running Workers");
expect(output).toContain("> SIM-123");
expect(output).toContain("SIM-777");
expect(output).toContain("Selected Issue");
expect(output).toContain("issue: SIM-123 Worker one");
expect(output).toContain("tracker state: In Progress");
expect(output).toContain("attempt: 2");
expect(output).toContain("turns: 3 / 20");
expect(output).toContain("last activity: 2026-05-14T12:04:00.000Z");
expect(output).toContain("worker host: worker-a");
expect(output).toContain("workspace: /tmp/symphony/issue-123");
expect(output).toContain("Events");
expect(output).toContain("worker_failed usage limit");
});
it("renders retry, blocked, completed, and selected issue sections", () => {
const state = createDefaultState();
state.console.showDetails = true;
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
const output = consoleComponent.render(180).join("\n");
expect(output).toContain("Retry Queue");
expect(output).toContain("Blocked Issues");
expect(output).toContain("Completed Issues");
expect(output).toContain("Selected Issue");
expect(output).toContain("SIM-200");
expect(output).toContain("retry in 1m 30s");
expect(output).toContain("SIM-300");
expect(output).toContain("SIM-100, SIM-101");
expect(output).toContain("SIM-400");
expect(output).toContain("2026-05-14T13:00:00Z");
});
it("moves selection across retry, blocked, and completed rows and limits steering to running rows", () => {
const state = createDefaultState();
state.console.showDetails = true;
const steer = vi.fn(async () => undefined);
const notify = vi.fn();
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer,
respondToEscalation: async () => undefined,
prompt: async () => "not used",
close: () => undefined,
requestRender: () => undefined,
notify,
});
consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("\u001b[B");
let output = consoleComponent.render(180).join("\n");
expect(output).toContain("> SIM-200");
expect(output).toContain("issue: SIM-200 rate limit");
expect(output).toContain("kind: retry");
expect(output).toContain("status: retry in 1m 30s");
expect(output).toContain("workspace: /tmp/retry");
consoleComponent.handleInput("s");
expect(steer).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith("Select a running worker before steering", "warning");
consoleComponent.handleInput("\u001b[B");
output = consoleComponent.render(180).join("\n");
expect(output).toContain("> SIM-300");
expect(output).toContain("issue: SIM-300 Blocked work");
expect(output).toContain("kind: blocked");
expect(output).toContain("blockers: SIM-100, SIM-101");
consoleComponent.handleInput("\u001b[B");
output = consoleComponent.render(180).join("\n");
expect(output).toContain("> SIM-400");
expect(output).toContain("issue: SIM-400 Done work");
expect(output).toContain("kind: completed");
expect(output).toContain("completed at: 2026-05-14T13:00:00Z");
});
it("renders pending escalations and selected escalation details", () => {
const state = createDefaultState();
state.console.showDetails = true;
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("\u001b[B");
const output = consoleComponent.render(180).join("\n");
expect(output).toContain("Pending Escalations");
expect(output).toContain("> esc-1");
expect(output).toContain("SIM-123");
expect(output).toContain("approval");
expect(output).toContain("Approve cargo test?");
expect(output).toContain("Selected Escalation");
expect(output).toContain("request: esc-1");
expect(output).toContain("timeout: 10m 0s");
});
it("expands keyboard shortcuts onto one row when the terminal is wide", () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
const output = consoleComponent.render(220).join("\n");
expect(output).toContain("Keyboard: ctrl+shift+↑/↓ select • ctrl+shift+r refresh • ctrl+shift+t steer • ctrl+shift+e escalation • ctrl+shift+i details • ctrl+shift+q close");
});
it("renders boxed colored sections and command actions for console readability", () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
state.lastKnownState = {
baseUrl: state.attachedBaseUrl,
trackerProjectUrl: "https://github.com/gannonh/kata/projects/1",
runningCount: 2,
retryCount: 1,
blockedCount: 1,
completedCount: 4,
pollingChecking: false,
nextPollInMs: 5000,
updatedAt: "2026-05-14T00:00:01Z",
};
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => runtimeEventsFixture(),
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
theme: fakeTheme(),
});
const output = consoleComponent.render(220).join("\n");
expect(output).toContain("┌");
expect(output).toContain("└");
expect(output).toContain("Status");
expect(output).toContain("Worker Summary");
expect(output).toContain("Running Workers");
expect(output).toContain("Selected Issue");
expect(output).toContain("Events");
expect(output).toContain("Keyboard");
expect(output).toContain("ctrl+shift+↑/↓ select");
expect(output).toContain("ctrl+shift+r refresh");
expect(output).toContain("ctrl+shift+t steer");
expect(output).toContain("ctrl+shift+e escalation");
expect(output).toContain("ctrl+shift+i details");
expect(output).toContain("ctrl+shift+q close");
expect(output).toContain("Keyboard: ctrl+shift+↑/↓ select • ctrl+shift+r refresh • ctrl+shift+t steer • ctrl+shift+e escalation • ctrl+shift+i details • ctrl+shift+q close");
expect(output).toContain("┌");
expect(output).toContain("running: 2");
expect(output).toContain("retry: 1");
expect(output).toContain("blocked: 1");
expect(output).toContain("> SIM-123");
expect(output).not.toContain("Running workers");
expect(output).not.toContain("Selected worker");
expect(output).not.toContain("Recent worker/runtime events");
});
it("moves selection with arrow keys", () => {
const state = createDefaultState();
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
consoleComponent.handleInput("\u001b[B");
const output = consoleComponent.render(160).join("\n");
expect(output).toContain("> SIM-777");
expect(output).toContain("issue: SIM-777 Worker two");
});
it("toggles selected issue details", () => {
const state = createDefaultState();
state.console.showDetails = true;
const consoleComponent = new SymphonyConsoleComponent({
state,
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
consoleComponent.handleInput("d");
expect(consoleComponent.render(160).join("\n")).not.toContain("issue: SIM-123 Worker one");
});
it("prompts for a steer instruction and sends it to the selected worker", async () => {
let resolveSteered: (() => void) | undefined;
const steered = new Promise((resolve) => {
resolveSteered = resolve;
});
const steer = vi.fn(async () => {
resolveSteered?.();
});
let resolveNotified: (() => void) | undefined;
const notified = new Promise((resolve) => {
resolveNotified = resolve;
});
const notify = vi.fn(() => {
resolveNotified?.();
});
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer,
respondToEscalation: async () => undefined,
prompt: async () => "Use the existing auth module",
close: () => undefined,
requestRender: () => undefined,
notify,
});
consoleComponent.handleInput("s");
await steered;
await notified;
expect(steer).toHaveBeenCalledWith("SIM-123", "Use the existing auth module");
expect(notify).toHaveBeenCalledWith("Steer delivered to SIM-123", "info");
});
it("responds to the selected escalation with parsed JSON input", async () => {
let resolveResponded: (() => void) | undefined;
const responded = new Promise((resolve) => {
resolveResponded = resolve;
});
const respondToEscalation = vi.fn(async () => {
resolveResponded?.();
});
let resolveNotified: (() => void) | undefined;
const notified = new Promise((resolve) => {
resolveNotified = resolve;
});
const notify = vi.fn(() => {
resolveNotified?.();
});
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation,
prompt: async () => '{"approved":true}',
close: () => undefined,
requestRender: () => undefined,
notify,
});
for (let index = 0; index < 5; index += 1) consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("e");
await responded;
await notified;
expect(respondToEscalation).toHaveBeenCalledWith("esc-1", { approved: true });
expect(notify).toHaveBeenCalledWith("Escalation response sent for esc-1", "info");
});
it("responds to the selected escalation with plain text when input is not JSON", async () => {
let resolveResponded: (() => void) | undefined;
const responded = new Promise((resolve) => {
resolveResponded = resolve;
});
const respondToEscalation = vi.fn(async () => {
resolveResponded?.();
});
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation,
prompt: async () => "approved",
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
for (let index = 0; index < 5; index += 1) consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("e");
await responded;
expect(respondToEscalation).toHaveBeenCalledWith("esc-1", "approved");
});
it("cancels escalation response when prompt returns only whitespace", async () => {
const respondToEscalation = vi.fn(async () => undefined);
const requestRender = vi.fn();
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation,
prompt: async () => " ",
close: () => undefined,
requestRender,
notify: () => undefined,
});
for (let index = 0; index < 5; index += 1) consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("e");
await expect.poll(() => requestRender.mock.calls.length, { interval: 10, timeout: 1000 }).toBeGreaterThan(0);
expect(respondToEscalation).not.toHaveBeenCalled();
});
it.each(["{approved: true}", "\"approved"])("rejects malformed JSON-like escalation response before sending: %s", async (input) => {
const respondToEscalation = vi.fn(async () => undefined);
const notify = vi.fn();
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation,
prompt: async () => input,
close: () => undefined,
requestRender: () => undefined,
notify,
});
for (let index = 0; index < 5; index += 1) consoleComponent.handleInput("\u001b[B");
consoleComponent.handleInput("e");
await expect.poll(() => notify.mock.calls.length, { interval: 10, timeout: 1000 }).toBe(1);
expect(respondToEscalation).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith("Escalation response must be valid JSON or plain text", "error");
});
it("notifies when escalation response is requested without a selected escalation", () => {
const notify = vi.fn();
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify,
});
consoleComponent.handleInput("e");
expect(notify).toHaveBeenCalledWith("Select an escalation before responding", "warning");
});
it("notifies and requests render when steering prompt fails", async () => {
const notify = vi.fn();
const requestRender = vi.fn();
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => workerStateFixture(),
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => {
throw new Error("prompt failed");
},
close: () => undefined,
requestRender,
notify,
});
consoleComponent.handleInput("s");
await expect.poll(() => notify.mock.calls.length, { interval: 10, timeout: 1000 }).toBe(1);
expect(notify).toHaveBeenCalledWith("prompt failed", "error");
expect(requestRender).toHaveBeenCalledOnce();
});
it("closes on q", () => {
const close = vi.fn();
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => undefined,
getEvents: () => [],
refresh: async () => undefined,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close,
requestRender: () => undefined,
notify: () => undefined,
});
consoleComponent.handleInput("q");
expect(close).toHaveBeenCalledOnce();
});
it("ignores refresh input while a refresh is already running", async () => {
let resolveRefresh: (() => void) | undefined;
const refreshDone = new Promise((resolve) => {
resolveRefresh = resolve;
});
const refresh = vi.fn(() => refreshDone);
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => undefined,
getEvents: () => [],
refresh,
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify: () => undefined,
});
consoleComponent.handleInput("r");
consoleComponent.handleInput("r");
expect(refresh).toHaveBeenCalledOnce();
resolveRefresh?.();
await refreshDone;
});
it("notifies when a refresh fails", async () => {
let resolveNotified: (() => void) | undefined;
const notified = new Promise((resolve) => {
resolveNotified = resolve;
});
const notify = vi.fn((_message: string, _level: "info" | "warning" | "error") => {
resolveNotified?.();
});
const consoleComponent = new SymphonyConsoleComponent({
state: createDefaultState(),
getState: () => undefined,
getEvents: () => [],
refresh: async () => {
throw new Error("refresh failed");
},
steer: async () => undefined,
respondToEscalation: async () => undefined,
prompt: async () => undefined,
close: () => undefined,
requestRender: () => undefined,
notify,
});
consoleComponent.handleInput("r");
await notified;
expect(notify).toHaveBeenCalledWith("refresh failed", "error");
});
});
describe("openConsole", () => {
it("renders as an above-editor widget and refreshes stale state from events", async () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
state.lastKnownState = {
baseUrl: state.attachedBaseUrl,
runningCount: 0,
retryCount: 0,
blockedCount: 0,
completedCount: 0,
pollingChecking: false,
nextPollInMs: 1000,
updatedAt: "2026-05-14T12:00:00Z",
};
let capturedOnEvent: ((event: SymphonyEventEnvelope) => void) | undefined;
vi.mocked(startSymphonyEventStream).mockImplementation((options) => {
capturedOnEvent = options.onEvent;
return { close: vi.fn() };
});
const requestRender = vi.fn();
const custom = vi.fn();
const setWidget = vi.fn((_key: string, factory: unknown) => {
const component = (factory as (tui: { requestRender: () => void }, theme: ReturnType) => SymphonyConsoleComponent)({ requestRender }, fakeTheme());
capturedOnEvent?.({
version: "v1",
sequence: 1,
timestamp: "2026-05-14T12:00:00Z",
kind: "worker",
severity: "info",
issue: "SIM-123",
event: "worker_started",
payload: {},
});
return component;
});
const ctx = { ui: { notify: vi.fn(), custom, input: vi.fn(), setWidget } } as unknown as ExtensionContext;
const refreshState = vi.fn(async function (this: { lastState?: SymphonyStateResponse }) {
this.lastState = workerStateFixture();
return this.lastState;
});
const runtime = {
client: {},
state,
lastState: undefined,
recentEvents: [],
recordEvent: vi.fn(function (this: { recentEvents: SymphonyEventEnvelope[] }, event: SymphonyEventEnvelope) {
this.recentEvents.push(event);
}),
requestRefresh: vi.fn(async () => undefined),
refreshState,
steerWorker: vi.fn(async () => undefined),
errorText: vi.fn((error: unknown) => (error instanceof Error ? error.message : String(error))),
} as unknown as SymphonyRuntime;
await openConsole(ctx, runtime);
expect(custom).not.toHaveBeenCalled();
expect(setWidget).toHaveBeenCalledWith("symphony-console", expect.any(Function));
expect(startSymphonyEventStream).toHaveBeenCalledWith(expect.objectContaining({ baseUrl: "http://127.0.0.1:8080" }));
await expect.poll(() => refreshState.mock.calls.length, { interval: 10, timeout: 1000 }).toBe(2);
expect(requestRender).toHaveBeenCalled();
});
it("reports stream close errors after malformed stream messages", async () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
let capturedOnError: ((error: Error) => void) | undefined;
vi.mocked(startSymphonyEventStream).mockImplementation((options) => {
capturedOnError = options.onError;
return { close: vi.fn() };
});
const notify = vi.fn();
const setWidget = vi.fn((_key: string, factory: unknown) => {
(factory as (tui: { requestRender: () => void }, theme: ReturnType) => SymphonyConsoleComponent)({ requestRender: vi.fn() }, fakeTheme());
});
const ctx = { ui: { notify, custom: vi.fn(), input: vi.fn(), setWidget } } as unknown as ExtensionContext;
const runtime = {
client: {},
state,
lastState: workerStateFixture(),
recentEvents: [],
refreshState: vi.fn(async () => workerStateFixture()),
requestRefresh: vi.fn(async () => workerStateFixture()),
steerWorker: vi.fn(async () => undefined),
errorText: vi.fn((error: unknown) => (error instanceof Error ? error.message : String(error))),
} as unknown as SymphonyRuntime;
await openConsole(ctx, runtime);
capturedOnError?.(new Error("invalid Symphony event JSON"));
capturedOnError?.(new Error("Symphony event stream closed with code 1006"));
expect(notify).toHaveBeenCalledWith("Symphony event stream unavailable: invalid Symphony event JSON", "warning");
expect(notify).toHaveBeenCalledWith("Symphony event stream unavailable: Symphony event stream closed with code 1006", "warning");
});
it("lets global shortcuts control the active above-editor console", async () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
state.console.showDetails = true;
vi.mocked(startSymphonyEventStream).mockImplementation(() => ({ close: vi.fn() }));
let component: SymphonyConsoleComponent | undefined;
const requestRender = vi.fn();
const setWidget = vi.fn((_key: string, factory: unknown) => {
component = (factory as (tui: { requestRender: () => void }, theme: ReturnType) => SymphonyConsoleComponent)({ requestRender }, fakeTheme());
});
const ctx = { ui: { notify: vi.fn(), custom: vi.fn(), input: vi.fn(), setWidget } } as unknown as ExtensionContext;
const runtime = {
client: {},
state,
lastState: workerStateFixture(),
recentEvents: [],
refreshState: vi.fn(async () => workerStateFixture()),
requestRefresh: vi.fn(async () => workerStateFixture()),
steerWorker: vi.fn(async () => undefined),
respondToEscalation: vi.fn(async () => undefined),
errorText: vi.fn((error: unknown) => (error instanceof Error ? error.message : String(error))),
} as unknown as SymphonyRuntime;
await openConsole(ctx, runtime);
await handleActiveConsoleShortcut("selectNext", ctx);
await handleActiveConsoleShortcut("toggleDetails", ctx);
const output = component?.render(160).join("\n") ?? "";
expect(output).toContain("> SIM-777");
expect(output).not.toContain("issue: SIM-777 Worker two");
expect(requestRender).toHaveBeenCalled();
});
it("lets a global shortcut respond to the selected active-console escalation", async () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
vi.mocked(startSymphonyEventStream).mockImplementation(() => ({ close: vi.fn() }));
const requestRender = vi.fn();
const setWidget = vi.fn((_key: string, factory: unknown) => {
(factory as (tui: { requestRender: () => void }, theme: ReturnType) => SymphonyConsoleComponent)({ requestRender }, fakeTheme());
});
const ctx = { ui: { notify: vi.fn(), custom: vi.fn(), input: vi.fn(async () => "approved"), setWidget } } as unknown as ExtensionContext;
const runtime = {
client: {},
state,
lastState: workerStateFixture(),
recentEvents: [],
refreshState: vi.fn(async () => workerStateFixture()),
requestRefresh: vi.fn(async () => workerStateFixture()),
steerWorker: vi.fn(async () => undefined),
respondToEscalation: vi.fn(async () => undefined),
errorText: vi.fn((error: unknown) => (error instanceof Error ? error.message : String(error))),
} as unknown as SymphonyRuntime;
await openConsole(ctx, runtime);
for (let index = 0; index < 5; index += 1) await handleActiveConsoleShortcut("selectNext", ctx);
await handleActiveConsoleShortcut("respondEscalation", ctx);
expect(runtime.respondToEscalation).toHaveBeenCalledWith("esc-1", "approved");
expect(requestRender).toHaveBeenCalled();
});
it("notifies and still renders the widget when launch refresh fails", async () => {
const state = createDefaultState();
state.attachedBaseUrl = "http://127.0.0.1:8080";
state.lastKnownState = {
baseUrl: state.attachedBaseUrl,
trackerProjectUrl: "https://github.com/gannonh/kata/projects/1",
runningCount: 1,
retryCount: 0,
blockedCount: 0,
completedCount: 2,
pollingChecking: false,
nextPollInMs: 1000,
updatedAt: "2026-05-14T00:00:01Z",
};
const notify = vi.fn();
const setWidget = vi.fn((_key: string, factory: unknown) => {
const component = (factory as (tui: { requestRender: () => void }, theme: ReturnType) => SymphonyConsoleComponent)({ requestRender: vi.fn() }, fakeTheme());
expect(component.render(120).join("\n")).toContain("running: 1");
});
const ctx = { ui: { notify, custom: vi.fn(), input: vi.fn(), setWidget } } as unknown as ExtensionContext;
const runtime = {
client: {},
state,
lastState: undefined,
recentEvents: [],
refreshState: vi.fn(async () => {
throw new Error("launch refresh failed");
}),
requestRefresh: vi.fn(async () => undefined),
steerWorker: vi.fn(async () => undefined),
errorText: vi.fn((error: unknown) => (error instanceof Error ? `formatted: ${error.message}` : String(error))),
} as unknown as SymphonyRuntime;
await openConsole(ctx, runtime);
expect(notify).toHaveBeenCalledWith("formatted: launch refresh failed", "error");
expect(setWidget).toHaveBeenCalledOnce();
});
});