import { CommandId, DEFAULT_SERVER_SETTINGS, type DesktopBridge, EventId, ProjectId, type OrchestrationEvent, type ServerConfig, type ServerProvider, type TerminalEvent, ThreadId, } from "@t3tools/contracts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ContextMenuItem } from "@t3tools/contracts"; const showContextMenuFallbackMock = vi.fn< ( items: readonly ContextMenuItem[], position?: { x: number; y: number }, ) => Promise >(); function registerListener(listeners: Set<(event: T) => void>, listener: (event: T) => void) { listeners.add(listener); return () => { listeners.delete(listener); }; } const terminalEventListeners = new Set<(event: TerminalEvent) => void>(); const orchestrationEventListeners = new Set<(event: OrchestrationEvent) => void>(); const rpcClientMock = { dispose: vi.fn(), terminal: { open: vi.fn(), write: vi.fn(), resize: vi.fn(), clear: vi.fn(), restart: vi.fn(), close: vi.fn(), onEvent: vi.fn((listener: (event: TerminalEvent) => void) => registerListener(terminalEventListeners, listener), ), }, projects: { searchEntries: vi.fn(), writeFile: vi.fn(), }, shell: { openInEditor: vi.fn(), }, git: { pull: vi.fn(), status: vi.fn(), runStackedAction: vi.fn(), listBranches: vi.fn(), createWorktree: vi.fn(), removeWorktree: vi.fn(), createBranch: vi.fn(), checkout: vi.fn(), init: vi.fn(), resolvePullRequest: vi.fn(), preparePullRequestThread: vi.fn(), }, server: { getConfig: vi.fn(), refreshProviders: vi.fn(), upsertKeybinding: vi.fn(), getSettings: vi.fn(), updateSettings: vi.fn(), subscribeConfig: vi.fn(), subscribeLifecycle: vi.fn(), }, orchestration: { getSnapshot: vi.fn(), dispatchCommand: vi.fn(), getTurnDiff: vi.fn(), getFullThreadDiff: vi.fn(), replayEvents: vi.fn(), onDomainEvent: vi.fn((listener: (event: OrchestrationEvent) => void) => registerListener(orchestrationEventListeners, listener), ), }, }; vi.mock("./wsRpcClient", () => { return { getWsRpcClient: () => rpcClientMock, __resetWsRpcClientForTests: vi.fn(), }; }); vi.mock("./contextMenuFallback", () => ({ showContextMenuFallback: showContextMenuFallbackMock, })); function emitEvent(listeners: Set<(event: T) => void>, event: T) { for (const listener of listeners) { listener(event); } } function getWindowForTest(): Window & typeof globalThis & { desktopBridge?: unknown } { const testGlobal = globalThis as typeof globalThis & { window?: Window & typeof globalThis & { desktopBridge?: unknown }; }; if (!testGlobal.window) { testGlobal.window = {} as Window & typeof globalThis & { desktopBridge?: unknown }; } return testGlobal.window; } function makeDesktopBridge(overrides: Partial = {}): DesktopBridge { return { getWsUrl: () => null, pickFolder: async () => null, confirm: async () => true, setTheme: async () => undefined, showContextMenu: async () => null, openExternal: async () => true, onMenuAction: () => () => undefined, getUpdateState: async () => { throw new Error("getUpdateState not implemented in test"); }, checkForUpdate: async () => { throw new Error("checkForUpdate not implemented in test"); }, downloadUpdate: async () => { throw new Error("downloadUpdate not implemented in test"); }, installUpdate: async () => { throw new Error("installUpdate not implemented in test"); }, onUpdateState: () => () => undefined, ...overrides, }; } const defaultProviders: ReadonlyArray = [ { provider: "codex", enabled: true, installed: true, version: "0.116.0", status: "ready", auth: { status: "authenticated" }, checkedAt: "2026-01-01T00:00:00.000Z", models: [], }, ]; const baseServerConfig: ServerConfig = { cwd: "/tmp/workspace", keybindingsConfigPath: "/tmp/workspace/.config/keybindings.json", keybindings: [], issues: [], providers: defaultProviders, availableEditors: ["cursor"], settings: DEFAULT_SERVER_SETTINGS, }; beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); showContextMenuFallbackMock.mockReset(); terminalEventListeners.clear(); orchestrationEventListeners.clear(); Reflect.deleteProperty(getWindowForTest(), "desktopBridge"); }); afterEach(() => { vi.restoreAllMocks(); }); describe("wsNativeApi", () => { it("forwards server config fetches directly to the RPC client", async () => { rpcClientMock.server.getConfig.mockResolvedValue(baseServerConfig); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); await expect(api.server.getConfig()).resolves.toEqual(baseServerConfig); expect(rpcClientMock.server.getConfig).toHaveBeenCalledWith(); expect(rpcClientMock.server.subscribeConfig).not.toHaveBeenCalled(); expect(rpcClientMock.server.subscribeLifecycle).not.toHaveBeenCalled(); }); it("forwards terminal and orchestration stream events", async () => { const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); const onTerminalEvent = vi.fn(); const onDomainEvent = vi.fn(); api.terminal.onEvent(onTerminalEvent); api.orchestration.onDomainEvent(onDomainEvent); const terminalEvent = { threadId: "thread-1", terminalId: "terminal-1", createdAt: "2026-02-24T00:00:00.000Z", type: "output", data: "hello", } as const; emitEvent(terminalEventListeners, terminalEvent); const orchestrationEvent = { sequence: 1, eventId: EventId.makeUnsafe("event-1"), aggregateKind: "project", aggregateId: ProjectId.makeUnsafe("project-1"), occurredAt: "2026-02-24T00:00:00.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, type: "project.created", payload: { projectId: ProjectId.makeUnsafe("project-1"), title: "Project", workspaceRoot: "/tmp/workspace", defaultModelSelection: { provider: "codex", model: "gpt-5-codex", }, scripts: [], createdAt: "2026-02-24T00:00:00.000Z", updatedAt: "2026-02-24T00:00:00.000Z", }, } satisfies Extract; emitEvent(orchestrationEventListeners, orchestrationEvent); expect(onTerminalEvent).toHaveBeenCalledWith(terminalEvent); expect(onDomainEvent).toHaveBeenCalledWith(orchestrationEvent); }); it("sends orchestration dispatch commands as the direct RPC payload", async () => { rpcClientMock.orchestration.dispatchCommand.mockResolvedValue({ sequence: 1 }); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); const command = { type: "project.create", commandId: CommandId.makeUnsafe("cmd-1"), projectId: ProjectId.makeUnsafe("project-1"), title: "Project", workspaceRoot: "/tmp/project", defaultModelSelection: { provider: "codex", model: "gpt-5-codex", }, createdAt: "2026-02-24T00:00:00.000Z", } as const; await api.orchestration.dispatchCommand(command); expect(rpcClientMock.orchestration.dispatchCommand).toHaveBeenCalledWith(command); }); it("forwards workspace file writes to the project RPC", async () => { rpcClientMock.projects.writeFile.mockResolvedValue({ relativePath: "plan.md" }); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); await api.projects.writeFile({ cwd: "/tmp/project", relativePath: "plan.md", contents: "# Plan\n", }); expect(rpcClientMock.projects.writeFile).toHaveBeenCalledWith({ cwd: "/tmp/project", relativePath: "plan.md", contents: "# Plan\n", }); }); it("forwards full-thread diff requests to the orchestration RPC", async () => { rpcClientMock.orchestration.getFullThreadDiff.mockResolvedValue({ diff: "patch" }); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); await api.orchestration.getFullThreadDiff({ threadId: ThreadId.makeUnsafe("thread-1"), toTurnCount: 1, }); expect(rpcClientMock.orchestration.getFullThreadDiff).toHaveBeenCalledWith({ threadId: "thread-1", toTurnCount: 1, }); }); it("forwards provider refreshes directly to the RPC client", async () => { const nextProviders: ReadonlyArray = [ { ...defaultProviders[0]!, checkedAt: "2026-01-03T00:00:00.000Z", }, ]; rpcClientMock.server.refreshProviders.mockResolvedValue({ providers: nextProviders }); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); await expect(api.server.refreshProviders()).resolves.toEqual({ providers: nextProviders }); expect(rpcClientMock.server.refreshProviders).toHaveBeenCalledWith(); }); it("forwards server settings updates directly to the RPC client", async () => { const nextSettings = { ...DEFAULT_SERVER_SETTINGS, enableAssistantStreaming: true, }; rpcClientMock.server.updateSettings.mockResolvedValue(nextSettings); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); await expect(api.server.updateSettings({ enableAssistantStreaming: true })).resolves.toEqual( nextSettings, ); expect(rpcClientMock.server.updateSettings).toHaveBeenCalledWith({ enableAssistantStreaming: true, }); }); it("forwards context menu metadata to the desktop bridge", async () => { const showContextMenu = vi.fn().mockResolvedValue("delete"); getWindowForTest().desktopBridge = makeDesktopBridge({ showContextMenu }); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); const items = [{ id: "delete", label: "Delete" }] as const; await expect(api.contextMenu.show(items)).resolves.toBe("delete"); expect(showContextMenu).toHaveBeenCalledWith(items, undefined); }); it("falls back to the browser context menu helper when the desktop bridge is missing", async () => { showContextMenuFallbackMock.mockResolvedValue("rename"); const { createWsNativeApi } = await import("./wsNativeApi"); const api = createWsNativeApi(); const items = [{ id: "rename", label: "Rename" }] as const; await expect(api.contextMenu.show(items, { x: 4, y: 5 })).resolves.toBe("rename"); expect(showContextMenuFallbackMock).toHaveBeenCalledWith(items, { x: 4, y: 5 }); }); });