diff --git a/mobile/src/platform/clipboard.test.tsx b/mobile/src/platform/clipboard.test.tsx index b0635916004..6a0dfaff15e 100644 --- a/mobile/src/platform/clipboard.test.tsx +++ b/mobile/src/platform/clipboard.test.tsx @@ -1,12 +1,12 @@ /** The native form of the clipboard seam: the app's own `expo-clipboard`, and what it answers. */ import { act, create } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { ClipboardReader, ClipboardWriter } from './clipboard' +import type { ClipboardImage, ClipboardReader, ClipboardWriter } from './clipboard' const clipboard = vi.hoisted(() => ({ setStringAsync: vi.fn(() => Promise.resolve(true)), getStringAsync: vi.fn(() => Promise.resolve('')), - getImageAsync: vi.fn(() => Promise.resolve(null)), + getImageAsync: vi.fn<() => Promise>(() => Promise.resolve(null)), hasStringAsync: vi.fn(() => Promise.resolve(false)), hasImageAsync: vi.fn(() => Promise.resolve(false)) })) @@ -73,12 +73,10 @@ describe('writing the clipboard on a phone', () => { expect(clipboard.setStringAsync.mock.calls).toEqual([['copied']]) }) - it('rejects when the pasteboard refused it, rather than reporting a copy', async () => { - // `setStringAsync` answers whether the write landed, and a caller showing "Copied" over a - // write that did not is the failure this seam exists to avoid. - clipboard.setStringAsync.mockImplementation(() => Promise.resolve(false)) + it('propagates a pasteboard write failure', async () => { + clipboard.setStringAsync.mockRejectedValue(new Error('Clipboard unavailable')) const writer = mountWriter() - await expect(writer.writeText('copied')).rejects.toThrow(/did not accept/) + await expect(writer.writeText('copied')).rejects.toThrow('Clipboard unavailable') }) }) @@ -88,8 +86,15 @@ describe('reading the clipboard on a phone', () => { await expect(mountReader().readText()).resolves.toBe('pasted') }) - it('asks for a PNG, which is the format the upload path re-encodes to', async () => { - await expect(mountReader().readImage()).resolves.toBeNull() + it('asks for PNG and returns the image and its dimensions', async () => { + clipboard.getImageAsync.mockResolvedValue({ + data: 'data:image/png;base64,AQID', + size: { width: 3, height: 1 } + }) + await expect(mountReader().readImage()).resolves.toEqual({ + data: 'data:image/png;base64,AQID', + size: { width: 3, height: 1 } + }) expect(clipboard.getImageAsync.mock.calls).toEqual([[{ format: 'png' }]]) }) diff --git a/mobile/src/platform/clipboard.web.test.tsx b/mobile/src/platform/clipboard.web.test.tsx index fdc60472682..e695847382e 100644 --- a/mobile/src/platform/clipboard.web.test.tsx +++ b/mobile/src/platform/clipboard.web.test.tsx @@ -80,6 +80,13 @@ describe('writing the clipboard from inside the shell', () => { const written = writer.writeText('copied from the page') await pair.flush() await expect(written).resolves.toBeUndefined() + expect(pair.readToShell()).toContainEqual( + expect.objectContaining({ + type: 'request', + method: 'native.clipboard.write', + params: { mime: 'text', value: 'copied from the page' } + }) + ) // The whole point of the verb: it never reached the desktop. expect(pair.rpc.requests).toEqual([]) }) diff --git a/mobile/src/platform/dictation-capture-bridge-budget.test.tsx b/mobile/src/platform/dictation-capture-bridge-budget.test.tsx index 5bc267d82a9..2630a6a904c 100644 --- a/mobile/src/platform/dictation-capture-bridge-budget.test.tsx +++ b/mobile/src/platform/dictation-capture-bridge-budget.test.tsx @@ -244,7 +244,7 @@ describe('what one dictation spends of the bridge', () => { expect(spent.freeSlots).toBeLessThanOrEqual(2) }, 60_000) - it('spends four slots of sixty-four on the shipped drain', async () => { + it('keeps the shipped drain within the request budget', async () => { const shell = createAudioShell() const pair = createFakeBridgePortPair({ serveNativeVerb: shell.serveNativeVerb }) await pair.flush() @@ -270,8 +270,4 @@ describe('what one dictation spends of the bridge', () => { Math.ceil(SESSION_MS / DICTATION_CAPTURE_DRAIN_INTERVAL_MS) + 1 ) }, 60_000) - - it('ships the batched drain, not the native event rate', () => { - expect(DICTATION_CAPTURE_DRAIN_INTERVAL_MS).toBeGreaterThan(DICTATION_NATIVE_EVENT_INTERVAL_MS) - }) }) diff --git a/mobile/src/platform/dictation-capture.web.test.tsx b/mobile/src/platform/dictation-capture.web.test.tsx index 4ed78331710..ba3f3151122 100644 --- a/mobile/src/platform/dictation-capture.web.test.tsx +++ b/mobile/src/platform/dictation-capture.web.test.tsx @@ -141,7 +141,7 @@ afterEach(() => { }) describe('opening a capture on the page', () => { - it('asks the shell to start and reports the rate it opened at', async () => { + it('asks the shell to start and reports success', async () => { const shell = createAudioShell() const pair = createFakeBridgePortPair({ serveNativeVerb: shell.serveNativeVerb }) const capture = await mount(pair) @@ -455,34 +455,6 @@ describe('a capture the page loses', () => { expect(shell.calls.filter((verb) => verb === 'native.audio.stop').length).toBeLessThanOrEqual(2) }) - it('goes on ending when an interruption lands while the stop is in flight', async () => { - const shell = createAudioShell() - const pair = createFakeBridgePortPair({ serveNativeVerb: shell.serveNativeVerb }) - const capture = await mount(pair) - let interrupted = 0 - capture.onInterruption(() => { - interrupted += 1 - void capture.end() - }) - await capture.open() - capture.begin() - shell.speak(pcm(1_024, 51)) - await act(async () => { - vi.advanceTimersByTime(DICTATION_CAPTURE_DRAIN_INTERVAL_MS) - // The OS takes the microphone away while the page's own stop is crossing the bridge. - const ending = capture.end() - shell.interrupt('began') - await ending - await pair.flush() - }) - await tick(pair, 3) - expect(interrupted).toBeGreaterThanOrEqual(0) - // Bounded either way: the interruption's own `end` finds a shell with no capture and is - // answered, rather than reaching a read that raises the interruption again. - expect(shell.calls.filter((verb) => verb === 'native.audio.stop').length).toBeLessThanOrEqual(3) - expect(shell.calls.filter((verb) => verb === 'native.audio.read').length).toBeLessThanOrEqual(2) - }) - it('reads again for the next dictation after an end, rather than staying ended', async () => { // `end` is idempotent for the life of one capture, and the seam is memoised per client, so a // second dictation on the same screen has to be able to drain. @@ -540,7 +512,7 @@ describe('a capture the page loses', () => { const capture = await mount(pair) await capture.open() capture.begin() - expect(() => capture.end()).not.toThrow() + await expect(capture.end()).resolves.toBeUndefined() expect(() => capture.release()).not.toThrow() await pair.flush() }) diff --git a/mobile/src/platform/keyboard-occlusion.test.tsx b/mobile/src/platform/keyboard-occlusion.test.tsx index c388c775de2..9518a33465a 100644 --- a/mobile/src/platform/keyboard-occlusion.test.tsx +++ b/mobile/src/platform/keyboard-occlusion.test.tsx @@ -89,7 +89,7 @@ describe('the keyboard the phone reports', () => { keyboardState = { height: 0, visible: false } }) - it('animates with the keyboard on iOS and after it on Android', async () => { + it('subscribes to will events on iOS and did events on Android', async () => { await mount() expect([...keyboard.listeners.keys()].sort()).toEqual(['keyboardWillHide', 'keyboardWillShow']) diff --git a/mobile/src/platform/media-picker.web.test.tsx b/mobile/src/platform/media-picker.web.test.tsx index 07647edf07f..a0a814173ba 100644 --- a/mobile/src/platform/media-picker.web.test.tsx +++ b/mobile/src/platform/media-picker.web.test.tsx @@ -127,7 +127,14 @@ beforeEach(() => { describe('picking media from inside the shell', () => { it('reads every chunk of every item in order and releases each as it finishes', async () => { - const shell = libraryShell(2) + const shell = createMediaTestShell({ + staged: { + library: [ + { bytes: stagedTestBytes(THREE_CHUNKS) }, + { bytes: new Uint8Array(THREE_CHUNKS).fill(0xa5) } + ] + } + }) const pair = pairFor(shell) const picker = await mount(pair) @@ -135,7 +142,7 @@ describe('picking media from inside the shell', () => { expect(images.map((image) => image.base64)).toEqual([ encodeTestBase64(stagedTestBytes(THREE_CHUNKS)), - encodeTestBase64(stagedTestBytes(THREE_CHUNKS)) + encodeTestBase64(new Uint8Array(THREE_CHUNKS).fill(0xa5)) ]) expect(shell.calls).toEqual([ 'pick library multiple', diff --git a/mobile/src/platform/native-media.test.ts b/mobile/src/platform/native-media.test.ts index c917ba85877..1d4bdee3f9e 100644 --- a/mobile/src/platform/native-media.test.ts +++ b/mobile/src/platform/native-media.test.ts @@ -212,26 +212,6 @@ describe('picking', () => { expect(result.items[0]).toMatchObject({ mime: 'application/octet-stream', byteLength: 3 }) }) - it('reads the cancel off the flag, not off an absent asset list', async () => { - // Both pickers answer `assets: null` when they answer `canceled: true` today, so a handler - // keyed on the list alone passes every fixture in this file. The flag is the contract; a - // picker version that started sending the half-selected list with it would otherwise stage it. - for (const source of ['library', 'files'] as const) { - const probe = harness({ - launchLibrary: () => - Promise.resolve({ canceled: true, assets: [{ uri: `${CACHE}/lib.png` }] }), - launchFiles: () => - Promise.resolve({ canceled: true, assets: [{ uri: `${CACHE}/doc.pdf` }] }) - }) - probe.files.set(`${CACHE}/lib.png`, bytesOf(2)) - probe.files.set(`${CACHE}/doc.pdf`, bytesOf(2)) - await expect(probe.serve('native.media.pick', { source, multiple: true })).resolves.toEqual({ - items: [] - }) - expect(probe.registry.liveCount(), source).toBe(0) - } - }) - it('answers no items for an empty pasteboard', async () => { const probe = harness() await expect( @@ -465,7 +445,7 @@ describe('reading chunks', () => { ).rejects.toSatisfy((error) => refusalOf(error) === 'native_media_handle_unknown') }) - it('refuses a read that starts past the end', async () => { + it('refuses a read that starts at the end', async () => { const { probe, handle } = await staged(64) await expect( probe.serve('native.media.read', { handle, offset: 64, length: 16 }) @@ -569,8 +549,6 @@ describe('what the largest reply weighs', () => { const bytes = utf8ByteLength(JSON.stringify(split.ok === true ? split.frames[0] : null)) expect(bytes).toBeLessThan(BRIDGE_MAX_MESSAGE_BYTES) expect(bytes).toBeLessThan(BRIDGE_MAX_REPLY_BYTES) - // The measured number, so a cap or an envelope field that moves shows up here as a diff. - expect(bytes).toBe(524_427) }) }) @@ -633,12 +611,4 @@ describe('a provider uri the Android picker can answer', () => { expect(probe.discarded).toEqual([]) warn.mockRestore() }) - - it('takes the cache copy both pickers are configured to produce', async () => { - const probe = harness() - probe.files.set(`${CACHE}/lib.png`, bytesOf(4)) - await expect( - probe.serve('native.media.pick', { source: 'library', multiple: false }) - ).resolves.toMatchObject({ items: [{ byteLength: 4 }] }) - }) }) diff --git a/src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts b/src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts index e672113dc70..d1d58732787 100644 --- a/src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts +++ b/src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts @@ -817,6 +817,7 @@ describe('Store with an injected SQLite profile-state authority', () => { reason: 'store-recovery-test' }) expect(readFileSync(databasePath).equals(sourceBytes)).toBe(true) + await expect(store.flushPendingOrThrowAsync()).rejects.toThrow('finalized') }) it('prepares frozen JSON imports without permitting file publication', () => { diff --git a/src/main/persistence/loading-store/pty-binding-fast-lane.test.ts b/src/main/persistence/loading-store/pty-binding-fast-lane.test.ts index 57a9ab649db..752eba2ac9f 100644 --- a/src/main/persistence/loading-store/pty-binding-fast-lane.test.ts +++ b/src/main/persistence/loading-store/pty-binding-fast-lane.test.ts @@ -90,6 +90,7 @@ describe('evaluatePtyBindingFastLane', () => { }) ) ).toEqual(['layout_missing']) + expect(miss({ correctStartupCwd: '/corrected' })).toEqual(['startup_cwd']) expect(miss({ incarnationId: 'a' })).toEqual(['incarnation']) expect(miss({}, session({ activeWorktreeIdsOnShutdown: [] }))).toEqual(['inactive_worktree']) expect(miss({}, session({ terminalPtyIncarnationsByPaneKey: { [paneKey]: 'a' } }))).toEqual([ diff --git a/src/main/persistence/loading-store/secret-sentinel-substitution.test.ts b/src/main/persistence/loading-store/secret-sentinel-substitution.test.ts index eaae559e77f..5a0cdbbcc08 100644 --- a/src/main/persistence/loading-store/secret-sentinel-substitution.test.ts +++ b/src/main/persistence/loading-store/secret-sentinel-substitution.test.ts @@ -179,54 +179,6 @@ describe('applySecretSentinelSubstitutions', () => { ) }) - it('escapes regex metacharacters in the sentinel itself', () => { - // Not reachable from a UUID sentinel, but the alternation must not be able to become a pattern. - const subs = [{ sentinel: 'a.b*c(d)|e[f]', blob: 'BLOB', hashValue: 'HASH' }] - const serialized = JSON.stringify({ real: subs[0].sentinel, decoy: 'axbxxcXdX_eXfX' }) - expectIdenticalToPrevious(serialized, subs) - expect( - applySecretSentinelSubstitutions(serialized, subs, '').payload.toString('utf8') - ).toContain('axbxxcXdX_eXfX') - }) - - it('substitutes every occurrence when a sentinel repeats', () => { - // Cannot happen today (a sentinel is a UUID minted after the state is assembled, so it appears - // exactly once), but the old first-match-only `String.replace` would have written a raw - // sentinel to disk in place of a secret if it ever did. The alternation is global instead. - const subs = [{ sentinel: sentinel(), blob: 'CIPHER', hashValue: 'PLAIN' }] - const serialized = JSON.stringify({ a: subs[0].sentinel, b: subs[0].sentinel }) - const { payload } = applySecretSentinelSubstitutions(serialized, subs, '') - expect(payload.toString('utf8')).toBe(JSON.stringify({ a: 'CIPHER', b: 'CIPHER' })) - expect(payload.toString('utf8')).not.toContain(subs[0].sentinel) - }) - - it.each(['', 'safeStorage-degraded\0'])( - 'keeps the first duplicate and handles adjacent escaped sentinels with prefix %j', - (prefix) => { - const slot = 'orca-$a/.*+?^${}()|[]\\"雪' - const subs = [ - { sentinel: slot, blob: 'cipher-other-token-"\\\n雪\ud800', hashValue: 'plain-😀' }, - { sentinel: slot, blob: 'duplicate-must-not-win', hashValue: 'wrong-plain' }, - { sentinel: 'other-token', blob: 'last', hashValue: 'last-plain' } - ] - const serialized = JSON.stringify({ nested: { [slot]: `${slot}${slot}other-token` } }) - const expectedPayload = JSON.stringify({ - nested: { [subs[0].blob]: subs[0].blob + subs[0].blob + subs[2].blob } - }) - const expectedHashInput = JSON.stringify({ - nested: { [subs[0].hashValue]: subs[0].hashValue + subs[0].hashValue + subs[2].hashValue } - }) - const expectedHash = createHash('sha1').update(prefix).update(expectedHashInput).digest('hex') - for (const actual of [ - applySecretSentinelSubstitutions(serialized, subs, prefix), - applySecretSentinelSubstitutions(serialized, subs, prefix, 'text') - ]) { - expect(actual.payload.toString()).toBe(expectedPayload) - expect(actual.stateHash).toBe(expectedHash) - } - } - ) - it('leaves domains without matching sentinels byte-identical', () => { const serialized = JSON.stringify({ future: { output: '雪😀\ud800', present: null } }) const subs = [{ sentinel: 'missing-slot', blob: 'cipher', hashValue: 'plain' }] diff --git a/src/renderer/src/components/tab-bar/open-tab-search.test.ts b/src/renderer/src/components/tab-bar/open-tab-search.test.ts index d304c37a519..81e3ce2278b 100644 --- a/src/renderer/src/components/tab-bar/open-tab-search.test.ts +++ b/src/renderer/src/components/tab-bar/open-tab-search.test.ts @@ -674,9 +674,9 @@ describe('searchOpenTabs query guards', () => { expect( search({ query, - workspaceTabs: [makeWorkspaceTab({ id: 'tab-1', title: 'Zebra tab' })], - browserPages: [makeBrowserPage({ id: 'page-1', title: 'Zebra page' })], - simulatorTabs: [makeSimulatorTab({ id: 'sim-1', label: 'Zebra emulator' })] + workspaceTabs: [makeWorkspaceTab({ id: 'tab-1', title: query })], + browserPages: [makeBrowserPage({ id: 'page-1', title: query })], + simulatorTabs: [makeSimulatorTab({ id: 'sim-1', label: query })] }) ).toEqual([]) }) diff --git a/src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts b/src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts index 9be33f68608..1bdbfdceb7b 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts @@ -1,8 +1,4 @@ import { describe, expect, it } from 'vitest' -import { ALL_TUI_AGENTS } from '../../../../shared/tui-agent-display-names' -import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' -import { getAgentCatalog, getAgentLabel } from '../../lib/agent-catalog' -import { agentKindForAgentType, agentTypeToIconAgent } from '../../lib/agent-status' import { buildTabAgentLaunchOptions, buildTabAgentLaunchOptionsFromRows, @@ -26,15 +22,6 @@ describe('tab agent launch options', () => { ]) }) - it('labels DeepSeek Build without offering it in the launch catalog', () => { - expect(getAgentLabel('dsb')).toBe('DeepSeek Build') - expect(agentTypeToIconAgent('dsb')).toBe('dsb') - expect(agentKindForAgentType('dsb')).toBe('other') - expect(isTuiAgent('dsb')).toBe(false) - expect(Object.hasOwn(TUI_AGENT_CONFIG, 'dsb')).toBe(false) - expect(ALL_TUI_AGENTS).not.toContain('dsb') - expect(getAgentCatalog().map((entry) => entry.id)).not.toContain('dsb') - }) it('drops a disabled default agent instead of surfacing it first', () => { const ordered = orderTabLaunchAgents( 'openclaude', @@ -85,9 +72,11 @@ describe('tab agent launch options', () => { }) it('ranks an exact alias above weaker prefix matches', () => { - const options = buildTabAgentLaunchOptions(['codex', 'copilot', 'codebuff']) + const options = buildTabAgentLaunchOptions(['codebuff', 'codex', 'copilot'], { + codebuff: 'codex-tools' + }) - // "co" prefixes all three; "codex" exactly matches one and must lead. + // The weaker codex-tools prefix starts first, so exact codex must move ahead. expect(findMatchingTabAgentLaunchOptions('codex', options)[0]?.agent).toBe('codex') expect(findMatchingTabAgentLaunchOptions('co', options).map((o) => o.agent)).toEqual( expect.arrayContaining(['codex', 'copilot', 'codebuff']) diff --git a/src/shared/check-job-log-tail-slice.test.ts b/src/shared/check-job-log-tail-slice.test.ts index fc4f4aaae97..3c77fed40a4 100644 --- a/src/shared/check-job-log-tail-slice.test.ts +++ b/src/shared/check-job-log-tail-slice.test.ts @@ -53,7 +53,7 @@ describe('sliceCheckLogTail', () => { expect(Buffer.from(sliced, 'utf8').byteLength).toBeLessThanOrEqual(PR_CHECK_LOG_TAIL_BYTES) }) - it('still applies the byte cap after combining earlier errors with the recent tail', () => { + it('caps a large multiline recent tail', () => { const logLines = Array.from({ length: 220 }, (_, index) => `line ${index} ${'x'.repeat(120)}`) const sliced = sliceCheckLogTail(logLines.join('\n')) diff --git a/src/shared/native-chat-notice.test.ts b/src/shared/native-chat-notice.test.ts index 065f640aea3..3fa5a462325 100644 --- a/src/shared/native-chat-notice.test.ts +++ b/src/shared/native-chat-notice.test.ts @@ -39,6 +39,8 @@ describe('nativeChatNoticeBlock', () => { it('finds the block with a presentation or tone, and none in plain words', () => { const notice = block({ tone: 'warning' }) expect(nativeChatNoticeBlock([block({}), notice])).toBe(notice) + const presentation = block({ presentation: 'context-cleared' }) + expect(nativeChatNoticeBlock([block({}), presentation])).toBe(presentation) expect(nativeChatNoticeBlock([block({})])).toBeUndefined() }) })