diff --git a/mobile/src/terminal/mobile-terminal-query-reply.test.ts b/mobile/src/terminal/mobile-terminal-query-reply.test.ts index 1b76a89574d..eb7342addd6 100644 --- a/mobile/src/terminal/mobile-terminal-query-reply.test.ts +++ b/mobile/src/terminal/mobile-terminal-query-reply.test.ts @@ -1,16 +1,25 @@ import { describe, expect, it, vi } from 'vitest' import type { RpcClient } from '../transport/rpc-client' +import type { RpcResponse } from '../transport/types' import { sendMobileTerminalQueryReply } from './mobile-terminal-query-reply' function createClient() { return { - sendRequest: vi.fn().mockResolvedValue({ + sendRequest: vi.fn().mockResolvedValue({ id: '1', ok: true, result: { send: { accepted: true, bytesWritten: 6 } }, _meta: { runtimeId: 'runtime-1' } - }) - } as unknown as Pick + }), + subscribe: () => () => {}, + updateTerminalSubscriptionViewport: () => {}, + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => null, + onStateChange: () => () => {}, + notifyForeground: () => {}, + close: () => {} + } satisfies RpcClient } describe('sendMobileTerminalQueryReply', () => { @@ -36,6 +45,25 @@ describe('sendMobileTerminalQueryReply', () => { inputKind: 'query-reply', client: { id: 'mobile-1', type: 'mobile' } }) + + const refused: RpcResponse = { + id: '2', + ok: true, + result: { send: { handle: 'terminal-1', accepted: false, bytesWritten: 0 } } + } + client.sendRequest.mockResolvedValueOnce(refused) + await expect( + sendMobileTerminalQueryReply({ + bytes: '\x1b[3;4R', + client, + clientId: 'mobile-1', + connected: true, + handle: 'terminal-1', + hostSupportsQueryReplyInput: true, + subscribedTerminals: new Set(['terminal-1']) + }) + ).resolves.toBe(false) + expect(client.sendRequest).toHaveBeenCalledTimes(2) }) it.each([ diff --git a/mobile/src/terminal/mock-server-terminal-stream-cancellation.test.ts b/mobile/src/terminal/mock-server-terminal-stream-cancellation.test.ts index 66e4900fe7c..26a06c9411d 100644 --- a/mobile/src/terminal/mock-server-terminal-stream-cancellation.test.ts +++ b/mobile/src/terminal/mock-server-terminal-stream-cancellation.test.ts @@ -50,6 +50,7 @@ describe('mock terminal stream cancellation', () => { expect(handle(request('new', 'terminal.subscribe'))).toBe(true) const oldResponses = deferred.filter(({ response }) => response.id === 'old') const newResponses = deferred.filter(({ response }) => response.id === 'new') + expect(newResponses).toHaveLength(1) expect(oldResponses.every(({ shouldSend }) => shouldSend?.() === false)).toBe(true) expect(newResponses.every(({ shouldSend }) => shouldSend?.() === true)).toBe(true) diff --git a/mobile/src/terminal/quick-commands.test.ts b/mobile/src/terminal/quick-commands.test.ts index 584dd8f4812..1333d422760 100644 --- a/mobile/src/terminal/quick-commands.test.ts +++ b/mobile/src/terminal/quick-commands.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' +import type { + TerminalCommandQuickCommand, + TerminalAgentQuickCommand +} from '../../../src/shared/terminal-quick-command-types' import { buildMobileQuickCommandLaunch, getQuickCommandDisplayPreview, @@ -7,7 +10,9 @@ import { supportsMobileQuickCommands } from './quick-commands' -function command(overrides: Partial = {}): TerminalQuickCommand { +function command( + overrides: Partial = {} +): TerminalCommandQuickCommand { return { id: 'command', label: 'Command', @@ -16,7 +21,18 @@ function command(overrides: Partial = {}): TerminalQuickCo appendEnter: true, scope: { type: 'global' }, ...overrides - } as TerminalQuickCommand + } +} + +function agentCommand(prompt: string): TerminalAgentQuickCommand { + return { + id: 'agent-command', + label: 'Command', + action: 'agent-prompt', + agent: 'codex', + prompt, + scope: { type: 'global' } + } } describe('mobile quick-command launch', () => { @@ -59,29 +75,20 @@ describe('mobile quick-command launch', () => { }) it('injects supported agent prompts into the host-built launch command', () => { - expect( - buildMobileQuickCommandLaunch( - command({ - action: 'agent-prompt', - agent: 'codex', - prompt: 'Review this diff' - }) - ) - ).toEqual({ agent: 'codex', options: { agentPrompt: 'Review this diff' } }) + expect(buildMobileQuickCommandLaunch(agentCommand('Review this diff'))).toEqual({ + agent: 'codex', + options: { agentPrompt: 'Review this diff' } + }) }) it('bounds native row text without truncating searchable or executable content', () => { const longPrompt = `Review ${'x'.repeat(5993)}` - const agentCommand = command({ - action: 'agent-prompt', - agent: 'codex', - prompt: longPrompt - }) + const savedCommand = agentCommand(longPrompt) - expect(getQuickCommandDisplayPreview(agentCommand)).toHaveLength(240) - expect(getQuickCommandDisplayPreview(agentCommand)).toMatch(/^Codex: Review .*…$/) - expect(getQuickCommandPreview(agentCommand)).toBe(`Codex: ${longPrompt}`) - expect(buildMobileQuickCommandLaunch(agentCommand)).toEqual({ + expect(getQuickCommandDisplayPreview(savedCommand)).toHaveLength(240) + expect(getQuickCommandDisplayPreview(savedCommand)).toMatch(/^Codex: Review .*…$/) + expect(getQuickCommandPreview(savedCommand)).toBe(`Codex: ${longPrompt}`) + expect(buildMobileQuickCommandLaunch(savedCommand)).toEqual({ agent: 'codex', options: { agentPrompt: longPrompt } }) diff --git a/mobile/src/terminal/terminal-auto-restore-fit-state.test.ts b/mobile/src/terminal/terminal-auto-restore-fit-state.test.ts index aa509b1227b..1b942283184 100644 --- a/mobile/src/terminal/terminal-auto-restore-fit-state.test.ts +++ b/mobile/src/terminal/terminal-auto-restore-fit-state.test.ts @@ -6,15 +6,23 @@ import { } from './terminal-auto-restore-fit-state' describe('terminal auto restore fit state', () => { - it('reads the saved delay out of the host reply envelope, after a read and after a save', async () => { + it('reads host values after a read, a clamped save, and a refused save', async () => { const sendRequest = vi .fn() .mockResolvedValueOnce({ id: '1', ok: true, result: { ms: 300_000 } }) - .mockResolvedValueOnce({ id: '2', ok: true, result: { ms: 60_000 } }) + .mockResolvedValueOnce({ id: '2', ok: true, result: { ms: 5_000 } }) + .mockResolvedValueOnce({ id: '3', ok: false, error: { code: 'denied', message: 'Refused' } }) + .mockResolvedValueOnce({ id: '4', ok: true, result: { ms: 120_000 } }) await expect(readTerminalAutoRestoreFitMs({ sendRequest })).resolves.toBe(300_000) - await expect(saveTerminalAutoRestoreFitMs({ sendRequest }, 60_000)).resolves.toBe(60_000) - expect(sendRequest).toHaveBeenLastCalledWith('terminal.setAutoRestoreFit', { ms: 60_000 }) + await expect(saveTerminalAutoRestoreFitMs({ sendRequest }, 1_000)).resolves.toBe(5_000) + await expect(saveTerminalAutoRestoreFitMs({ sendRequest }, 300_000)).resolves.toBe(120_000) + expect(sendRequest.mock.calls).toEqual([ + ['terminal.getAutoRestoreFit'], + ['terminal.setAutoRestoreFit', { ms: 1_000 }], + ['terminal.setAutoRestoreFit', { ms: 300_000 }], + ['terminal.getAutoRestoreFit'] + ]) }) it('reads a host without the setting as the default', async () => { diff --git a/mobile/src/terminal/terminal-keyboard-dismiss.test.ts b/mobile/src/terminal/terminal-keyboard-dismiss.test.ts index 1e523b5d5c5..156dc8eb9ad 100644 --- a/mobile/src/terminal/terminal-keyboard-dismiss.test.ts +++ b/mobile/src/terminal/terminal-keyboard-dismiss.test.ts @@ -20,21 +20,6 @@ describe('dismissTerminalKeyboard', () => { expect(calls).toEqual(['clear', 'live-blur', 'command-blur', 'dismiss']) }) - it('blurs both live and buffered command inputs', () => { - const liveInput = { blur: vi.fn() } - const commandInput = { blur: vi.fn() } - - dismissTerminalKeyboard({ - clearPendingLiveInputFocus: vi.fn(), - commandInput, - dismissKeyboard: vi.fn(), - liveInput - }) - - expect(liveInput.blur).toHaveBeenCalledTimes(1) - expect(commandInput.blur).toHaveBeenCalledTimes(1) - }) - it('dismisses the keyboard without a live input handle', () => { const commandInput = { blur: vi.fn() } const dismissKeyboard = vi.fn() diff --git a/mobile/src/terminal/terminal-live-accessory-raw-send.test.ts b/mobile/src/terminal/terminal-live-accessory-raw-send.test.ts index 4ecab7dd10c..ed4157b9e1e 100644 --- a/mobile/src/terminal/terminal-live-accessory-raw-send.test.ts +++ b/mobile/src/terminal/terminal-live-accessory-raw-send.test.ts @@ -36,6 +36,12 @@ describe('terminal live accessory raw send', () => { // Why: an accessory key parked in the connect wait would fire into the PTY long after the tap. { failWhenDisconnected: true } ) + expect(sendRequest).toHaveBeenCalledTimes(2) + expect(sendRequest).toHaveBeenLastCalledWith( + 'orchestration.workerTerminalUserInput', + { terminal: 'terminal-a' }, + { timeoutMs: 5_000, budgetSpansConnect: true, failWhenDisconnected: true } + ) }) it('drops the bytes instead of sending while disconnected', async () => { diff --git a/mobile/src/terminal/terminal-live-control-send-order.test.ts b/mobile/src/terminal/terminal-live-control-send-order.test.ts index 8f1797c7ca0..188c7f28986 100644 --- a/mobile/src/terminal/terminal-live-control-send-order.test.ts +++ b/mobile/src/terminal/terminal-live-control-send-order.test.ts @@ -27,26 +27,29 @@ describe('terminal live control send order', () => { }) it('Given a successful pending text flush When control bytes follow Then sends them afterward', async () => { - // Given - const events: string[] = [] - const flushPendingText = vi.fn(async () => { - events.push('flush') - return true - }) - const sendControlBytes = vi.fn(async () => { - events.push('control') - return true - }) + vi.useFakeTimers() + try { + const events: string[] = [] + const pendingFlush = Promise.withResolvers() + const flushPendingText = vi.fn(() => { + events.push('flush') + return pendingFlush.promise + }) + const sendControlBytes = vi.fn(async () => { + events.push('control') + return true + }) - // When - const result = await sendTerminalLiveControlAfterPendingFlush( - flushPendingText, - sendControlBytes - ) - - // Then - expect(result).toBe(true) - expect(events).toEqual(['flush', 'control']) + const result = sendTerminalLiveControlAfterPendingFlush(flushPendingText, sendControlBytes) + await vi.advanceTimersByTimeAsync(0) + expect(events).toEqual(['flush']) + expect(sendControlBytes).not.toHaveBeenCalled() + pendingFlush.resolve(true) + await expect(result).resolves.toBe(true) + expect(events).toEqual(['flush', 'control']) + } finally { + vi.useRealTimers() + } }) it('Given a failed control byte send When pending text flushed Then reports failure', async () => { diff --git a/mobile/src/terminal/terminal-write-coalescer-boundaries.test.ts b/mobile/src/terminal/terminal-write-coalescer-boundaries.test.ts index 3dd290fa1ff..06733f05172 100644 --- a/mobile/src/terminal/terminal-write-coalescer-boundaries.test.ts +++ b/mobile/src/terminal/terminal-write-coalescer-boundaries.test.ts @@ -1,101 +1,121 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import type { TerminalWebViewCommand } from './terminal-webview-messages' -import { createTerminalWebViewPendingMessages } from './terminal-webview-pending-messages' -import { - createTerminalWriteCoalescer, - TERMINAL_WRITE_FLUSH_WINDOW_MS -} from './terminal-write-coalescer' +import { useTerminalWebViewController } from './use-terminal-webview-controller' +import { TERMINAL_WRITE_FLUSH_WINDOW_MS } from './terminal-write-coalescer' -// Simulates TerminalWebView's postMessage: ready → deliver, not ready → queue. -// There is no React render harness in the node environment, so the boundary -// invariants are gated here against the same pending-queue module the component uses. -function createSimulatedTerminalWebView() { - const delivered: TerminalWebViewCommand[] = [] - const pendingMessages = createTerminalWebViewPendingMessages() - const readiness = { webReady: true } - const send = (msg: TerminalWebViewCommand) => { - delivered.push(msg) +vi.mock('react-native', () => ({ + AppState: { currentState: 'active' }, + Pressable: 'Pressable', + Text: 'Text', + View: 'View', + StyleSheet: { absoluteFillObject: {}, create: (styles: unknown) => styles } +})) +vi.mock('lucide-react-native', () => ({ RefreshCw: 'RefreshCw' })) + +let activeRenderer: ReactTestRenderer | undefined + +function createTerminalController() { + const delivered: (TerminalWebViewCommand & { id: number })[] = [] + let controller: ReturnType | undefined + const transport = { + post: (command: TerminalWebViewCommand & { id: number }) => { + delivered.push(command) + }, + pingsOnForegroundRecovery: () => true } - const postMessage = (msg: TerminalWebViewCommand) => { - if (!readiness.webReady) { - pendingMessages.queue(msg) - return + function Harness() { + controller = useTerminalWebViewController({}, transport) + return null + } + act(() => { + activeRenderer = create(createElement(Harness)) + }) + const mountedController = () => controller + const view = mountedController() + if (!view) { + throw new Error('Terminal controller did not mount') + } + act(() => { + view.receive({ type: 'web-ready' }) + }) + delivered.length = 0 + const recover = () => { + const ping = delivered.find((command) => command.type === 'ping') + if (!ping) { + throw new Error('Foreground recovery did not ping the document') } - send(msg) + act(() => { + view.receive({ type: 'pong', pingId: ping.id }) + }) } - const coalescer = createTerminalWriteCoalescer((data) => postMessage({ type: 'write', data })) - return { coalescer, delivered, pendingMessages, postMessage, readiness, send } + const writesAndInits = () => + delivered.filter((command) => command.type === 'write' || command.type === 'init') + return { delivered, view: view.handle, recover, writesAndInits } } describe('terminal write coalescer boundaries', () => { afterEach(() => { + act(() => { + activeRenderer?.unmount() + }) + activeRenderer = undefined vi.useRealTimers() }) it('drops buffered pre-snapshot writes on init (write → clear → init supersession)', () => { vi.useFakeTimers() - const view = createSimulatedTerminalWebView() - - view.coalescer.write('a') - view.coalescer.write('stale-pre-snapshot') - // init() boundary as wired in TerminalWebView: clear the coalescer, then post init. - view.coalescer.clear() - view.postMessage({ type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null }) - vi.runOnlyPendingTimers() - - expect(view.delivered).toEqual([ + const { view, writesAndInits } = createTerminalController() + act(() => { + view.write('a') + view.write('stale-pre-snapshot') + view.init({ cols: 80, rows: 24, initialData: 'snapshot', frame: null }) + vi.runOnlyPendingTimers() + }) + expect(writesAndInits()).toMatchObject([ { type: 'write', data: 'a' }, { type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null } ]) - const initIndex = view.delivered.findIndex((msg) => msg.type === 'init') - expect(initIndex).toBeGreaterThanOrEqual(0) - expect(view.delivered.slice(initIndex + 1)).toEqual([]) }) it('keeps foreground recovery safe: a late timer flush lands before init in FIFO order', () => { vi.useFakeTimers() - const view = createSimulatedTerminalWebView() - - view.coalescer.write('live') - view.coalescer.write('buffered-mid-recovery') - // prepareForForegroundRecovery(): readiness invalidated before the timer fires. - view.readiness.webReady = false - vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS) - - // Recovery re-init: coalescer.clear() cancels any pending timer synchronously, - // so nothing can flush after this point; the queued init supersedes the flush. - view.coalescer.clear() - view.postMessage({ type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null }) - - view.readiness.webReady = true - view.pendingMessages.flush(view.send) - vi.runOnlyPendingTimers() - - expect(view.delivered).toEqual([ + const { view, delivered, recover, writesAndInits } = createTerminalController() + act(() => { + view.write('live') + view.write('buffered-mid-recovery') + view.prepareForForegroundRecovery() + vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS) + }) + expect(delivered.map((command) => command.type)).toEqual(['write', 'ping']) + act(() => { + view.init({ cols: 80, rows: 24, initialData: 'snapshot', frame: null }) + }) + expect(delivered.map((command) => command.type)).toEqual(['write', 'ping']) + recover() + act(() => { + vi.runOnlyPendingTimers() + }) + expect(writesAndInits()).toMatchObject([ { type: 'write', data: 'live' }, { type: 'write', data: 'buffered-mid-recovery' }, { type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null } ]) - // Invariant: no write reaches the document after the recovery init. - const initIndex = view.delivered.findIndex((msg) => msg.type === 'init') - expect(view.delivered.slice(initIndex + 1).filter((msg) => msg.type === 'write')).toEqual([]) }) it('delivers the deferred-recovery timer flush when no init follows (skipped recovery)', () => { vi.useFakeTimers() - const view = createSimulatedTerminalWebView() - - view.coalescer.write('live') - view.coalescer.write('still-current-document') - view.readiness.webReady = false - vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS) - - // 'skipped' recovery: pong re-confirms readiness, no init is posted — the - // buffered bytes belong to the still-live document and must not be lost. - view.readiness.webReady = true - view.pendingMessages.flush(view.send) - - expect(view.delivered).toEqual([ + const { view, delivered, recover, writesAndInits } = createTerminalController() + act(() => { + view.write('live') + view.write('still-current-document') + view.prepareForForegroundRecovery() + vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS) + }) + expect(delivered.map((command) => command.type)).toEqual(['write', 'ping']) + recover() + expect(writesAndInits()).toMatchObject([ { type: 'write', data: 'live' }, { type: 'write', data: 'still-current-document' } ]) diff --git a/mobile/src/terminal/worker-terminal-takeover-report.test.ts b/mobile/src/terminal/worker-terminal-takeover-report.test.ts index 5ef62be1f0b..81e60b11ff8 100644 --- a/mobile/src/terminal/worker-terminal-takeover-report.test.ts +++ b/mobile/src/terminal/worker-terminal-takeover-report.test.ts @@ -50,7 +50,11 @@ it.each(['throw', 'rpc refusal'])( sendRequest: failure === 'throw' ? vi.fn().mockRejectedValue(new Error('offline')) - : vi.fn().mockResolvedValue({ id: 'report', ok: false, error: { message: 'refused' } }) + : vi.fn().mockResolvedValue({ + id: 'report', + ok: false, + error: { code: 'permission_denied', message: 'refused' } + }) } reportWorkerTerminalUserInput(client, 'term-1') await vi.advanceTimersByTimeAsync(249) diff --git a/mobile/tests-typecheck-baseline.txt b/mobile/tests-typecheck-baseline.txt index 94393668192..44256a34643 100644 --- a/mobile/tests-typecheck-baseline.txt +++ b/mobile/tests-typecheck-baseline.txt @@ -71,7 +71,6 @@ src/source-control/use-mobile-hosted-review-eligibility.test.ts src/tasks/source-workspace-create.test.ts src/tasks/task-source-search-reply-schema.test.ts src/terminal/buffered-terminal-draft-restoration.test.ts -src/terminal/mobile-terminal-query-reply.test.ts src/terminal/terminal-caret-rendering-oracle.test.ts src/terminal/terminal-live-accessory-raw-send.test.ts src/terminal/terminal-live-text-commit.test.ts diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts index edd4334cc85..6bfd71730ec 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts @@ -26,7 +26,7 @@ beforeEach(() => { afterEach(() => rmSync(root, { recursive: true, force: true })) describe('orcad migration blob staging', () => { - it('stages, resumes from disk after a restart, and accepts only exact retries', () => { + it('stages, reloads progress from disk, and accepts only exact retries', () => { const bytes = Buffer.from('echo one\necho two 🐋\n', 'utf8') const descriptor = blob('shell-history', `${'a'.repeat(16)}-zsh`, bytes) const manifest = migrationManifest('resume') diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts index c21d7d85739..5caa5421255 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts @@ -108,11 +108,10 @@ describe('orcad scrollback snapshot transfer', () => { expect(readFinal(descriptor)).toBeNull() }) - it('recognizes an already-materialized matching final file after restart', () => { + it('recognizes an already-materialized matching final file without staged bytes', () => { const bytes = Buffer.from('already committed bytes', 'utf8') const descriptor = snapshot('3', 'tab-3', 'leaf-3', bytes) const manifest = migrationManifest('materialized', [descriptor]) - stagedState(manifest) writeFinal(descriptor, bytes) expect(inspectOrcadMigrationSnapshotUploads(manifest, storage)?.[0]?.receivedBytes).toBe( diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts index f980a305217..e70fba4e265 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts @@ -34,7 +34,7 @@ function manifest(): OrcadMigrationManifest { } describe('retiring client-hosted browser close intents', () => { - it('finishes a retry after the moving write already flushed', () => { + it('moves close intents to the destination and accepts an exact retry', () => { const state = getDefaultPersistedState('/home/test') state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment = { old: [ @@ -43,9 +43,13 @@ describe('retiring client-hosted browser close intents', () => { ] } subtractOrcadMigrationClientState(state, manifest()) - const moved = structuredClone( - state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment - ) + const moved = { + 'environment-1': [ + { browserPageId: 'page-1', worktreeId, closedAt: 42 }, + { browserPageId: 'page-2', worktreeId, closedAt: 43 } + ] + } + expect(state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment).toEqual(moved) expect(() => subtractOrcadMigrationClientState(state, manifest())).not.toThrow() expect(state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment).toEqual(moved) }) diff --git a/src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts b/src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts index 0ae8ab32e9f..d936a8036ee 100644 --- a/src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts +++ b/src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts @@ -31,7 +31,7 @@ describe('bindTabStripContentResizeObservers', () => { expect(onResize).toHaveBeenCalled() disconnectObservers() - expect(disconnect).toHaveBeenCalled() + expect(disconnect).toHaveBeenCalledTimes(2) vi.unstubAllGlobals() }) diff --git a/src/shared/ai-vault-search-settings.test.ts b/src/shared/ai-vault-search-settings.test.ts index 78ff95fa2f3..1088c3ed22e 100644 --- a/src/shared/ai-vault-search-settings.test.ts +++ b/src/shared/ai-vault-search-settings.test.ts @@ -1,7 +1,6 @@ import { expect, it } from 'vitest' import { AiVaultSearchSettingsSchema, - DEFAULT_AI_VAULT_SEARCH_SETTINGS, resolveAiVaultSearchSettings, sameAiVaultSearchSettings } from './ai-vault-search-settings' @@ -9,17 +8,20 @@ import { // Off is the only safe default: building the index reads every transcript on the // machine, so a profile that has never answered must read as "no". it('reads anything that is not an explicit opt-in as off', () => { - expect(resolveAiVaultSearchSettings(undefined)).toEqual(DEFAULT_AI_VAULT_SEARCH_SETTINGS) - expect(resolveAiVaultSearchSettings({})).toEqual(DEFAULT_AI_VAULT_SEARCH_SETTINGS) - expect(resolveAiVaultSearchSettings({ aiVaultSearch: null })).toEqual( - DEFAULT_AI_VAULT_SEARCH_SETTINGS - ) - expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: 'yes' } })).toEqual( - DEFAULT_AI_VAULT_SEARCH_SETTINGS - ) - expect(resolveAiVaultSearchSettings({ aiVaultSearch: 'on' })).toEqual( - DEFAULT_AI_VAULT_SEARCH_SETTINGS - ) + expect(resolveAiVaultSearchSettings(undefined)).toEqual({ enabled: false, historyDays: null }) + expect(resolveAiVaultSearchSettings({})).toEqual({ enabled: false, historyDays: null }) + expect(resolveAiVaultSearchSettings({ aiVaultSearch: null })).toEqual({ + enabled: false, + historyDays: null + }) + expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: 'yes' } })).toEqual({ + enabled: false, + historyDays: null + }) + expect(resolveAiVaultSearchSettings({ aiVaultSearch: 'on' })).toEqual({ + enabled: false, + historyDays: null + }) }) it('normalizes a history bound and drops anything that is not one', () => { @@ -59,7 +61,7 @@ it('accepts what it produces and refuses what it does not', () => { expect(AiVaultSearchSettingsSchema.safeParse({ historyDays: null }).success).toBe(false) }) -it('treats an unchanged policy as unchanged so a re-save never restarts the index', () => { +it('compares both enabled state and history bounds', () => { expect( sameAiVaultSearchSettings( { enabled: true, historyDays: 30 }, diff --git a/src/shared/ai-vault-types.test.ts b/src/shared/ai-vault-types.test.ts index e832a9958ef..a84f928aecc 100644 --- a/src/shared/ai-vault-types.test.ts +++ b/src/shared/ai-vault-types.test.ts @@ -33,7 +33,7 @@ describe('isAiVaultSessionResumableContent', () => { expect(isAiVaultSessionResumableContent(signal())).toBe(false) }) - it('accepts conversation previews when the turn count is missing', () => { + it('accepts conversation previews when the turn count is zero', () => { // Some parsers (Grok, OpenCode fallback schemas) derive messageCount from // metadata that can be absent while real turns exist in previews. expect(isAiVaultSessionResumableContent(signal({ previewMessages: [preview('user')] }))).toBe( @@ -84,5 +84,11 @@ describe('aiVaultSessionRecoverableSignalCount', () => { subagentTranscriptCount: 3 }) ).toBe(3) + expect( + aiVaultSessionRecoverableSignalCount({ + queuedMessageCount: 4, + subagentTranscriptCount: -2 + }) + ).toBe(4) }) }) diff --git a/src/shared/claude-background-run-isolation.test.ts b/src/shared/claude-background-run-isolation.test.ts index 9b9da9ebc08..dabcf24664f 100644 --- a/src/shared/claude-background-run-isolation.test.ts +++ b/src/shared/claude-background-run-isolation.test.ts @@ -16,10 +16,11 @@ it('rebuilds only after a canonical nonzero completed exit, at most twice', asyn ['codex', 1, canonical, false] ] as const) { const result = { exitCode, stdout: '', stderr } - const run = vi.fn(async (_strictMcp: boolean) => result) + const retryResult = { ...result, stdout: 'retry output' } + const run = vi.fn(async (strictMcp: boolean) => (strictMcp ? result : retryResult)) expect( await runClaudeBackgroundAttempts({ agentId, run, failedExit: (outcome) => outcome }) - ).toBe(result) + ).toBe(eligible ? retryResult : result) expect(run.mock.calls).toEqual(eligible ? [[true], [false]] : [[true]]) } }) diff --git a/src/shared/codex-auth-errors.test.ts b/src/shared/codex-auth-errors.test.ts index de92e01c8f0..b9a4a28be6a 100644 --- a/src/shared/codex-auth-errors.test.ts +++ b/src/shared/codex-auth-errors.test.ts @@ -17,7 +17,9 @@ describe('isCodexAuthError', () => { describe('extractCodexAuthError', () => { it('returns the first matching auth line', () => { expect( - extractCodexAuthError('startup log\nERROR: not logged in. Please sign in again.\nmore log') + extractCodexAuthError( + 'startup log\nERROR: not logged in. Please sign in again.\nplease reauthenticate\nmore log' + ) ).toBe('ERROR: not logged in. Please sign in again.') })