import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import type { DesktopApi } from '../shared/contracts' import { ipcChannels } from '../shared/ipc-channels' const electron = vi.hoisted(() => ({ contextBridge: { exposeInMainWorld: vi.fn() }, ipcRenderer: { invoke: vi.fn(), on: vi.fn(), removeListener: vi.fn() }, webUtils: {} })) vi.mock('electron', () => electron) describe('sandboxed preload', () => { it('forwards status deltas without the Electron event and removes the exact subscription', async () => { await import('./index') const api = electron.contextBridge.exposeInMainWorld.mock.calls[0]![1] as DesktopApi const listener = vi.fn() const unsubscribe = api.channels!.onStatusChanged(listener) const [channel, handler] = electron.ipcRenderer.on.mock.calls.at(-1)! expect(channel).toBe(ipcChannels.channelStatusChanged) const change = { channel: 'telegram', status: { state: 'error', lastError: 'Network unavailable' } } handler({ sender: 'main' }, change) expect(listener).toHaveBeenCalledExactlyOnceWith(change) unsubscribe() expect(electron.ipcRenderer.removeListener).toHaveBeenCalledExactlyOnceWith(channel, handler) }) it('forwards Telegram settings and draft or saved connection tests through channel IPC', async () => { await import('./index') const api = electron.contextBridge.exposeInMainWorld.mock.calls[0]![1] as DesktopApi const settings = { enabled: false, secret: { action: 'replace' as const, value: '456:test-token' }, allowedSenderIds: ['123'], allowGroupMessages: false as const } await api.channels!.getSnapshot() await api.channels!.apply({ telegram: settings }) await api.channels!.testConnection('telegram', settings) await api.channels!.testConnection('telegram') expect(electron.ipcRenderer.invoke.mock.calls).toEqual([ [ipcChannels.channelSettingsGet], [ipcChannels.channelSettingsApply, { telegram: settings }], [ipcChannels.channelSettingsTest, { channel: 'telegram', settings }], [ipcChannels.channelSettingsTest, { channel: 'telegram', settings: undefined }] ]) }) it('does not import Node built-ins unavailable in Electron sandbox', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).not.toMatch(/\bfrom\s+['"]node:/u) expect(source).not.toMatch(/\brequire\(\s*['"]node:/u) }) it('does not load runtime schema libraries in the Electron sandbox', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).not.toMatch(/\b\w+Schema\b/u) expect(source).not.toMatch(/\bfrom\s+['"]zod['"]/u) }) it('exposes only explicit clipboard text reading and writing', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) const clipboard = source.match( /clipboard: \{(?[\s\S]*?)\r?\n {2}\},\r?\n {2}agent:/u )?.groups?.body ?? '' expect(clipboard).toContain('readText: ()') expect(clipboard).toContain('ipcChannels.clipboardReadText') expect(clipboard).toContain('writeText: async (text: string)') expect(clipboard).toContain('ipcChannels.clipboardWriteText') expect(clipboard).not.toMatch(/\b(?:image|html|rtf|bookmark)\b/iu) }) it('exposes only narrow browser-control methods', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) const browser = source.match( /browser: \{(?[\s\S]*?)\r?\n {2}\},\r?\n {2}terminal:/u )?.groups?.body ?? '' for (const method of [ 'createTab:', 'listTabs:', 'closeTab:', 'navigate:', 'back:', 'reload:', 'stopLoading:', 'snapshot:', 'click:', 'type:', 'select:', 'screenshot:', 'setViewport:', 'stop:', 'onState:' ]) { expect(browser).toContain(method) } for (const channel of [ 'browserCreateTab', 'browserListTabs', 'browserCloseTab', 'browserSnapshot', 'browserClick', 'browserType', 'browserSelect', 'browserScreenshot', 'browserSetViewport' ]) { expect(browser).toContain(`ipcChannels.${channel}`) } expect(browser).toContain('requestOrConversation: BrowserNavigateRequest') expect(browser).toContain('setViewport: async (request: BrowserSetViewportRequest)') expect(browser).not.toContain('leaseId') expect(browser).not.toMatch( /\b(?:webContents|debugger|session|partition|cookie)\b/iu ) }) it('exposes bounded local tool environment operations without execution inputs', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) const localTools = source.match( /localToolEnvironment: \{(?[\s\S]*?)\r?\n {2}\},\r?\n {2}feedback:/u )?.groups?.body ?? '' for (const method of [ 'getSnapshot:', 'updateSettings:', 'refreshCandidates:', 'selectExecutable:', 'diagnose:', 'installPython:', 'cancelPython:', 'removePython:', 'onProgress:' ]) { expect(localTools).toContain(method) } expect(localTools).not.toMatch( /\b(?:command|env|url|hash|installDirectory|shim)\b/iu ) expect(localTools).toContain('ipcRenderer.removeListener(') }) it('exposes only explicit computer capability and managed profile IPC methods', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('setComputerCapabilityEnabled:') expect(source).toContain('diagnoseComputerCapability:') expect(source).toContain('createBrowserProfile:') expect(source).toContain('renameBrowserProfile:') expect(source).toContain('setDefaultBrowserProfile:') expect(source).toContain('removeBrowserProfile:') expect(source).not.toMatch( /(?:setComputerCapability|BrowserProfile).{0,80}(?:executablePath|command|env|args)/su ) }) it('exposes model ZIP dialogs without renderer-controlled paths', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('importArchive: (modelId: string)') expect(source).toContain('exportArchive: (modelId: string)') expect(source).toContain('importOcrModelArchive: (modelId: string)') expect(source).toContain('exportOcrModelArchive: (modelId: string)') expect(source).toContain('importModelArchive: (modelId: string)') expect(source).toContain('ipcChannels.embeddingModelsImportArchive') expect(source).not.toContain('importLocalDirectory:') expect(source).not.toContain('importOcrModel:') expect(source).not.toMatch( /importModelArchive:\s*\([^)]*(?:path|directory|archivePath)/u ) }) it('exposes explicit embedding settings operations without vectors or credentials', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) const embeddings = source.match( /embeddings: \{(?[\s\S]*?)\r?\n {2}\},\r?\n {2}documentParsing:/u )?.groups?.body ?? '' expect(embeddings).toContain('getSnapshot:') expect(embeddings).toContain('getModelProgress:') expect(embeddings).toContain('diagnose: (connectionId: string)') expect(embeddings).toContain('setCurrent: (connectionId: string)') expect(embeddings).toContain('installModel:') expect(embeddings).toContain('cancelModelOperation:') expect(embeddings).toContain('removeModel:') expect(embeddings).not.toMatch(/\b(?:apiKey|credential|vector)\b/iu) }) it('passes an explicit parsing scenario to document diagnostics', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain( 'test: (purpose: DocumentParsingTestPurpose, operationId?: string)' ) expect(source).toMatch( /ipcChannels\.documentParsingTest,\r?\n {8}\{ purpose, \.\.\.\(operationId/u ) }) it('exposes a narrow OCR operation progress snapshot', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('getOcrModelProgress:') expect(source).toContain('ipcChannels.documentOcrModelsProgress') }) it('exposes a removable attachment parsing progress listener', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('onFileSelectionProgress:') expect(source).toContain('contextFileSelectionProgress') expect(source).toContain('ipcRenderer.removeListener(') }) it('exposes only bounded release-note actions', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('releaseNotes: {') expect(source).toContain('getPending:') expect(source).toContain('acknowledge: async (version: string)') expect(source).toContain('ipcChannels.releaseNotesGetPending') expect(source).toContain('ipcChannels.releaseNotesAcknowledge') }) it('exposes only the narrow feedback submission method', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) const feedback = source.match( /feedback: \{(?[\s\S]*?)\r?\n {2}\},\r?\n {2}shortcuts:/u )?.groups?.body ?? '' expect(feedback).toContain('submit: (input: FeedbackSubmitInput)') expect(feedback).toContain('ipcChannels.feedbackSubmit') expect(feedback).not.toMatch( /\b(?:url|endpoint|header|cookie|authorization|path)\b/iu ) }) it('exposes only get and validated-update shortcut settings methods', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('shortcuts: {') expect(source).toContain('ipcChannels.shortcutSettingsGet') expect(source).toContain('ipcChannels.shortcutSettingsUpdate') expect(source).not.toContain('globalShortcut.') }) it('exposes only explicit SSH host-management methods', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('sshHosts: {') expect(source).toContain('getSnapshot:') expect(source).toContain('onAgentConnectionStatus:') expect(source).toContain( 'ipcChannels.sshHostsAgentConnectionStatus' ) expect(source).toContain( 'getAgentPackageInventory: (refresh = false, localOnly = false)' ) expect(source).toContain('downloadAgentPackage:') expect(source).toContain('importAgentPackage:') expect(source).toContain('exportAgentPackage: async') expect(source).toContain('onAgentPackageProgress:') expect(source).toContain('inspectDraftHostKey:') expect(source).toContain('discardCandidate:') expect(source).toContain('validateAndSave:') expect(source).toContain( 'browseDirectories: (hostId: string, path?: string)' ) expect(source).toContain('cancelDirectoryBrowse: async ()') expect(source).toContain('getRemoteEnvironment: (hostId: string)') expect(source).toContain( 'input: RemoteEnvironmentUpdateRequest' ) expect(source).toContain( 'cancelRemoteEnvironmentUpdate: async (hostId: string)' ) expect(source).toContain( 'onRemoteEnvironmentUpdateProgress:' ) expect(source).toContain( 'ipcChannels.sshHostsRemoteEnvironment' ) expect(source).toContain( 'ipcChannels.sshHostsAgentPackageInventory' ) expect(source).toContain( 'ipcChannels.sshHostsAgentPackageDownload' ) expect(source).toContain( 'ipcChannels.sshHostsAgentPackageImport' ) expect(source).toContain( 'ipcChannels.sshHostsAgentPackageExport' ) expect(source).toContain( 'ipcChannels.sshHostsAgentPackageProgress' ) expect(source).toContain( 'ipcChannels.sshHostsUpdateRemoteEnvironment' ) expect(source).toContain( 'ipcChannels.sshHostsCancelRemoteEnvironmentUpdate' ) expect(source).toContain( 'ipcChannels.sshHostsRemoteEnvironmentUpdateProgress' ) expect(source).toContain('ipcChannels.sshHostsBrowseDirectories') expect(source).toContain( 'ipcChannels.sshHostsCancelDirectoryBrowse' ) expect(source).not.toContain('sshHostsCreate') expect(source).not.toContain('sshHostsAcceptKey') expect(source).not.toContain('privateKey:') expect(source).not.toContain('agentForward: true') const sshHostsSource = source.match( /sshHosts: \{(?[\s\S]*?)\n {2}\},\n {2}channels:/u ) ?.groups?.body ?? '' expect(sshHostsSource).not.toMatch( /\b(?:credential|password|revision|sftp|shell|command)\b/iu ) expect(sshHostsSource).not.toMatch( /\b(?:detail|phaseName|installationId)\b/iu ) }) it('exposes only bounded terminal session operations', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) const terminal = source.match( /terminal: \{(?[\s\S]*?)\r?\n {2}\},\r?\n {2}settings:/u )?.groups?.body ?? '' expect(terminal).toContain('create:') expect(terminal).toContain('write:') expect(terminal).toContain('resize:') expect(terminal).toContain('close:') expect(terminal).toContain('getSnapshot:') expect(terminal).toContain('ack:') expect(terminal).toContain('onEvent:') expect(terminal).toContain('ipcRenderer.removeListener(') expect(terminal).not.toMatch( /\b(?:shell|cwd|command|environment|password|privateKey|pid)\b/u ) }) it('exposes narrow awaited remote project activation and save operations', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('remote: {') expect(source).toContain('remoteProjectSave') expect(source).toContain('remoteProjectCancelCurrent') expect(source).toContain('remoteProjectSaveProgress') expect(source).toContain('RemoteProjectSaveRequest') expect(source).not.toContain('commitToken') expect(source).toContain('ipcRenderer.removeListener(') }) it('exposes removable project recovery snapshot, retry, and progress APIs', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('getRecoverySnapshot:') expect(source).toContain('retryRecovery: (projectId: string)') expect(source).toContain('onRecoveryProgress:') expect(source).toContain('ipcChannels.remoteProjectRecoveryGet') expect(source).toContain('ipcChannels.remoteProjectRecoveryRetry') expect(source).toContain( 'ipcChannels.remoteProjectRecoveryProgress' ) expect(source).toMatch( /remoteProjectRecoveryProgress,[\s\S]*?removeListener\([\s\S]*?remoteProjectRecoveryProgress/u ) }) it('exposes bounded knowledge task actions', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('cancelTask: (taskId: string)') expect(source).toContain('retryTask: async (taskId: string)') expect(source).toContain('ipcChannels.knowledgeTaskCancel') expect(source).toContain('ipcChannels.knowledgeTaskRetry') }) it('exposes only explicit Conversation queue operations and listeners', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain('conversationQueue: {') expect(source).toContain('ipcChannels.conversationQueueEnqueueUser') expect(source).toContain( 'ipcChannels.conversationQueueInterruptAndRun' ) expect(source).toContain('ipcChannels.conversationQueueDispatch') expect(source).toContain('listener(conversationId)') }) it('exposes a narrow local conversation branch operation', () => { const source = readFileSync( join(process.cwd(), 'src', 'preload', 'index.ts'), 'utf8' ) expect(source).toContain( 'branchLocal: (input: ConversationBranchInput)' ) expect(source).toContain('ipcChannels.conversationsBranchLocal') }) })