import { afterEach, expect, expectTypeOf, test, vi } from "vitest"; import { DaemonClient, type DaemonTransport } from "./daemon-client"; import { encodeFileTransferFrame, FileTransferOpcode } from "../shared/binary-frames/index.js"; import { asUint8Array, decodeTerminalResizePayload, decodeTerminalStreamFrame, encodeTerminalSnapshotPayload, encodeTerminalStreamFrame, TerminalStreamOpcode, } from "../shared/terminal-stream-protocol.js"; expectTypeOf<"getGitDiff" extends keyof DaemonClient ? true : false>().toEqualTypeOf(); expectTypeOf< "getHighlightedDiff" extends keyof DaemonClient ? true : false >().toEqualTypeOf(); expectTypeOf< "exploreFileSystem" extends keyof DaemonClient ? true : false >().toEqualTypeOf(); function createMockLogger() { return { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; } function createMockTransport() { const sent: Array = []; let onMessage: (data: unknown) => void = () => {}; let onOpen: () => void = () => {}; let onClose: (_event?: unknown) => void = () => {}; let onError: (_event?: unknown) => void = () => {}; let serverInfoOrdinal = 1; const transport: DaemonTransport = { send: (data) => sent.push(data), close: () => {}, onMessage: (handler) => { onMessage = handler; return () => {}; }, onOpen: (handler) => { onOpen = handler; return () => {}; }, onClose: (handler) => { onClose = handler; return () => {}; }, onError: (handler) => { onError = handler; return () => {}; }, }; return { transport, sent, triggerOpen: (options?: { preserveSent?: boolean }) => { onOpen(); if (!options?.preserveSent) { // Ignore HELLO handshake payloads in assertions. sent.length = 0; } onMessage( JSON.stringify({ type: "session", message: { type: "status", payload: { status: "server_info", serverId: `srv_test_${serverInfoOrdinal++}`, hostname: null, version: null, }, }, }), ); }, triggerClose: (event?: unknown) => onClose(event), triggerError: (event?: unknown) => onError(event), triggerMessage: (data: unknown) => onMessage(data), }; } function wrapSessionMessage(message: unknown): string { return JSON.stringify({ type: "session", message, }); } const clients: DaemonClient[] = []; afterEach(async () => { await Promise.all(clients.map((client) => client.close())); clients.length = 0; }); test("dedupes in-flight checkout status requests per agentId", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const p1 = client.getCheckoutStatus("/tmp/project"); const p2 = client.getCheckoutStatus("/tmp/project"); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "checkout_status_request"; cwd: string; requestId: string }; }; const response = { type: "session", message: { type: "checkout_status_response", payload: { cwd: "/tmp/project", error: null, requestId: request.message.requestId, isGit: false, isPaseoOwnedWorktree: false, repoRoot: null, currentBranch: null, isDirty: null, baseRef: null, aheadBehind: null, aheadOfOrigin: null, behindOfOrigin: null, hasRemote: false, remoteUrl: null, }, }, }; mock.triggerMessage(JSON.stringify(response)); const [r1, r2] = await Promise.all([p1, p2]); expect(r1).toMatchObject({ cwd: "/tmp/project", requestId: request.message.requestId, isGit: false, }); expect(r2).toMatchObject({ cwd: "/tmp/project", requestId: request.message.requestId, isGit: false, }); // After completion, a new call should issue a new request. const p3 = client.getCheckoutStatus("/tmp/project"); expect(mock.sent).toHaveLength(2); const request2 = JSON.parse(mock.sent[1]) as { type: "session"; message: { type: "checkout_status_request"; cwd: string; requestId: string }; }; mock.triggerMessage( JSON.stringify({ ...response, message: { ...response.message, payload: { ...response.message.payload, requestId: request2.message.requestId }, }, }), ); await expect(p3).resolves.toMatchObject({ cwd: "/tmp/project", requestId: request2.message.requestId, isGit: false, }); }); test("passes password as HTTP bearer header and WebSocket subprotocol", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const transportFactory = vi.fn(() => mock.transport); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", password: "shared-secret", logger, reconnect: { enabled: false }, transportFactory, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; expect(transportFactory).toHaveBeenCalledWith({ url: "ws://test", headers: { Authorization: "Bearer shared-secret" }, protocols: ["paseo.bearer.shared-secret"], }); }); test("advertises reasoning_merge_enum in hello", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen({ preserveSent: true }); await connectPromise; expect(mock.sent).toHaveLength(1); expect(JSON.parse(mock.sent[0] as string)).toEqual({ type: "hello", clientId: "clsk_unit_test", clientType: "cli", protocolVersion: 1, capabilities: { reasoning_merge_enum: true, }, }); }); test("does not reconnect after close when ensureConnected is called", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; expect(client.getConnectionState().status).toBe("connected"); await client.close(); expect(client.getConnectionState().status).toBe("disposed"); client.ensureConnected(); expect(client.getConnectionState().status).toBe("disposed"); }); test("listDirectory sends a list file explorer request and returns directory entries", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const responsePromise = client.listDirectory("/tmp/project", "src", "req-list"); expect(JSON.parse(String(mock.sent[0]))).toEqual({ type: "session", message: { type: "file_explorer_request", cwd: "/tmp/project", path: "src", mode: "list", requestId: "req-list", }, }); mock.triggerMessage( wrapSessionMessage({ type: "file_explorer_response", payload: { cwd: "/tmp/project", path: "src", mode: "list", directory: { path: "src", entries: [ { name: "index.ts", path: "src/index.ts", kind: "file", size: 12, modifiedAt: "2026-05-02T00:00:00.000Z", }, ], }, file: null, error: null, requestId: "req-list", }, }), ); await expect(responsePromise).resolves.toEqual({ path: "src", entries: [ { name: "index.ts", path: "src/index.ts", kind: "file", size: 12, modifiedAt: "2026-05-02T00:00:00.000Z", }, ], }); }); test("readFile hides legacy base64 behind bytes", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const responsePromise = client.readFile("/tmp/project", "logo.png", "req-file"); expect(JSON.parse(String(mock.sent[0]))).toEqual({ type: "session", message: { type: "file_explorer_request", cwd: "/tmp/project", path: "logo.png", mode: "file", acceptBinary: true, requestId: "req-file", }, }); mock.triggerMessage( wrapSessionMessage({ type: "file_explorer_response", payload: { cwd: "/tmp/project", path: "logo.png", mode: "file", directory: null, file: { path: "logo.png", kind: "image", encoding: "base64", content: "aGVsbG8=", mimeType: "image/png", size: 5, modifiedAt: "2026-05-02T00:00:00.000Z", }, error: null, requestId: "req-file", }, }), ); const result = await responsePromise; expect(result).toMatchObject({ mime: "image/png", size: 5, path: "logo.png", kind: "image", modifiedAt: "2026-05-02T00:00:00.000Z", }); expect(new TextDecoder().decode(result.bytes)).toBe("hello"); }); test("readFile resolves from binary file frames when the daemon supports them", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const responsePromise = client.readFile("/tmp/project", "logo.png", "req-binary"); expect(JSON.parse(String(mock.sent[0]))).toEqual({ type: "session", message: { type: "file_explorer_request", cwd: "/tmp/project", path: "logo.png", mode: "file", acceptBinary: true, requestId: "req-binary", }, }); mock.triggerMessage( encodeFileTransferFrame({ opcode: FileTransferOpcode.FileBegin, requestId: "req-binary", metadata: { mime: "image/png", size: 5, encoding: "binary", modifiedAt: "2026-05-02T00:00:00.000Z", }, }), ); mock.triggerMessage( encodeFileTransferFrame({ opcode: FileTransferOpcode.FileChunk, requestId: "req-binary", payload: new TextEncoder().encode("hello"), }), ); mock.triggerMessage( encodeFileTransferFrame({ opcode: FileTransferOpcode.FileEnd, requestId: "req-binary", }), ); const result = await responsePromise; expect(result).toMatchObject({ mime: "image/png", size: 5, path: "logo.png", kind: "image", modifiedAt: "2026-05-02T00:00:00.000Z", }); expect(new TextDecoder().decode(result.bytes)).toBe("hello"); }); test("normalizes workspace_setup_progress into a workspace-scoped daemon event", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const events: Array[0]>[0]> = []; client.subscribe((event) => { events.push(event); }); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; mock.triggerMessage( wrapSessionMessage({ type: "workspace_setup_progress", payload: { workspaceId: "ws-feature-a", status: "running", detail: { type: "worktree_setup", worktreePath: "/tmp/project/.paseo/worktrees/feature-a", branchName: "feature-a", log: "phase-one\n", commands: [ { index: 1, command: "npm install", cwd: "/tmp/project/.paseo/worktrees/feature-a", log: "phase-one\n", status: "running", exitCode: null, }, ], }, error: null, }, }), ); expect(events).toContainEqual({ type: "workspace_setup_progress", workspaceId: "ws-feature-a", payload: { workspaceId: "ws-feature-a", status: "running", detail: { type: "worktree_setup", worktreePath: "/tmp/project/.paseo/worktrees/feature-a", branchName: "feature-a", log: "phase-one\n", commands: [ { index: 1, command: "npm install", cwd: "/tmp/project/.paseo/worktrees/feature-a", log: "phase-one\n", status: "running", exitCode: null, }, ], }, error: null, }, }); }); test("sends create_agent_request with string workspace ids", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createAgent({ provider: "codex", cwd: "/tmp/project/.paseo/worktrees/feature-a", workspaceId: "ws-feature-a", title: "Compat agent", modeId: "default", }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "create_agent_request"; requestId: string; workspaceId: string; }; }; expect(request.message).toEqual( expect.objectContaining({ type: "create_agent_request", workspaceId: "ws-feature-a", }), ); mock.triggerMessage( wrapSessionMessage({ type: "status", payload: { status: "agent_create_failed", requestId: request.message.requestId, error: "compat test sentinel", }, }), ); await expect(createPromise).rejects.toThrow("compat test sentinel"); }); test("sends structured attachments with create_agent_request", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createAgent({ provider: "codex", cwd: "/tmp/project", initialPrompt: "Review this PR", attachments: [ { type: "github_pr", mimeType: "application/github-pr", number: 123, title: "Fix race in worktree setup", url: "https://github.com/getpaseo/paseo/pull/123", baseRefName: "main", headRefName: "fix/worktree-race", }, ], }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "create_agent_request"; requestId: string; attachments: Array<{ type: string; mimeType: string; number: number }>; }; }; expect(request.message.attachments).toEqual([ { type: "github_pr", mimeType: "application/github-pr", number: 123, title: "Fix race in worktree setup", url: "https://github.com/getpaseo/paseo/pull/123", baseRefName: "main", headRefName: "fix/worktree-race", }, ]); mock.triggerMessage( wrapSessionMessage({ type: "status", payload: { status: "agent_create_failed", requestId: request.message.requestId, error: "attachment test sentinel", }, }), ); await expect(createPromise).rejects.toThrow("attachment test sentinel"); }); test("sends worktree base-ref fields in create_agent_request git options", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createAgent({ provider: "codex", cwd: "/tmp/project", requestId: "req-agent-ref", git: { createWorktree: true, worktreeSlug: "review-pr-123", refName: "feature/worktree-base-ref", action: "checkout", githubPrNumber: 123, }, }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "create_agent_request"; requestId: string; git: { createWorktree: boolean; worktreeSlug: string; refName: string; action: string; githubPrNumber: number; }; }; }; expect(request.message.git).toEqual({ createWorktree: true, worktreeSlug: "review-pr-123", refName: "feature/worktree-base-ref", action: "checkout", githubPrNumber: 123, }); mock.triggerMessage( wrapSessionMessage({ type: "status", payload: { status: "agent_create_failed", requestId: request.message.requestId, error: "git ref fields sentinel", }, }), ); await expect(createPromise).rejects.toThrow("git ref fields sentinel"); }); test("omitting create_agent_request worktree base-ref fields preserves legacy wire shape", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createAgent({ provider: "codex", cwd: "/tmp/project", requestId: "req-agent-legacy", git: { createWorktree: true, worktreeSlug: "feature-a", }, }); expect(String(mock.sent[0])).toBe( JSON.stringify({ type: "session", message: { type: "create_agent_request", config: { provider: "codex", cwd: "/tmp/project", }, git: { createWorktree: true, worktreeSlug: "feature-a", }, labels: {}, requestId: "req-agent-legacy", }, }), ); mock.triggerMessage( wrapSessionMessage({ type: "status", payload: { status: "agent_create_failed", requestId: "req-agent-legacy", error: "legacy git shape sentinel", }, }), ); await expect(createPromise).rejects.toThrow("legacy git shape sentinel"); }); test("sends structured first-agent context attachments with create_paseo_worktree_request", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createPaseoWorktree({ cwd: "/tmp/project", worktreeSlug: "review-pr-123", firstAgentContext: { attachments: [ { type: "github_pr", mimeType: "application/github-pr", number: 123, title: "Fix race in worktree setup", url: "https://github.com/getpaseo/paseo/pull/123", }, ], }, }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "create_paseo_worktree_request"; requestId: string; firstAgentContext: { attachments: Array<{ type: string; mimeType: string; number: number }>; }; }; }; expect(request.message.firstAgentContext.attachments).toEqual([ { type: "github_pr", mimeType: "application/github-pr", number: 123, title: "Fix race in worktree setup", url: "https://github.com/getpaseo/paseo/pull/123", }, ]); mock.triggerMessage( wrapSessionMessage({ type: "create_paseo_worktree_response", payload: { requestId: request.message.requestId, workspace: null, error: "worktree attachment sentinel", setupTerminalId: null, }, }), ); await expect(createPromise).resolves.toEqual({ requestId: request.message.requestId, workspace: null, error: "worktree attachment sentinel", setupTerminalId: null, }); }); test("sends worktree base-ref fields in create_paseo_worktree_request", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createPaseoWorktree( { cwd: "/tmp/project", worktreeSlug: "review-pr-123", refName: "feature/worktree-base-ref", action: "checkout", githubPrNumber: 123, }, "req-worktree-ref", ); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "create_paseo_worktree_request"; requestId: string; cwd: string; worktreeSlug: string; refName: string; action: string; githubPrNumber: number; }; }; expect(request.message).toEqual({ type: "create_paseo_worktree_request", cwd: "/tmp/project", worktreeSlug: "review-pr-123", refName: "feature/worktree-base-ref", action: "checkout", githubPrNumber: 123, requestId: "req-worktree-ref", }); mock.triggerMessage( wrapSessionMessage({ type: "create_paseo_worktree_response", payload: { requestId: request.message.requestId, workspace: null, error: "worktree ref fields sentinel", setupTerminalId: null, }, }), ); await expect(createPromise).resolves.toEqual({ requestId: request.message.requestId, workspace: null, error: "worktree ref fields sentinel", setupTerminalId: null, }); }); test("omitting create_paseo_worktree_request worktree base-ref fields preserves legacy wire shape", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const createPromise = client.createPaseoWorktree( { cwd: "/tmp/project", worktreeSlug: "feature-a", }, "req-worktree-legacy", ); expect(String(mock.sent[0])).toBe( JSON.stringify({ type: "session", message: { type: "create_paseo_worktree_request", cwd: "/tmp/project", worktreeSlug: "feature-a", requestId: "req-worktree-legacy", }, }), ); mock.triggerMessage( wrapSessionMessage({ type: "create_paseo_worktree_response", payload: { requestId: "req-worktree-legacy", workspace: null, error: "legacy worktree shape sentinel", setupTerminalId: null, }, }), ); await expect(createPromise).resolves.toEqual({ requestId: "req-worktree-legacy", workspace: null, error: "legacy worktree shape sentinel", setupTerminalId: null, }); }); test("sends explicit shutdown_server_request via shutdownServer", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const lifecycleClient = client as unknown as { shutdownServer: (requestId?: string) => Promise<{ status: "shutdown_requested"; clientId: string; requestId: string; }>; }; expect(typeof lifecycleClient.shutdownServer).toBe("function"); const promise = lifecycleClient.shutdownServer("req-shutdown-1"); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: string; requestId: string; }; }; expect(request.message).toEqual({ type: "shutdown_server_request", requestId: "req-shutdown-1", }); mock.triggerMessage( wrapSessionMessage({ type: "status", payload: { status: "shutdown_requested", clientId: "clsk_unit_test", requestId: "req-shutdown-1", }, }), ); await expect(promise).resolves.toEqual({ status: "shutdown_requested", clientId: "clsk_unit_test", requestId: "req-shutdown-1", }); }); test("restartServer remains restart-only and sends restart_server_request", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.restartServer("settings_update", "req-restart-1"); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: string; reason?: string; requestId: string; }; }; expect(request.message).toEqual({ type: "restart_server_request", reason: "settings_update", requestId: "req-restart-1", }); mock.triggerMessage( wrapSessionMessage({ type: "status", payload: { status: "restart_requested", clientId: "clsk_unit_test", reason: "settings_update", requestId: "req-restart-1", }, }), ); await expect(promise).resolves.toEqual({ status: "restart_requested", clientId: "clsk_unit_test", reason: "settings_update", requestId: "req-restart-1", }); }); test("transitions out of connecting when connect timeout elapses", async () => { vi.useFakeTimers(); try { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, connectTimeoutMs: 100, transportFactory: () => mock.transport, }); clients.push(client); const pendingConnect = client.connect().then( () => ({ ok: true as const }), (error) => ({ ok: false as const, error }), ); expect(client.getConnectionState().status).toBe("connecting"); await vi.advanceTimersByTimeAsync(120); const result = await pendingConnect; expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeInstanceOf(Error); expect((result.error as Error).message).toContain("Connection timed out"); } expect(client.getConnectionState().status).toBe("disconnected"); } finally { vi.useRealTimers(); } }); test("reconnects after relay close with replaced-by-new-connection reason", async () => { vi.useFakeTimers(); try { const logger = createMockLogger(); const first = createMockTransport(); const second = createMockTransport(); const transports = [first, second]; let transportIndex = 0; const client = new DaemonClient({ url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2", clientId: "clsk_test", logger, reconnect: { enabled: true, baseDelayMs: 5, maxDelayMs: 5, }, transportFactory: () => { const next = transports[Math.min(transportIndex, transports.length - 1)]; transportIndex += 1; return next.transport; }, }); clients.push(client); const connectPromise = client.connect(); first.triggerOpen(); await connectPromise; expect(client.getConnectionState().status).toBe("connected"); first.triggerClose({ code: 1008, reason: "Replaced by new connection" }); expect(client.getConnectionState().status).toBe("disconnected"); await vi.advanceTimersByTimeAsync(10); expect(client.getConnectionState().status).toBe("connecting"); second.triggerOpen(); expect(client.getConnectionState().status).toBe("connected"); } finally { vi.useRealTimers(); } }); test("requires non-empty clientId", () => { expect(() => { const _client = new DaemonClient({ url: "ws://relay.test/ws?role=client&serverId=srv_test&v=2", clientId: "", reconnect: { enabled: false }, }); void _client; }).toThrow("Daemon client requires a non-empty clientId"); }); test("requires non-empty clientId for direct connections", () => { expect(() => { const _client = new DaemonClient({ url: "ws://127.0.0.1:6767/ws", clientId: " ", reconnect: { enabled: false }, }); void _client; }).toThrow("Daemon client requires a non-empty clientId"); }); test("logs configured runtime generation in connection transition events", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, runtimeGeneration: 7, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const transitionPayloads = logger.debug.mock.calls .filter(([, message]) => message === "DaemonClientTransition") .map(([payload]) => payload as { generation?: number | null }); expect(transitionPayloads.length).toBeGreaterThan(0); for (const payload of transitionPayloads) { expect(payload.generation).toBe(7); } }); test("subscribes to checkout diff updates via RPC handshake", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.subscribeCheckoutDiff( "/tmp/project", { mode: "uncommitted" }, { subscriptionId: "checkout-sub-1" }, ); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "subscribe_checkout_diff_request"; subscriptionId: string; cwd: string; compare: { mode: "uncommitted" | "base"; baseRef?: string }; requestId: string; }; }; expect(request.message.type).toBe("subscribe_checkout_diff_request"); expect(request.message.subscriptionId).toBe("checkout-sub-1"); expect(request.message.cwd).toBe("/tmp/project"); expect(request.message.compare).toEqual({ mode: "uncommitted" }); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "subscribe_checkout_diff_response", payload: { subscriptionId: "checkout-sub-1", cwd: "/tmp/project", files: [], error: null, requestId: request.message.requestId, }, }, }), ); await expect(promise).resolves.toEqual({ subscriptionId: "checkout-sub-1", cwd: "/tmp/project", files: [], error: null, requestId: request.message.requestId, }); }); test("getCheckoutDiff uses one-shot subscription protocol", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.getCheckoutDiff("/tmp/project", { mode: "base", baseRef: "main" }); expect(mock.sent).toHaveLength(1); const subscribeRequest = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "subscribe_checkout_diff_request"; subscriptionId: string; cwd: string; compare: { mode: "uncommitted" | "base"; baseRef?: string }; requestId: string; }; }; expect(subscribeRequest.message.type).toBe("subscribe_checkout_diff_request"); expect(subscribeRequest.message.cwd).toBe("/tmp/project"); expect(subscribeRequest.message.compare).toEqual({ mode: "base", baseRef: "main", }); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "subscribe_checkout_diff_response", payload: { subscriptionId: subscribeRequest.message.subscriptionId, cwd: "/tmp/project", files: [], error: null, requestId: subscribeRequest.message.requestId, }, }, }), ); await expect(promise).resolves.toEqual({ cwd: "/tmp/project", files: [], error: null, requestId: subscribeRequest.message.requestId, }); expect(mock.sent).toHaveLength(2); const unsubscribeRequest = JSON.parse(mock.sent[1]) as { type: "session"; message: { type: "unsubscribe_checkout_diff_request"; subscriptionId: string; }; }; expect(unsubscribeRequest.message.type).toBe("unsubscribe_checkout_diff_request"); expect(unsubscribeRequest.message.subscriptionId).toBe(subscribeRequest.message.subscriptionId); }); test("requests branch suggestions via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.getBranchSuggestions( { cwd: "/tmp/project", query: "mai", limit: 5 }, "req-branches", ); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "branch_suggestions_request"; cwd: string; query?: string; limit?: number; requestId: string; }; }; expect(request.message.type).toBe("branch_suggestions_request"); expect(request.message.cwd).toBe("/tmp/project"); expect(request.message.query).toBe("mai"); expect(request.message.limit).toBe(5); expect(request.message.requestId).toBe("req-branches"); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "branch_suggestions_response", payload: { branches: ["main"], error: null, requestId: "req-branches", }, }, }), ); await expect(promise).resolves.toEqual({ branches: ["main"], error: null, requestId: "req-branches", }); }); test("reads project config via correlated RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.readProjectConfig("/repo/app", "read-project-config-1"); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "read_project_config_request"; requestId: string; repoRoot: string }; }; expect(request.message).toEqual({ type: "read_project_config_request", requestId: "read-project-config-1", repoRoot: "/repo/app", }); mock.triggerMessage( wrapSessionMessage({ type: "read_project_config_response", payload: { requestId: "read-project-config-1", repoRoot: "/repo/app", ok: true, config: { worktree: { setup: "npm install" } }, revision: { mtimeMs: 10, size: 20 }, }, }), ); await expect(promise).resolves.toEqual({ requestId: "read-project-config-1", repoRoot: "/repo/app", ok: true, config: { worktree: { setup: "npm install" } }, revision: { mtimeMs: 10, size: 20 }, }); }); test("writes project config via correlated RPC and returns inline failures", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.writeProjectConfig({ requestId: "write-project-config-1", repoRoot: "/repo/app", config: { worktree: { setup: ["npm install"] } }, expectedRevision: { mtimeMs: 10, size: 20 }, }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "write_project_config_request"; requestId: string; repoRoot: string; config: unknown; expectedRevision: unknown; }; }; expect(request.message).toEqual({ type: "write_project_config_request", requestId: "write-project-config-1", repoRoot: "/repo/app", config: { worktree: { setup: ["npm install"] } }, expectedRevision: { mtimeMs: 10, size: 20 }, }); mock.triggerMessage( wrapSessionMessage({ type: "write_project_config_response", payload: { requestId: "write-project-config-1", repoRoot: "/repo/app", ok: false, error: { code: "stale_project_config", currentRevision: { mtimeMs: 11, size: 21 }, }, }, }), ); await expect(promise).resolves.toEqual({ requestId: "write-project-config-1", repoRoot: "/repo/app", ok: false, error: { code: "stale_project_config", currentRevision: { mtimeMs: 11, size: 21 }, }, }); }); test("requests directory suggestions via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.getDirectorySuggestions( { query: "proj", limit: 10, cwd: "/tmp/project", includeFiles: true, includeDirectories: true, }, "req-directories", ); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "directory_suggestions_request"; query: string; cwd?: string; includeFiles?: boolean; includeDirectories?: boolean; limit?: number; requestId: string; }; }; expect(request.message.type).toBe("directory_suggestions_request"); expect(request.message.query).toBe("proj"); expect(request.message.cwd).toBe("/tmp/project"); expect(request.message.includeFiles).toBe(true); expect(request.message.includeDirectories).toBe(true); expect(request.message.limit).toBe(10); expect(request.message.requestId).toBe("req-directories"); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "directory_suggestions_response", payload: { directories: ["/Users/test/projects/paseo"], entries: [{ path: "README.md", kind: "file" }], error: null, requestId: "req-directories", }, }, }), ); await expect(promise).resolves.toEqual({ directories: ["/Users/test/projects/paseo"], entries: [{ path: "README.md", kind: "file" }], error: null, requestId: "req-directories", }); }); test("requests checkout merge from base via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.checkoutMergeFromBase( "/tmp/project", { baseRef: "main", requireCleanTarget: true }, "req-merge-from-base", ); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "checkout_merge_from_base_request"; cwd: string; baseRef?: string; requireCleanTarget?: boolean; requestId: string; }; }; expect(request.message.type).toBe("checkout_merge_from_base_request"); expect(request.message.cwd).toBe("/tmp/project"); expect(request.message.baseRef).toBe("main"); expect(request.message.requireCleanTarget).toBe(true); expect(request.message.requestId).toBe("req-merge-from-base"); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "checkout_merge_from_base_response", payload: { cwd: "/tmp/project", requestId: "req-merge-from-base", success: true, error: null, }, }, }), ); await expect(promise).resolves.toEqual({ cwd: "/tmp/project", requestId: "req-merge-from-base", success: true, error: null, }); }); test("requests checkout pull via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.checkoutPull("/tmp/project", "req-pull"); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "checkout_pull_request"; cwd: string; requestId: string; }; }; expect(request.message.type).toBe("checkout_pull_request"); expect(request.message.cwd).toBe("/tmp/project"); expect(request.message.requestId).toBe("req-pull"); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "checkout_pull_response", payload: { cwd: "/tmp/project", requestId: "req-pull", success: true, error: null, }, }, }), ); await expect(promise).resolves.toEqual({ cwd: "/tmp/project", requestId: "req-pull", success: true, error: null, }); }); test("resubscribes checkout diff streams after reconnect", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const internal = client as unknown as { checkoutDiffSubscriptions: Map< string, { cwd: string; compare: { mode: "uncommitted" | "base"; baseRef?: string } } >; }; internal.checkoutDiffSubscriptions.set("checkout-sub-1", { cwd: "/tmp/project", compare: { mode: "base", baseRef: "main" }, }); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "subscribe_checkout_diff_request"; subscriptionId: string; cwd: string; compare: { mode: "uncommitted" | "base"; baseRef?: string }; requestId: string; }; }; expect(request.message.type).toBe("subscribe_checkout_diff_request"); expect(request.message.subscriptionId).toBe("checkout-sub-1"); expect(request.message.cwd).toBe("/tmp/project"); expect(request.message.compare).toEqual({ mode: "base", baseRef: "main" }); expect(typeof request.message.requestId).toBe("string"); expect(request.message.requestId.length).toBeGreaterThan(0); }); test("fetches agents via RPC with filters, sort, and pagination", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.fetchAgents({ filter: { labels: { surface: "workspace" } }, sort: [ { key: "status_priority", direction: "asc" }, { key: "created_at", direction: "desc" }, ], page: { limit: 25, cursor: "cursor-1" }, subscribe: { subscriptionId: "sub-1" }, }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "fetch_agents_request"; requestId: string; filter?: { labels?: Record }; sort?: Array<{ key: "status_priority" | "created_at" | "updated_at" | "title"; direction: "asc" | "desc"; }>; page?: { limit: number; cursor?: string }; subscribe?: { subscriptionId?: string }; }; }; expect(request.message.type).toBe("fetch_agents_request"); expect(request.message.sort).toEqual([ { key: "status_priority", direction: "asc" }, { key: "created_at", direction: "desc" }, ]); expect(request.message.page).toEqual({ limit: 25, cursor: "cursor-1" }); expect(request.message.subscribe).toEqual({ subscriptionId: "sub-1" }); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "fetch_agents_response", payload: { requestId: request.message.requestId, subscriptionId: "sub-1", entries: [], pageInfo: { nextCursor: null, prevCursor: "cursor-1", hasMore: false, }, }, }, }), ); await expect(promise).resolves.toEqual({ requestId: request.message.requestId, subscriptionId: "sub-1", entries: [], pageInfo: { nextCursor: null, prevCursor: "cursor-1", hasMore: false, }, }); }); test("sends active-scoped fetch_agents_request", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.fetchAgents({ scope: "active", page: { limit: 50 }, }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "fetch_agents_request"; requestId: string; scope?: "active"; }; }; expect(request.message).toMatchObject({ type: "fetch_agents_request", scope: "active", }); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "fetch_agents_response", payload: { requestId: request.message.requestId, entries: [], pageInfo: { nextCursor: null, prevCursor: null, hasMore: false, }, }, }, }), ); await expect(promise).resolves.toMatchObject({ requestId: request.message.requestId, entries: [], }); }); test("fetches paginated agent history separately from active agents", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.fetchAgentHistory({ page: { limit: 25, cursor: "cursor-1" }, sort: [{ key: "updated_at", direction: "desc" }], }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "fetch_agent_history_request"; requestId: string; page?: { limit: number; cursor?: string }; }; }; expect(request.message.type).toBe("fetch_agent_history_request"); expect(request.message.page).toEqual({ limit: 25, cursor: "cursor-1" }); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "fetch_agent_history_response", payload: { requestId: request.message.requestId, entries: [], pageInfo: { nextCursor: null, prevCursor: "cursor-1", hasMore: false, }, }, }, }), ); await expect(promise).resolves.toEqual({ requestId: request.message.requestId, entries: [], pageInfo: { nextCursor: null, prevCursor: "cursor-1", hasMore: false, }, }); }); test("uses server-provided dictation finish timeout budget", async () => { vi.useFakeTimers(); const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const finishPromise = client.finishDictationStream("dict-1", 0); const finishError = finishPromise.then( () => null, (error) => error, ); expect(mock.sent).toHaveLength(1); mock.triggerMessage( wrapSessionMessage({ type: "dictation_stream_finish_accepted", payload: { dictationId: "dict-1", timeoutMs: 100, }, }), ); await vi.advanceTimersByTimeAsync(5_101); const error = await finishError; expect(error).toBeInstanceOf(Error); expect((error as Error).message).toContain("Timeout waiting for dictation finalization (5100ms)"); vi.useRealTimers(); }); test("resolves dictation finish when final arrives after finish accepted", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const finishPromise = client.finishDictationStream("dict-2", 1); expect(mock.sent).toHaveLength(1); mock.triggerMessage( wrapSessionMessage({ type: "dictation_stream_finish_accepted", payload: { dictationId: "dict-2", timeoutMs: 1000, }, }), ); mock.triggerMessage( wrapSessionMessage({ type: "dictation_stream_final", payload: { dictationId: "dict-2", text: "hello", }, }), ); await expect(finishPromise).resolves.toEqual({ dictationId: "dict-2", text: "hello", }); }); test("cancels waiters when send fails (no leaked timeouts)", async () => { vi.useFakeTimers(); const logger = createMockLogger(); const mock = createMockTransport(); let sendCount = 0; const transportFactory = () => ({ ...mock.transport, send: () => { sendCount += 1; if (sendCount > 1) { throw new Error("boom"); } }, }); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.getCheckoutStatus("/tmp/project"); await expect(promise).rejects.toThrow("boom"); // Ensure we didn't leave a waiter behind that will reject later. const internal = client as unknown as { waiters: Set }; expect(internal.waiters.size).toBe(0); vi.runOnlyPendingTimers(); vi.useRealTimers(); }); test("lists available providers via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.listAvailableProviders(); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "list_available_providers_request"; requestId: string }; }; expect(request.message.type).toBe("list_available_providers_request"); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "list_available_providers_response", payload: { providers: [ { provider: "claude", available: true, error: null }, { provider: "codex", available: false, error: "Missing binary" }, ], error: null, fetchedAt: "2026-02-12T00:00:00.000Z", requestId: request.message.requestId, }, }, }), ); await expect(promise).resolves.toEqual({ providers: [ { provider: "claude", available: true, error: null }, { provider: "codex", available: false, error: "Missing binary" }, ], error: null, fetchedAt: "2026-02-12T00:00:00.000Z", requestId: request.message.requestId, }); }); test("lists commands with draft config via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.listCommands("__new_agent__", { draftConfig: { provider: "codex", cwd: "/tmp/project", modeId: "bypassPermissions", model: "gpt-5", thinkingOptionId: "off", }, }); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "list_commands_request"; agentId: string; draftConfig?: { provider: string; cwd: string; modeId?: string; model?: string; thinkingOptionId?: string; }; requestId: string; }; }; expect(request.message.type).toBe("list_commands_request"); expect(request.message.agentId).toBe("__new_agent__"); expect(request.message.draftConfig).toEqual({ provider: "codex", cwd: "/tmp/project", modeId: "bypassPermissions", model: "gpt-5", thinkingOptionId: "off", }); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "list_commands_response", payload: { agentId: "__new_agent__", commands: [{ name: "help", description: "Show help", argumentHint: "" }], error: null, requestId: request.message.requestId, }, }, }), ); await expect(promise).resolves.toEqual({ agentId: "__new_agent__", commands: [{ name: "help", description: "Show help", argumentHint: "" }], error: null, requestId: request.message.requestId, }); }); test("lists commands with legacy requestId signature via RPC", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const promise = client.listCommands("agent-1", "req-legacy"); expect(mock.sent).toHaveLength(1); const request = JSON.parse(mock.sent[0]) as { type: "session"; message: { type: "list_commands_request"; agentId: string; draftConfig?: unknown; requestId: string; }; }; expect(request.message.type).toBe("list_commands_request"); expect(request.message.agentId).toBe("agent-1"); expect(request.message.requestId).toBe("req-legacy"); expect(request.message.draftConfig).toBeUndefined(); mock.triggerMessage( JSON.stringify({ type: "session", message: { type: "list_commands_response", payload: { agentId: "agent-1", commands: [], error: null, requestId: "req-legacy", }, }, }), ); await expect(promise).resolves.toEqual({ agentId: "agent-1", commands: [], error: null, requestId: "req-legacy", }); }); test("emits output events for the active terminal stream", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const seen: string[] = []; const unsubscribe = client.onTerminalStreamEvent((event) => { if (event.type !== "output") { return; } seen.push(new TextDecoder().decode(event.data)); }); const subscribePromise = client.subscribeTerminal("term-1", "sub-1"); mock.triggerMessage( wrapSessionMessage({ type: "subscribe_terminal_response", payload: { terminalId: "term-1", slot: 11, error: null, requestId: "sub-1", }, }), ); await subscribePromise; mock.triggerMessage( encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Output, slot: 11, payload: new TextEncoder().encode("hello"), }), ); expect(seen).toEqual(["hello"]); expect(mock.sent).toHaveLength(1); unsubscribe(); }); test("emits snapshot events for the subscribed terminal stream", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const snapshots: unknown[] = []; client.onTerminalStreamEvent((event) => { if (event.type !== "snapshot") { return; } snapshots.push(event.state); }); const subscribePromise = client.subscribeTerminal("term-1", "sub-2"); mock.triggerMessage( wrapSessionMessage({ type: "subscribe_terminal_response", payload: { terminalId: "term-1", slot: 12, error: null, requestId: "sub-2", }, }), ); await subscribePromise; const state = { rows: 1, cols: 5, grid: [[{ char: "h" }, { char: "e" }, { char: "l" }, { char: "l" }, { char: "o" }]], scrollback: [], cursor: { row: 0, col: 5 }, }; mock.triggerMessage( encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Snapshot, slot: 12, payload: encodeTerminalSnapshotPayload(state), }), ); expect(snapshots).toEqual([state]); }); test("sends input and resize frames for the subscribed terminal slot", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const subscribePromise = client.subscribeTerminal("term-1", "sub-3"); mock.triggerMessage( wrapSessionMessage({ type: "subscribe_terminal_response", payload: { terminalId: "term-1", slot: 13, error: null, requestId: "sub-3", }, }), ); await subscribePromise; mock.sent.length = 0; client.sendTerminalInput("term-1", { type: "input", data: "echo hello\r", }); client.sendTerminalInput("term-1", { type: "resize", rows: 24, cols: 80, }); const inputFrame = decodeTerminalStreamFrame(asUint8Array(mock.sent[0])!); const resizeFrame = decodeTerminalStreamFrame(asUint8Array(mock.sent[1])!); expect(inputFrame?.opcode).toBe(TerminalStreamOpcode.Input); expect(inputFrame?.slot).toBe(13); expect(new TextDecoder().decode(inputFrame?.payload ?? new Uint8Array())).toBe("echo hello\r"); expect(resizeFrame?.opcode).toBe(TerminalStreamOpcode.Resize); expect(resizeFrame?.slot).toBe(13); expect(decodeTerminalResizePayload(resizeFrame?.payload ?? new Uint8Array())).toEqual({ rows: 24, cols: 80, }); }); test("routes concurrent terminal stream frames by slot", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const seen: string[] = []; client.onTerminalStreamEvent((event) => { if (event.type !== "output") { return; } seen.push(`${event.terminalId}:${new TextDecoder().decode(event.data)}`); }); const subscribeFirstPromise = client.subscribeTerminal("term-1", "sub-multi-1"); mock.triggerMessage( wrapSessionMessage({ type: "subscribe_terminal_response", payload: { terminalId: "term-1", slot: 21, error: null, requestId: "sub-multi-1", }, }), ); await subscribeFirstPromise; const subscribeSecondPromise = client.subscribeTerminal("term-2", "sub-multi-2"); mock.triggerMessage( wrapSessionMessage({ type: "subscribe_terminal_response", payload: { terminalId: "term-2", slot: 22, error: null, requestId: "sub-multi-2", }, }), ); await subscribeSecondPromise; mock.sent.length = 0; mock.triggerMessage( encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Output, slot: 22, payload: new TextEncoder().encode("beta"), }), ); mock.triggerMessage( encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Output, slot: 21, payload: new TextEncoder().encode("alpha"), }), ); client.sendTerminalInput("term-2", { type: "input", data: "echo beta\r", }); client.sendTerminalInput("term-1", { type: "resize", rows: 10, cols: 20, }); const inputFrame = decodeTerminalStreamFrame(asUint8Array(mock.sent[0])!); const resizeFrame = decodeTerminalStreamFrame(asUint8Array(mock.sent[1])!); expect(seen).toEqual(["term-2:beta", "term-1:alpha"]); expect(inputFrame?.opcode).toBe(TerminalStreamOpcode.Input); expect(inputFrame?.slot).toBe(22); expect(resizeFrame?.opcode).toBe(TerminalStreamOpcode.Resize); expect(resizeFrame?.slot).toBe(21); }); test("ignores terminal stream frames after terminal_stream_exit", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const seen: string[] = []; const unsubscribe = client.onTerminalStreamEvent((event) => { if (event.type !== "output") { return; } seen.push(new TextDecoder().decode(event.data)); }); const subscribePromise = client.subscribeTerminal("term-1", "sub-4"); mock.triggerMessage( wrapSessionMessage({ type: "subscribe_terminal_response", payload: { terminalId: "term-1", slot: 14, error: null, requestId: "sub-4", }, }), ); await subscribePromise; mock.triggerMessage( encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Output, slot: 14, payload: new TextEncoder().encode("before-exit"), }), ); expect(seen).toEqual(["before-exit"]); mock.triggerMessage( wrapSessionMessage({ type: "terminal_stream_exit", payload: { terminalId: "term-1", }, }), ); mock.triggerMessage( encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Output, slot: 14, payload: new TextEncoder().encode("after-exit"), }), ); expect(seen).toEqual(["before-exit"]); unsubscribe(); }); test("parses canonical agent_stream tool_call payloads without crashing", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const received: unknown[] = []; const unsubscribe = client.on("agent_stream", (msg) => { received.push(msg); }); mock.triggerMessage( wrapSessionMessage({ type: "agent_stream", payload: { agentId: "agent_cli", timestamp: "2026-02-08T20:20:00.000Z", event: { type: "timeline", provider: "codex", item: { type: "tool_call", callId: "call_cli_stream", name: "shell", status: "running", detail: { type: "shell", command: "pwd", }, error: null, }, }, }, }), ); unsubscribe(); expect(received).toHaveLength(1); const streamMsg = received[0] as { payload: { event: { type: "timeline"; item: { type: "tool_call"; status: string; error: unknown; detail: { type: string; }; }; }; }; }; expect(streamMsg.payload.event.item.status).toBe("running"); expect(streamMsg.payload.event.item.error).toBeNull(); expect(streamMsg.payload.event.item.detail.type).toBe("shell"); expect(logger.warn).not.toHaveBeenCalled(); }); test("drops legacy agent_stream tool_call payloads and logs validation warning", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const received: unknown[] = []; const unsubscribe = client.on("agent_stream", (msg) => { received.push(msg); }); mock.triggerMessage( wrapSessionMessage({ type: "agent_stream", payload: { agentId: "agent_cli", timestamp: "2026-02-08T20:20:00.000Z", event: { type: "timeline", provider: "codex", item: { type: "tool_call", callId: "call_cli_stream_legacy", name: "shell", status: "inProgress", detail: { type: "unknown", input: { command: "pwd" }, output: null, }, }, }, }, }), ); unsubscribe(); expect(received).toHaveLength(0); expect(logger.warn).toHaveBeenCalled(); }); test("parses canonical fetch_agent_timeline_response payloads without crashing", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const received: unknown[] = []; const unsubscribe = client.on("fetch_agent_timeline_response", (msg) => { received.push(msg); }); mock.triggerMessage( wrapSessionMessage({ type: "fetch_agent_timeline_response", payload: { requestId: "req-1", agentId: "agent_cli", agent: null, direction: "tail", projection: "projected", epoch: "epoch-1", reset: false, staleCursor: false, gap: false, window: { minSeq: 1, maxSeq: 1, nextSeq: 2 }, startCursor: { epoch: "epoch-1", seq: 1 }, endCursor: { epoch: "epoch-1", seq: 1 }, hasOlder: false, hasNewer: false, entries: [ { timestamp: "2026-02-08T20:20:00.000Z", provider: "codex", seqStart: 1, seqEnd: 1, sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }], collapsed: [], item: { type: "tool_call", callId: "call_cli_snapshot", name: "shell", status: "running", detail: { type: "shell", command: "pwd", }, error: null, }, }, ], error: null, }, }), ); unsubscribe(); expect(received).toHaveLength(1); const timelineMsg = received[0] as { payload: { entries: Array<{ item: { type: "tool_call"; status: string; error: unknown; detail: { type: string; }; }; }>; }; }; const firstEntry = timelineMsg.payload.entries[0]; expect(firstEntry?.item.type).toBe("tool_call"); if (firstEntry?.item.type === "tool_call") { expect(firstEntry.item.status).toBe("running"); expect(firstEntry.item.error).toBeNull(); expect(firstEntry.item.detail.type).toBe("shell"); } expect(logger.warn).not.toHaveBeenCalled(); }); test("drops invalid fetch_agent_timeline_response tool_call payloads and logs validation warning", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const received: unknown[] = []; const unsubscribe = client.on("fetch_agent_timeline_response", (msg) => { received.push(msg); }); mock.triggerMessage( wrapSessionMessage({ type: "fetch_agent_timeline_response", payload: { requestId: "req-invalid", agentId: "agent_cli", agent: null, direction: "tail", projection: "projected", epoch: "epoch-1", reset: false, staleCursor: false, gap: false, window: { minSeq: 1, maxSeq: 1, nextSeq: 2 }, startCursor: { epoch: "epoch-1", seq: 1 }, endCursor: { epoch: "epoch-1", seq: 1 }, hasOlder: false, hasNewer: false, entries: [ { timestamp: "2026-02-08T20:20:00.000Z", provider: "codex", seqStart: 1, seqEnd: 1, sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }], collapsed: [], item: { type: "tool_call", callId: "call_cli_invalid", name: "shell", status: "inProgress", detail: { type: "unknown", input: { command: "pwd" }, output: null, }, }, }, ], error: null, }, }), ); unsubscribe(); expect(received).toHaveLength(0); expect(logger.warn).toHaveBeenCalled(); }); test("sends subscribe/unsubscribe terminals messages", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; client.subscribeTerminals({ cwd: "/tmp/project" }); client.unsubscribeTerminals({ cwd: "/tmp/project" }); expect(mock.sent).toHaveLength(2); expect(JSON.parse(String(mock.sent[0]))).toEqual({ type: "session", message: { type: "subscribe_terminals_request", cwd: "/tmp/project", }, }); expect(JSON.parse(String(mock.sent[1]))).toEqual({ type: "session", message: { type: "unsubscribe_terminals_request", cwd: "/tmp/project", }, }); }); test("dispatches terminals_changed events to typed listeners", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const received: Array<{ cwd: string; names: string[] }> = []; const unsubscribe = client.on("terminals_changed", (message) => { received.push({ cwd: message.payload.cwd, names: message.payload.terminals.map((terminal) => terminal.name), }); }); mock.triggerMessage( wrapSessionMessage({ type: "terminals_changed", payload: { cwd: "/tmp/project", terminals: [ { id: "term-1", name: "Dev Server", }, ], }, }), ); unsubscribe(); expect(received).toEqual([ { cwd: "/tmp/project", names: ["Dev Server"], }, ]); }); test("sends close_items_request and resolves close_items_response", async () => { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const responsePromise = client.closeItems( { agentIds: ["agent-1"], terminalIds: ["term-1"], }, "req-close-items", ); expect(JSON.parse(String(mock.sent[0]))).toEqual({ type: "session", message: { type: "close_items_request", agentIds: ["agent-1"], terminalIds: ["term-1"], requestId: "req-close-items", }, }); mock.triggerMessage( wrapSessionMessage({ type: "close_items_response", payload: { agents: [{ agentId: "agent-1", archivedAt: "2026-04-01T00:00:00.000Z" }], terminals: [{ terminalId: "term-1", success: true }], requestId: "req-close-items", }, }), ); await expect(responsePromise).resolves.toEqual({ agents: [{ agentId: "agent-1", archivedAt: "2026-04-01T00:00:00.000Z" }], terminals: [{ terminalId: "term-1", success: true }], requestId: "req-close-items", }); }); test("waitForFinish with timeout=0 omits timeoutMs and has no client deadline", async () => { vi.useFakeTimers(); try { const logger = createMockLogger(); const mock = createMockTransport(); const client = new DaemonClient({ url: "ws://test", clientId: "clsk_unit_test", logger, reconnect: { enabled: false }, transportFactory: () => mock.transport, }); clients.push(client); const connectPromise = client.connect(); mock.triggerOpen(); await connectPromise; const waitPromise = client.waitForFinish("agent-wait-zero-timeout", 0); expect(mock.sent).toHaveLength(1); const request = JSON.parse(String(mock.sent[0])) as { type: "session"; message: { type: "wait_for_finish_request"; requestId: string; agentId: string; timeoutMs?: number; }; }; expect(request.message.type).toBe("wait_for_finish_request"); expect(request.message.agentId).toBe("agent-wait-zero-timeout"); expect(request.message).not.toHaveProperty("timeoutMs"); const settled = vi.fn(); void waitPromise.then( () => settled("resolved"), () => settled("rejected"), ); await vi.advanceTimersByTimeAsync(5 * 60 * 1000); expect(settled).not.toHaveBeenCalled(); mock.triggerMessage( wrapSessionMessage({ type: "wait_for_finish_response", payload: { requestId: request.message.requestId, status: "idle", final: null, error: null, lastMessage: null, }, }), ); await expect(waitPromise).resolves.toEqual({ status: "idle", final: null, error: null, lastMessage: null, }); } finally { vi.useRealTimers(); } });