diff --git a/config/scripts/run-internal-dev-setup.test.mjs b/config/scripts/run-internal-dev-setup.test.mjs index b00073156fa..a721d3bab7d 100644 --- a/config/scripts/run-internal-dev-setup.test.mjs +++ b/config/scripts/run-internal-dev-setup.test.mjs @@ -45,7 +45,7 @@ describe('runInternalDevSetup', () => { expect( runInternalDevSetup({ env: { - ORCA_INTERNAL_DEV_SETUP: '/tmp/setup', + ORCA_INTERNAL_DEV_SETUP: ' /tmp/setup ', ORCA_WORKTREE_PATH: '/tmp/worktree' }, platform: 'linux', @@ -59,14 +59,17 @@ describe('runInternalDevSetup', () => { }) }) - it('uses cmd.exe with explicit quoting so .cmd setup shims receive paths with spaces', () => { + it.each([ + ['C:\\repo with spaces\\worktree', 'C:\\repo with spaces\\worktree'], + ['/c/repo with spaces/worktree', 'C:\\repo with spaces\\worktree'] + ])('quotes Windows setup paths and converts MSYS worktree %s', (worktree, expected) => { const spawn = vi.fn(() => ({ status: 0 })) expect( runInternalDevSetup({ env: { - ORCA_INTERNAL_DEV_SETUP: 'C:\\tools\\setup.cmd', - ORCA_WORKTREE_PATH: 'C:\\repo\\worktree' + ORCA_INTERNAL_DEV_SETUP: 'C:\\tool suite\\setup.cmd', + ORCA_WORKTREE_PATH: worktree }, platform: 'win32', exists: () => true, @@ -75,7 +78,7 @@ describe('runInternalDevSetup', () => { ).toBe(0) expect(spawn).toHaveBeenCalledWith( 'cmd.exe', - ['/d', '/s', '/c', 'call "C:\\tools\\setup.cmd" "C:\\repo\\worktree"'], + ['/d', '/s', '/c', `call "C:\\tool suite\\setup.cmd" "${expected}"`], { stdio: 'inherit', windowsVerbatimArguments: true diff --git a/config/scripts/shared-native-build.test.mjs b/config/scripts/shared-native-build.test.mjs index 70abe2f7e29..a569db03973 100644 --- a/config/scripts/shared-native-build.test.mjs +++ b/config/scripts/shared-native-build.test.mjs @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { checkNativeBuildDirArtifacts, ensureSharedNativeBuild } from './shared-native-build.mjs' +import { peImage } from './windows-pe-image-fixture.mjs' const roots = [] @@ -19,7 +20,7 @@ function makePackage() { } describe('ensureSharedNativeBuild', () => { - it('reuses a usable published entry without compiling, even when forced', () => { + it('reuses a usable published entry without compiling', () => { const packageDir = makePackage() const entryDir = path.join(packageDir, 'build', 'orca-napi-linux-x64') mkdirSync(entryDir, { recursive: true }) @@ -90,6 +91,17 @@ describe('checkNativeBuildDirArtifacts', () => { for (const file of ['conpty.node', 'conpty/conpty.dll', 'conpty/OpenConsole.exe']) { writeFileSync(path.join(dir, file), 'not-a-pe') } + writeFileSync(path.join(dir, 'conpty.node'), peImage({ arch: 'x64' })) + expect( + checkNativeBuildDirArtifacts('node-pty', dir, { platform: 'win32', arch: 'arm64' }) + ).toMatch(/not arm64/) + + writeFileSync(path.join(dir, 'conpty.node'), peImage({ arch: 'arm64' })) + expect( + checkNativeBuildDirArtifacts('node-pty', dir, { platform: 'win32', arch: 'arm64' }) + ).toBe(null) + + writeFileSync(path.join(dir, 'conpty.node'), 'not-a-pe') expect( checkNativeBuildDirArtifacts('node-pty', dir, { platform: 'win32', arch: 'arm64' }) ).toMatch(/not arm64/) diff --git a/config/scripts/template-qualification-wait.test.mjs b/config/scripts/template-qualification-wait.test.mjs index 23b506cf8fb..34676496cc5 100644 --- a/config/scripts/template-qualification-wait.test.mjs +++ b/config/scripts/template-qualification-wait.test.mjs @@ -29,6 +29,7 @@ function execute(snapshots) { let now = 0 let index = 0 const calls = [] + const delays = [] const github = { rest: { actions: { listJobsForWorkflowRun: 'jobs' } }, async paginate(endpoint, input) { @@ -38,12 +39,14 @@ function execute(snapshots) { } return { calls, + delays, result: new AsyncFunction('github', 'context', 'Date', 'setTimeout', step.with.script)( github, { repo: { owner: 'owner', repo: 'repo' }, runId: 42 }, { now: () => now }, - (resolve) => { - now += 5000 + (resolve, delay) => { + delays.push(delay) + now += delay index++ resolve() } @@ -57,12 +60,19 @@ describe('early template assembly qualification gate', () => { async (prefix) => { const incomplete = lanes(prefix) incomplete[1] = { ...incomplete[1], status: 'in_progress', conclusion: null } - const { calls, result } = execute([incomplete, lanes(prefix)]) + const { calls, delays, result } = execute([incomplete, lanes(prefix)]) await result expect(calls).toHaveLength(2) - expect(calls.every(({ input }) => input.filter === 'latest' && input.run_id === 42)).toBe( - true - ) + expect(delays).toEqual([15_000]) + for (const { endpoint, input } of calls) { + expect(endpoint).toBe('jobs') + expect(input).toMatchObject({ + owner: 'owner', + repo: 'repo', + filter: 'latest', + run_id: 42 + }) + } } ) it.each(['failure', 'cancelled', 'skipped', 'timed_out', 'action_required', 'startup_failure'])( diff --git a/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts b/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts index 4447e3511ee..13ce756542d 100644 --- a/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts +++ b/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts @@ -41,6 +41,9 @@ describe('mobile native chat image preview reconciliation', () => { deadline: null } + expect( + findLandedImagePreviewEchoes(messages, [{ ...preview, baselineTailMessageId: 'prompt' }]) + ).toEqual([]) expect(findLandedUnconfirmedSends(messages, [unconfirmed])).toEqual([unconfirmed]) expect(findLandedImagePreviewEchoes(messages, [preview])).toEqual([ { pendingId: 'pending', messageId: 'prompt', images: ['file:///a.jpg'] } diff --git a/mobile/src/session/mobile-native-chat-file-attachment.test.ts b/mobile/src/session/mobile-native-chat-file-attachment.test.ts index 15c437f422a..901631eb65e 100644 --- a/mobile/src/session/mobile-native-chat-file-attachment.test.ts +++ b/mobile/src/session/mobile-native-chat-file-attachment.test.ts @@ -41,6 +41,7 @@ describe('uploadPickedNativeChatFiles', () => { function run(files: AsyncIterable, failName?: string) { const chips = new Map() const errors: string[] = [] + const pickedChips: PendingNativeChatFile[] = [] let id = 0 const done = uploadPickedNativeChatFiles({ picked: files, @@ -51,7 +52,10 @@ describe('uploadPickedNativeChatFiles', () => { return { path: `/store/${file.name}`, name: file.name, byteLength: 1 } }, nextId: () => `file-${(id += 1)}`, - onPicked: (file) => chips.set(file.id, file), + onPicked: (file) => { + pickedChips.push(file) + chips.set(file.id, file) + }, onStored: (fileId, stored) => { const chip = chips.get(fileId) if (chip) { @@ -61,12 +65,18 @@ describe('uploadPickedNativeChatFiles', () => { onFailed: (fileId) => chips.delete(fileId), onError: (message) => errors.push(message) }) - return { chips, errors, done } + return { chips, errors, pickedChips, done } } it('keeps every stored file as a chip with its host path', async () => { - const { chips, errors, done } = run(pickedFiles([picked('a.txt'), picked('b.txt')])) + const { chips, errors, pickedChips, done } = run( + pickedFiles([picked('a.txt'), picked('b.txt')]) + ) await done + expect(pickedChips).toEqual([ + { id: 'file-1', name: 'a.txt', mime: 'text/plain', byteLength: 1, path: null }, + { id: 'file-2', name: 'b.txt', mime: 'text/plain', byteLength: 1, path: null } + ]) expect([...chips.values()].map((chip) => chip.path)).toEqual(['/store/a.txt', '/store/b.txt']) expect(errors).toEqual([]) }) diff --git a/mobile/src/session/mobile-native-chat-image-attachment.test.ts b/mobile/src/session/mobile-native-chat-image-attachment.test.ts index f6565c32289..f95547a7304 100644 --- a/mobile/src/session/mobile-native-chat-image-attachment.test.ts +++ b/mobile/src/session/mobile-native-chat-image-attachment.test.ts @@ -59,7 +59,7 @@ describe('uploadMobileNativeChatImages', () => { // Native chat defers the paste to submit — nothing is sent to the terminal here. expect(client.calls.some((call) => call.method === 'terminal.send')).toBe(false) const saveCall = client.calls.find((c) => c.method === 'clipboard.saveImageAsTempFile') - expect(saveCall?.params).toMatchObject({ connectionId: 'conn-7' }) + expect(saveCall?.params).toEqual({ contentBase64: 'AAAA', connectionId: 'conn-7' }) }) it('uploads all three selected images in picker order', async () => { @@ -114,7 +114,7 @@ describe('uploadMobileNativeChatImages', () => { ]) }) - it('returns null when the picker is cancelled and uploads nothing', async () => { + it('returns an empty array when the picker is cancelled and uploads nothing', async () => { const client = clientWithResponses([]) const result = await uploadMobileNativeChatImages('library', { diff --git a/mobile/src/session/mobile-native-chat-image-preview.test.ts b/mobile/src/session/mobile-native-chat-image-preview.test.ts index 64ec62d57e3..93078fd654a 100644 --- a/mobile/src/session/mobile-native-chat-image-preview.test.ts +++ b/mobile/src/session/mobile-native-chat-image-preview.test.ts @@ -5,6 +5,8 @@ describe('isRenderableImageUri', () => { it('accepts local previews and real URLs the device can load', () => { for (const uri of [ 'file:///var/mobile/a.jpg', + 'FILE:///var/mobile/a.jpg', + 'HTTPS://host/a.png', 'data:image/png;base64,AAAA', 'content://media/1', 'blob:abc', diff --git a/mobile/src/session/mobile-native-chat-image-send.test.ts b/mobile/src/session/mobile-native-chat-image-send.test.ts index 20ee6f0db82..0c0bb5f91ab 100644 --- a/mobile/src/session/mobile-native-chat-image-send.test.ts +++ b/mobile/src/session/mobile-native-chat-image-send.test.ts @@ -190,21 +190,6 @@ describe('clearing a parked multi-line launch draft before the image paste', () expect(client.calls[0]?.params.text).toBe(clearInput) }) - it('falls back to a single Ctrl+U when no draft is parked', async () => { - const client = clientWithResponses([sendResult(true), sendResult(true)]) - - await pasteMobileNativeChatImagePaths({ - client, - agent: 'claude', - terminal: 'term-1', - deviceToken: null, - imagePaths: ['/tmp/a.png'], - followedByText: true - }) - - expect(client.calls[0]?.params.text).toBe('\x15') - }) - it('keeps image writes byte-clean when no text or submit follows', async () => { const client = clientWithResponses([sendResult(true), sendResult(true), sendResult(true)]) diff --git a/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts b/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts index 43b90edf8be..9998866b957 100644 --- a/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts +++ b/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts @@ -36,22 +36,46 @@ describe('activateTabAndFocusPane', () => { }) it('cancels a pending pane focus frame when a newer activation starts', () => { - const cancelAnimationFrame = vi.fn() + const frames = new Map() + let frameId = 0 + const cancelAnimationFrame = vi.fn((id: number) => frames.delete(id)) + const dispatchEvent = vi.fn() vi.stubGlobal( 'requestAnimationFrame', - vi.fn(() => 12) + vi.fn((callback: FrameRequestCallback) => { + frames.set(++frameId, callback) + return frameId + }) ) vi.stubGlobal('cancelAnimationFrame', cancelAnimationFrame) - vi.stubGlobal('window', { - dispatchEvent: vi.fn() - }) + vi.stubGlobal('window', { dispatchEvent }) activateTabAndFocusPane('tab-1', 'leaf-1') - activateTabAndFocusPane('tab-2', 'leaf-2') + activateTabAndFocusPane('tab-2', 'leaf-2', { + ackPaneKeyOnSuccess: 'tab-2:leaf-2', + flashFocusedPane: true, + scrollToBottomIfOutputSinceLastView: true + }) expect(setActiveTab).toHaveBeenNthCalledWith(1, 'tab-1') expect(setActiveTab).toHaveBeenNthCalledWith(2, 'tab-2') - expect(cancelAnimationFrame).toHaveBeenCalledWith(12) + expect(cancelAnimationFrame).toHaveBeenCalledWith(1) + expect(dispatchEvent).not.toHaveBeenCalled() + for (const callback of frames.values()) { + callback(0) + } + expect(dispatchEvent).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + type: 'orca-focus-terminal-pane', + detail: { + tabId: 'tab-2', + leafId: 'leaf-2', + ackPaneKeyOnSuccess: 'tab-2:leaf-2', + flashFocusedPane: true, + scrollToBottomIfOutputSinceLastView: true + } + }) + ) }) it('reveals the terminal surface before selecting the terminal tab', () => { diff --git a/src/renderer/src/lib/active-agent-note-send-focused-session.test.ts b/src/renderer/src/lib/active-agent-note-send-focused-session.test.ts index 26d62378275..ddaca4fa321 100644 --- a/src/renderer/src/lib/active-agent-note-send-focused-session.test.ts +++ b/src/renderer/src/lib/active-agent-note-send-focused-session.test.ts @@ -190,6 +190,17 @@ describe('active agent note send', () => { 'terminal.isRunningAgent', 'terminal.send' ]) + expect(testState.callRuntimeRpc).toHaveBeenLastCalledWith( + { kind: 'local' }, + 'terminal.send', + { + terminal: 'term-1', + text: 'File: src/app.ts', + enter: true, + client: { id: 'orca-desktop', type: 'desktop' } + }, + { timeoutMs: 15000 } + ) }) it('does not write notes when the active terminal is not an agent', async () => { diff --git a/src/shared/agent-session-account-home.test.ts b/src/shared/agent-session-account-home.test.ts index bb3778c80e6..7c012d5684b 100644 --- a/src/shared/agent-session-account-home.test.ts +++ b/src/shared/agent-session-account-home.test.ts @@ -24,6 +24,7 @@ describe('agent session account home', () => { isPersistedAgentSessionRecord({ ...record, accountHome: { variable, path: '/tmp/x' } }) expect(withVariable('CODEX_HOME')).toBe(true) expect(withVariable('GROK_HOME')).toBe(true) + expect(withVariable('X'.repeat(128))).toBe(true) for (const malformed of ['', 'A B', '1HOME', 'HOME=x', 'X'.repeat(129)]) { expect(withVariable(malformed)).toBe(false) } @@ -40,6 +41,20 @@ describe('agent session account home', () => { expect(isPersistedAgentSessionRecord({ ...record, accountHome: unmanaged })).toBe(true) expect(agentSessionAccountHomesEqual(managed, unmanaged)).toBe(false) expect(agentSessionAccountHomesEqual(managed, { ...managed })).toBe(true) + expect( + agentSessionAccountHomesEqual(managed, { + ...managed, + locator: { ...managed.locator, managedProfileId: '223e4567-e89b-42d3-a456-426614174000' } + }) + ).toBe(false) + for (const managedProfileId of ['', 'not-a-profile-id']) { + expect( + isPersistedAgentSessionRecord({ + ...record, + accountHome: { ...managed, locator: { ...managed.locator, managedProfileId } } + }) + ).toBe(false) + } expect(agentSessionAccountHomesEqual(unmanaged, { ...unmanaged })).toBe(true) expect( isPersistedAgentSessionRecord({