Files
orca/src/main/opencode/opencode-launch-model-context-cleanup.test.ts
T
Brennan Benson 0266b5f95f fix(opencode): allow model picks when discovery is unavailable (#31985)
* fix(opencode): allow picked models across CLI updates

* test(opencode): prune obsolete timeout ratchet allowance

* fix(opencode): use resolved agents and refuse unsafe workers upfront

* fix(i18n): include OpenCode refusal messages in runtime catalog
2026-10-26 13:12:53 -07:00

283 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from 'zod'
import { ChildProcess } from 'node:child_process'
import { PassThrough } from 'node:stream'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { spawnProcess } from '@orca/process-host'
import {
forceTerminateProcessTree,
signalProcessTree
} from '@orca/process-host/process-tree-termination'
import { probeOpenCodeLaunchModelContext } from './opencode-launch-model-context'
import { readFetchResponseJsonWithinLimit } from '../../shared/fetch-response-body'
import { PROVIDER_SUPERVISOR_MAX_STOP_MS } from '../provider-process/provider-process-supervisor'
import { terminateProviderProcessTree } from '../provider-process/provider-process-teardown'
vi.mock('@orca/process-host', () => ({ spawnProcess: vi.fn() }))
vi.mock('@orca/process-host/process-tree-termination', () => ({
signalProcessTree: vi.fn(),
forceTerminateProcessTree: vi.fn()
}))
vi.mock('../provider-process/provider-process-teardown', () => ({
terminateProviderProcessTree: vi.fn()
}))
vi.mock('../../shared/fetch-response-body', () => ({ readFetchResponseJsonWithinLimit: vi.fn() }))
class ProbeChild extends ChildProcess {
override stdin = new PassThrough()
override stdout = new PassThrough()
override stderr = new PassThrough()
override pid = 123456
override exitCode: number | null = null
override signalCode: NodeJS.Signals | null = null
// A handle-less ChildProcess's own kill can signal this test's process group.
override kill = vi.fn((_signal?: NodeJS.Signals | number): boolean => true)
override stdio: [PassThrough, PassThrough, PassThrough, undefined, undefined] = [
this.stdin,
this.stdout,
this.stderr,
undefined,
undefined
]
}
const directory = '/private/project'
const model = { id: 'model-b', providerID: 'private-proof', enabled: true }
const snapshot = {
model: { location: { directory }, data: [model] },
agent: {
location: { directory },
data: [{ id: 'build', mode: 'primary', hidden: false, model }]
},
'model/default': { location: { directory }, data: model },
config: [{ type: 'document', info: { default_agent: 'build' } }]
}
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
let child: ProbeChild
let closeDuringFetch = false
function close() {
child.exitCode = 0
child.emit('exit', 0, null)
child.emit('close', 0, null)
}
function supervisorExits(code: number | null, signal: NodeJS.Signals | null) {
child.exitCode = code
child.signalCode = signal
child.emit('exit', code, signal)
child.emit('close', code, signal)
}
const options = { executable: '/private/opencode', cwd: directory, env: {} }
function decodedSupervisorSpec(env: unknown): unknown {
const encoded =
typeof env === 'object' && env !== null && 'ORCA_PROVIDER_SUPERVISOR_SPEC' in env
? String(env.ORCA_PROVIDER_SUPERVISOR_SPEC)
: ''
return JSON.parse(Buffer.from(encoded, 'base64').toString() || 'null')
}
async function untilStopRequested(
requested = () => child.kill.mock.calls.length > 0
): Promise<void> {
while (!requested()) {
await new Promise((resolve) => setImmediate(resolve))
}
}
describe('OpenCode model probe termination evidence', () => {
beforeEach(() => {
vi.resetAllMocks()
Object.defineProperty(process, 'platform', { value: 'darwin' })
vi.mocked(readFetchResponseJsonWithinLimit).mockImplementation((response) => response.json())
child = new ProbeChild()
closeDuringFetch = false
vi.mocked(spawnProcess).mockImplementation(() => {
queueMicrotask(() => child.stdout.write('server listening on http://127.0.0.1:45678\n'))
return child
})
vi.stubGlobal(
'fetch',
vi.fn(async (input: URL) => {
const endpoint = z
.enum(['model', 'agent', 'model/default', 'config'])
.parse(input.pathname.slice('/api/'.length))
const response = snapshot[endpoint]
if (closeDuringFetch && endpoint === 'agent') {
queueMicrotask(close)
}
return new Response(JSON.stringify(response), { status: 200 })
})
)
vi.mocked(signalProcessTree).mockImplementation(async () => {
close()
return true
})
vi.mocked(forceTerminateProcessTree).mockResolvedValue(false)
child.kill.mockImplementation((signal) => {
if (signal === 'SIGTERM') {
supervisorExits(null, 'SIGTERM')
}
return true
})
})
afterEach(() => {
vi.useRealTimers()
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
vi.unstubAllGlobals()
})
it('cancels all unread responses if the bounded body reader fails', async () => {
const cancel = vi.fn()
vi.mocked(readFetchResponseJsonWithinLimit).mockRejectedValue(new Error('body_reader_failed'))
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(new ReadableStream({ cancel }), { status: 200 }))
)
expect(await probeOpenCodeLaunchModelContext(options)).toBeNull()
expect(cancel).toHaveBeenCalledTimes(1)
expect(child.kill).toHaveBeenCalledWith('SIGTERM')
})
it('accepts an already-closed Windows probe without signaling its former pid', async () => {
Object.defineProperty(process, 'platform', { value: 'win32' })
closeDuringFetch = true
vi.mocked(signalProcessTree).mockResolvedValue(false)
expect(await probeOpenCodeLaunchModelContext(options)).toMatchObject({ primaryAgent: 'build' })
expect(signalProcessTree).not.toHaveBeenCalled()
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
})
it('runs a POSIX probe under a one-shot supervisor and stops it through that supervisor', async () => {
expect(await probeOpenCodeLaunchModelContext(options)).toMatchObject({ primaryAgent: 'build' })
const [spec] = vi.mocked(spawnProcess).mock.calls[0]
expect(spec).toMatchObject({ program: process.execPath, cwd: directory, detached: true })
const args = spec.args ?? []
expect(args.slice(args.indexOf('--') + 1)).toEqual([
'/private/opencode',
'serve',
'--hostname',
'127.0.0.1',
'--port',
'0'
])
// Session lifetime would stop this probe after its stdin closes.
expect(decodedSupervisorSpec(spec.env)).toMatchObject({ lifetime: 'one-shot', cwd: directory })
expect(child.kill.mock.calls).toEqual([['SIGTERM']])
// Killing the supervisor’s group can orphan the server’s detached group.
expect(signalProcessTree).not.toHaveBeenCalled()
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
expect(terminateProviderProcessTree).not.toHaveBeenCalled()
})
it('forces a POSIX supervisor tree only after its full stop time, and trusts no forced stop', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
child.kill.mockReturnValue(true)
// Supervisor teardown alone cannot prove the detached server group is gone.
vi.mocked(terminateProviderProcessTree).mockImplementation(async () => {
supervisorExits(null, 'SIGKILL')
return null
})
const probe = probeOpenCodeLaunchModelContext(options)
await untilStopRequested()
await vi.advanceTimersByTimeAsync(PROVIDER_SUPERVISOR_MAX_STOP_MS - 1)
expect(terminateProviderProcessTree).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(await probe).toBeNull()
expect(terminateProviderProcessTree).toHaveBeenCalledWith(child, {
site: 'opencode-launch-model-preflight'
})
})
it('trusts no forced POSIX stop, even one whose teardown reports the tree gone', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
child.kill.mockReturnValue(true)
vi.mocked(terminateProviderProcessTree).mockImplementation(async () => {
supervisorExits(null, 'SIGKILL')
return 'exited'
})
const probe = probeOpenCodeLaunchModelContext(options)
await untilStopRequested()
await vi.advanceTimersByTimeAsync(PROVIDER_SUPERVISOR_MAX_STOP_MS)
expect(await probe).toBeNull()
})
it('trusts no POSIX stop whose supervisor exited 1, which a failed group reap also exits', async () => {
child.kill.mockImplementation(() => {
supervisorExits(1, null)
return true
})
expect(await probeOpenCodeLaunchModelContext(options)).toBeNull()
expect(terminateProviderProcessTree).not.toHaveBeenCalled()
})
it('accepts a POSIX supervisor that relayed the server exiting on its own', async () => {
child.kill.mockImplementation(() => {
supervisorExits(0, null)
return true
})
expect(await probeOpenCodeLaunchModelContext(options)).toMatchObject({ primaryAgent: 'build' })
})
it('never forces a POSIX supervisor whose root exited without closing its pipes', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
child.kill.mockImplementation(() => {
child.exitCode = 0
child.emit('exit', 0, null)
return true
})
const probe = probeOpenCodeLaunchModelContext(options)
await untilStopRequested()
await vi.advanceTimersByTimeAsync(PROVIDER_SUPERVISOR_MAX_STOP_MS)
expect(await probe).toBeNull()
expect(terminateProviderProcessTree).not.toHaveBeenCalled()
})
it('keeps a Windows root exit with inherited pipes unverified and avoids its former pid', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
Object.defineProperty(process, 'platform', { value: 'win32' })
vi.mocked(signalProcessTree).mockImplementation(async () => {
child.exitCode = 0
child.emit('exit', 0, null)
return false
})
let settled = false
const probe = probeOpenCodeLaunchModelContext(options).finally(() => (settled = true))
await untilStopRequested(() => vi.mocked(signalProcessTree).mock.calls.length > 0)
await vi.advanceTimersByTimeAsync(1_499)
expect(settled).toBe(false)
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(await probe).toBeNull()
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
})
it('requires verified termination when a still-live Windows probe does not close', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
Object.defineProperty(process, 'platform', { value: 'win32' })
vi.mocked(signalProcessTree).mockResolvedValue(true)
let settled = false
const probe = probeOpenCodeLaunchModelContext(options).finally(() => (settled = true))
await untilStopRequested(() => vi.mocked(signalProcessTree).mock.calls.length > 0)
await vi.advanceTimersByTimeAsync(1_499)
expect(settled).toBe(false)
expect(forceTerminateProcessTree).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(await probe).toBeNull()
expect(forceTerminateProcessTree).toHaveBeenCalledOnce()
})
})