Files
orca/src/main/ipc/qoder-desktop-execution-host.test.ts
T
Neil ebeea319b2 Support modern Qoder commands and verify authenticated resume
Start and resume Qoder through the existing execution-host selector when only the documented qoder command exists. Preserve legacy qodercli preference, explicit commands, quoting and session identity; disconnected SSH execution refuses without local fallback.

Mobile history uses command-at-create only with the optional owned-create capability and an existing stable mutation identity. Reconcile authoritative execution-host inventory before retrying creation, adopt the same surviving operation, preserve WSL/incarnation metadata and restore only missing original launch metadata. Changed retry settings cannot replace original capture. Bounded evidence expires after 15 minutes; unavailable inventory or original evidence refuses recovery and leaves surviving execution untouched. Authoritative inventory proving absence preserves the existing recreation behavior; this is not a durable exactly-once ledger for completed one-shot side effects. Older hosts retain the acknowledged create-then-send path.

Scope mobile launch authority to the current committed host/client generation, and recheck operation ownership after asynchronous preparation before later sends. Retire the mutation once the host acknowledges the resume; later ownership changes stop navigation without reporting a completed resume as failed or reusing a cached legacy pane. Preserve genuinely interrupted mutation identity. Preserve current-main OpenCode validation and merged Pi/Cursor behavior. Correct unchanged-main editor test fixtures to their production insertion-range and store contracts while retaining original assertions and production behavior.

Credit: Neil Parker (@nwparker); Soperf and jyang for Qoder integration/history groundwork in #24614; actual Pullfrog and CodeRabbit reviews and the independent Source reviewer for the mobile retry, launch capture, evidence lifetime and connection ownership findings. Conservative positional process recognition and existing folder-history matching-worktree limitations remain documented.
2026-10-04 19:50:45 -07:00

300 lines
12 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
import { createDaemonActiveProviderFixtures } from './pty-ipc-daemon-provider-fixtures'
import {
registerPtyHandlers,
registerSshPtyProvider,
unregisterSshPtyProvider,
getLocalPtyProvider
} from './pty'
import { isCommandOnPath } from './preflight-command-exec'
import { detectWslCommandsOnPath } from './preflight-wsl-agent-detection'
import type * as FsPromises from 'node:fs/promises'
import type { Store } from '../persistence'
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import { buildAgentResumeStartupPlan } from '../../shared/tui-agent-startup'
import type { PtySpawnIpcArgs } from './pty/ipc/spawn-types'
vi.mock('./preflight-command-exec', () => ({ isCommandOnPath: vi.fn() }))
vi.mock('./local-agent-install-dir-detection', () => ({
detectCommandsInInstallDirs: () => new Set()
}))
vi.mock('./preflight-wsl-agent-detection', () => ({ detectWslCommandsOnPath: vi.fn() }))
const { mux } = vi.hoisted(() => ({ mux: vi.fn() }))
vi.mock('../ssh/ssh-target-registry', () => ({ getActiveMultiplexer: mux }))
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof FsPromises>()),
stat: vi.fn(async () => ({ isDirectory: () => true }))
}))
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock()))
vi.mock('node:child_process', async (importOriginal) =>
(await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal())
)
vi.mock('../opencode/hook-service', () =>
import('./pty-ipc-mock-registry').then((m) => m.openCodeHookServiceModuleMock())
)
vi.mock('../mimo/hook-service', () =>
import('./pty-ipc-mock-registry').then((m) => m.mimoHookServiceModuleMock())
)
vi.mock('../agent-hooks/server', () =>
import('./pty-ipc-mock-registry').then((m) => m.agentHookServerModuleMock())
)
vi.mock('../pi/titlebar-extension-service', () =>
import('./pty-ipc-mock-registry').then((m) => m.piTitlebarExtensionModuleMock())
)
vi.mock('../pwsh', () => import('./pty-ipc-mock-registry').then((m) => m.pwshModuleMock()))
vi.mock('../wsl', async (importOriginal) =>
(await import('./pty-ipc-mock-registry')).wslModuleMock(await importOriginal())
)
vi.mock('../telemetry/client', () =>
import('./pty-ipc-mock-registry').then((m) => m.telemetryClientModuleMock())
)
vi.mock('../telemetry/classify-error', () =>
import('./pty-ipc-mock-registry').then((m) => m.classifyErrorModuleMock())
)
vi.mock('../cli/linux-terminal-orca-cli-shim', () =>
import('./pty-ipc-mock-registry').then((m) => m.linuxCliShimModuleMock())
)
vi.mock('../memory/pty-registry', () =>
import('./pty-ipc-mock-registry').then((m) => m.ptyRegistryModuleMock())
)
vi.mock('../agent-hooks/migration-unsupported-pty-state', () =>
import('./pty-ipc-mock-registry').then((m) => m.migrationUnsupportedPtyModuleMock())
)
vi.mock('../codex/codex-pane-account-registry', () =>
import('./pty-ipc-mock-registry').then((m) => m.codexPaneAccountRegistryModuleMock())
)
vi.mock('../codex/codex-state-db-backfill-recovery', () =>
import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
)
describe('desktop Qoder execution-host selection', () => {
const { handlers, mainWindow } = setupPtyIpcSuite()
const { setupDaemonAdapter, withWin32Platform } = createDaemonActiveProviderFixtures({
handlers,
mainWindow
})
const nativeProbe = vi.mocked(isCommandOnPath)
const wslProbe = vi.mocked(detectWslCommandsOnPath)
beforeEach(() => {
nativeProbe.mockReset().mockImplementation(async (cmd) => cmd === 'qodercli')
wslProbe.mockReset().mockResolvedValue(new Set(['qoder']))
mux.mockReset()
})
async function spawn(args: Partial<PtySpawnIpcArgs> = {}, store?: Store) {
const providerSpawn = setupDaemonAdapter()
registerPtyHandlers(mainWindow, undefined, undefined, undefined, undefined, store)
const result = await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: process.cwd(),
worktreeId: `repo-review::${process.cwd()}`,
launchAgent: 'qoder',
command: 'qodercli --resume original-id',
...args
})
return { providerSpawn, result }
}
function folderStore(folderPath: string): Store {
const folder: FolderWorkspace = {
id: 'review-folder',
projectGroupId: 'review-group',
name: 'review',
folderPath,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
const store: Pick<
Store,
'getFolderWorkspace' | 'getFolderWorkspaces' | 'getProjectGroups' | 'getRepos'
> = {
getFolderWorkspace: () => folder,
getFolderWorkspaces: () => [folder],
getProjectGroups: () => [],
getRepos: () => []
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: These launches omit pane metadata; only folder path/status methods are reached, trust is disabled by the suite.
return store as Store
}
it.each([
['legacy-only', ['qodercli']],
['both', ['qoder', 'qodercli']]
] as const)('keeps legacy preference on %s native hosts', async (_, commands) => {
nativeProbe.mockImplementation(async (cmd) => commands.some((found) => found === cmd))
const { providerSpawn } = await spawn()
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ command: 'qodercli --resume original-id' })
)
expect(nativeProbe).toHaveBeenCalledWith('qodercli')
expect(nativeProbe).toHaveBeenCalledWith('qoder')
expect(wslProbe).not.toHaveBeenCalled()
})
it('preserves exact resume tokens and captured configuration on a native folder', async () => {
nativeProbe.mockImplementation(async (cmd) => cmd === 'qoder')
const plan = buildAgentResumeStartupPlan({
agent: 'qoder',
providerSession: { key: 'session_id', id: 'session with spaces' },
platform: 'darwin',
cmdOverrides: {}
})
if (!plan) {
throw new Error('Missing Qoder resume plan')
}
const { providerSpawn, result } = await spawn(
{
worktreeId: 'folder:review-folder',
command: plan.launchCommand,
launchConfig: plan.launchConfig
},
folderStore(process.cwd())
)
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({
command: "qoder '--resume' 'session with spaces'",
cwd: process.cwd(),
worktreeId: 'folder:review-folder'
})
)
expect(result).toMatchObject({ launchConfig: { ...plan.launchConfig, agentCommand: 'qoder' } })
})
it.each([
{ command: "echo 'qodercli --resume text'", launchAgent: 'qoder' },
{ command: 'qodercli --resume original-id', launchAgent: undefined },
{ command: '/caller/qodercli --resume original-id', launchAgent: 'qoder' },
{ command: 'qoder --resume original-id', launchAgent: 'qoder' }
] as const)('leaves caller and shell commands unchanged: $command', async (args) => {
const { providerSpawn } = await spawn(args)
expect(providerSpawn).toHaveBeenCalledWith(expect.objectContaining({ command: args.command }))
expect(nativeProbe).not.toHaveBeenCalled()
expect(wslProbe).not.toHaveBeenCalled()
expect(mux).not.toHaveBeenCalled()
})
it('uses the selected project WSL distro rather than installed native legacy', async () => {
await withWin32Platform(async () => {
const { providerSpawn } = await spawn({
cwd: '/work/project',
worktreeId: 'repo-review::/work/project',
projectRuntime: {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'project-review',
distro: 'Debian',
reason: 'project-override',
cacheKey: 'review'
}
}
})
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({
command: 'qoder --resume original-id',
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: 'Debian'
})
)
expect(wslProbe).toHaveBeenCalledWith({ distro: 'Debian' }, ['qodercli', 'qoder'])
expect(nativeProbe).not.toHaveBeenCalled()
})
})
it('retains a repo-less UNC folder execution distro', async () => {
await withWin32Platform(async () => {
const { providerSpawn } = await spawn(
{
cwd: String.raw`\\wsl.localhost\Ubuntu\home\review\folder`,
worktreeId: 'folder:review-folder',
shellOverride: 'wsl.exe'
},
folderStore(String.raw`\\wsl.localhost\Ubuntu\home\review\folder`)
)
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({
command: 'qoder --resume original-id',
terminalWindowsWslDistro: 'Ubuntu',
cwd: '//wsl.localhost/Ubuntu/home/review/folder'
})
)
expect(wslProbe).toHaveBeenCalledWith({ distro: 'Ubuntu' }, ['qodercli', 'qoder'])
expect(nativeProbe).not.toHaveBeenCalled()
})
})
it('keeps native Windows launches on the host shell', async () => {
await withWin32Platform(async () => {
nativeProbe.mockImplementation(async (cmd) => cmd === 'qoder')
const { providerSpawn } = await spawn({
cwd: 'C:\\review',
worktreeId: 'repo-review::C:\\review',
shellOverride: 'cmd.exe'
})
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({
command: 'qoder --resume original-id',
shellOverride: 'cmd.exe',
terminalWindowsWslDistro: null
})
)
expect(wslProbe).not.toHaveBeenCalled()
})
})
it.each(['modern-only', 'legacy-only', 'missing', 'disposed'] as const)(
'lets SSH own detection and refuse absent contact: %s',
async (host) => {
const remoteSpawn = setupDaemonAdapter()
registerSshPtyProvider('ssh-qoder-review', getLocalPtyProvider())
const localSpawn = setupDaemonAdapter()
const request = vi
.fn()
.mockResolvedValue({ agents: [host === 'modern-only' ? 'qoder' : 'qodercli'] })
mux.mockReturnValue(
host === 'missing' ? null : { isDisposed: () => host === 'disposed', request }
)
registerPtyHandlers(mainWindow)
try {
const pending = handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/remote/folder',
worktreeId: 'repo-ssh::/remote/folder',
connectionId: 'ssh-qoder-review',
launchAgent: 'qoder',
command: "qodercli '--resume' 'same remote id'"
})
if (host === 'missing' || host === 'disposed') {
await expect(pending).rejects.toThrow('execution host connection')
expect(remoteSpawn).not.toHaveBeenCalled()
expect(request).not.toHaveBeenCalled()
} else {
await pending
expect(remoteSpawn).toHaveBeenCalledWith(
expect.objectContaining({
cwd: '/remote/folder',
command: `${host === 'modern-only' ? 'qoder' : 'qodercli'} '--resume' 'same remote id'`
})
)
expect(request).toHaveBeenCalledWith('preflight.detectAgents', {
commands: [
{ id: 'qodercli', cmd: 'qodercli' },
{ id: 'qoder', cmd: 'qoder' }
]
})
}
expect(localSpawn).not.toHaveBeenCalled()
expect(nativeProbe).not.toHaveBeenCalled()
expect(wslProbe).not.toHaveBeenCalled()
expect(mux).toHaveBeenCalledWith('ssh-qoder-review')
} finally {
unregisterSshPtyProvider('ssh-qoder-review')
}
}
)
})