Files
orca/src/main/opencode/opencode-startup-prompt-installer.test.ts
T
e8310d5a4f fix(opencode): submit admitted native startup briefs without overwriting input (#24762)
* fix: wait for OpenCode worker composer before first dispatch

Reuse captured composer readiness on local and paired execution hosts and revoke launching-shell paste anchors.

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>

* feat(opencode): probe execution-host CLI capabilities

* fix(opencode): select plugin default for execution host loader

* fix(opencode): limit prompt prefill capability to verified release

* feat(opencode): probe launch capabilities on the execution host

* fix(opencode): select plugin loader for the launched host binary

* fix(opencode): match WSL probe cwd and declared guest environment

* fix(opencode): preserve launch environment deletion boundaries

* wip(opencode): authorize native startup prompt intent at execution owner

* fix(opencode): atomically replace status plugin entrypoints

* fix(opencode): retain plugin permissions across restrictive umasks

* test(opencode): resolve permission fixture from primary cwd

* feat(opencode): install startup prompt plugin independently of status hooks

* fix(opencode): wait for admitted startup intent and preserve failed-launch briefs

* fix(opencode): unsubscribe hook settings during async host shutdown

* STRICT launch CI contract correction

* CAPS launch CI contract correction

* INTENT launch CI contract correction

* test: initialize Claude prompt state in output retention fixture

* Wait for OpenCode location hydration in intent startup

* Bind OpenCode startup readiness to the current location in intent startup

* Retry interrupted OpenCode startup prompt claims

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Orca startup hydration review <agents@stably.ai>
Co-authored-by: Orca <dev@stably.ai>
2026-10-04 01:41:01 -07:00

254 lines
10 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { setAppEnvironment } from '../../shared/app-environment'
import {
createOpenCodeStartupPromptInstaller,
installOpenCodeStartupPromptForLaunch
} from './opencode-startup-prompt-installer'
import {
captureOpenCodeSourceConfig,
applyOpenCodeStatusPluginEnv
} from '../ipc/pty/host-env/opencode-config'
let root: string
let originalXdg: string | undefined
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'opencode-prompt-install-'))
originalXdg = process.env.XDG_CONFIG_HOME
process.env.XDG_CONFIG_HOME = join(root, 'config')
setAppEnvironment({
getPath: () => join(root, 'profile'),
getAppPath: () => root,
getVersion: () => 'test',
isPackaged: () => false,
onWillQuit: () => {},
exit: () => {},
getAppMetrics: () => []
})
})
afterEach(() => {
vi.unstubAllEnvs()
if (originalXdg === undefined) {
delete process.env.XDG_CONFIG_HOME
} else {
process.env.XDG_CONFIG_HOME = originalXdg
}
rmSync(root, { recursive: true, force: true })
})
describe('OpenCode startup prompt installer', () => {
it('refuses a replaced status overlay instead of writing through its symlink or losing status hooks', () => {
const source = join(root, 'safe-source')
const foreign = join(root, 'foreign')
mkdirSync(source)
mkdirSync(foreign)
writeFileSync(join(foreign, 'untouched.txt'), 'foreign bytes')
const env: Record<string, string> = {
OPENCODE_CONFIG_DIR: source,
ORCA_OPENCODE_PLUGIN_API: 'v2',
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'replaced-overlay'
}
const config = captureOpenCodeSourceConfig(env, join(root, 'profile'))
applyOpenCodeStatusPluginEnv(
'pane',
env,
config,
{
launchAgent: 'opencode',
agentStatusHooksEnabled: true
},
'opencode'
)
rmSync(env.OPENCODE_CONFIG_DIR, { recursive: true })
symlinkSync(foreign, env.OPENCODE_CONFIG_DIR, 'junction')
expect(installOpenCodeStartupPromptForLaunch(env)).toBe(false)
expect(env.ORCA_OPENCODE_STARTUP_PROMPT_NONCE).toBeUndefined()
expect(readFileSync(join(foreign, 'untouched.txt'), 'utf8')).toBe('foreign bytes')
expect(existsSync(join(foreign, 'plugins'))).toBe(false)
})
it.each(['opencode', 'opencode2'] as const)(
'keeps real source provenance and composes the %s status and prompt in one final overlay',
(agent) => {
const source = join(root, 'real-source')
mkdirSync(join(source, 'plugins'), { recursive: true })
writeFileSync(join(source, 'plugins', 'user.js'), 'user bytes')
writeFileSync(join(source, 'opencode.json'), '{"model":"selected/model"}')
const env: Record<string, string> = {
OPENCODE_CONFIG_DIR: source,
ORCA_OPENCODE_PLUGIN_API: 'v2',
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'source-provenance'
}
expect(installOpenCodeStartupPromptForLaunch(env, false)).toBe(true)
expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBe(source)
const captured = captureOpenCodeSourceConfig(env, join(root, 'profile'))
expect(captured.directory).toBe(source)
applyOpenCodeStatusPluginEnv(
'pane',
env,
captured,
{
launchAgent: agent,
agentStatusHooksEnabled: true
},
`${agent} --standalone`
)
const statusOverlay = env.OPENCODE_CONFIG_DIR
expect(installOpenCodeStartupPromptForLaunch(env)).toBe(true)
expect(env.OPENCODE_CONFIG_DIR).toBe(statusOverlay)
expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBe(source)
expect(
readFileSync(join(statusOverlay, 'plugins', `orca-${agent}-status.js`), 'utf8')
).toContain('ORCA_STATUS_AGENT')
expect(
existsSync(join(statusOverlay, 'plugins', 'orca-opencode-startup-prompt', 'tui.js'))
).toBe(true)
expect(readFileSync(join(statusOverlay, 'plugins', 'user.js'), 'utf8')).toBe('user bytes')
const nested: Record<string, string> = {
...env,
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'nested-source-provenance'
}
expect(installOpenCodeStartupPromptForLaunch(nested)).toBe(true)
expect(nested.OPENCODE_CONFIG_DIR).toBe(statusOverlay)
expect(nested.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBe(source)
expect(readFileSync(join(source, 'plugins', 'user.js'), 'utf8')).toBe('user bytes')
expect(existsSync(join(source, 'plugins', `orca-${agent}-status.js`))).toBe(false)
}
)
it.each(['inherited', 'explicit'] as const)(
'preserves status and user plugins from the %s XDG config when launch env is sparse',
(selection) => {
const configHome = join(root, selection === 'explicit' ? 'selected-config' : 'config')
const config = join(configHome, 'opencode')
mkdirSync(join(config, 'plugins'), { recursive: true })
writeFileSync(join(config, 'plugins', 'orca-opencode-status.js'), 'status entry')
writeFileSync(join(config, 'plugins', 'user.js'), 'user entry')
writeFileSync(join(config, 'opencode.json'), '{"model":"selected/model"}')
const env: Record<string, string> = {
ORCA_OPENCODE_PLUGIN_API: 'v2',
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'sparse-launch',
...(selection === 'explicit' ? { XDG_CONFIG_HOME: configHome } : {})
}
expect(installOpenCodeStartupPromptForLaunch(env)).toBe(true)
expect(
readFileSync(join(env.OPENCODE_CONFIG_DIR, 'plugins', 'orca-opencode-status.js'), 'utf8')
).toBe('status entry')
expect(readFileSync(join(env.OPENCODE_CONFIG_DIR, 'plugins', 'user.js'), 'utf8')).toBe(
'user entry'
)
expect(readFileSync(join(env.OPENCODE_CONFIG_DIR, 'opencode.json'), 'utf8')).toContain(
'selected/model'
)
}
)
it("uses the execution owner's resolved environment instead of reintroducing a deleted ambient XDG home", () => {
const selected = join(root, 'resolved-config')
mkdirSync(join(selected, 'opencode', 'plugins'), { recursive: true })
writeFileSync(join(selected, 'opencode', 'plugins', 'user.js'), 'resolved entry')
const env: Record<string, string> = {
ORCA_OPENCODE_PLUGIN_API: 'v2',
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'resolved-launch'
}
expect(installOpenCodeStartupPromptForLaunch(env, false, { XDG_CONFIG_HOME: selected })).toBe(
true
)
expect(readFileSync(join(env.OPENCODE_CONFIG_DIR, 'plugins', 'user.js'), 'utf8')).toBe(
'resolved entry'
)
expect(env.ORCA_OPENCODE_CONFIG_DIR).toBeUndefined()
})
it.each(['inherited', 'explicit'] as const)(
'preserves the %s OPENCODE_CONFIG_DIR ahead of the XDG default',
(selection) => {
const ambient = join(root, 'ambient-source')
const selected = join(root, 'selected-source')
for (const config of [ambient, selected]) {
mkdirSync(join(config, 'plugins'), { recursive: true })
writeFileSync(join(config, 'plugins', 'user.js'), config)
}
vi.stubEnv('OPENCODE_CONFIG_DIR', ambient)
const env: Record<string, string> = {
ORCA_OPENCODE_PLUGIN_API: 'v2',
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'custom-config-launch',
...(selection === 'explicit' ? { OPENCODE_CONFIG_DIR: selected } : {})
}
expect(installOpenCodeStartupPromptForLaunch(env)).toBe(true)
expect(readFileSync(join(env.OPENCODE_CONFIG_DIR, 'plugins', 'user.js'), 'utf8')).toBe(
selection === 'explicit' ? selected : ambient
)
}
)
it('keeps a missing user config untouched and installs the launch into an owned overlay', () => {
const source = join(root, 'missing-user-config')
const env: Record<string, string> = {
ORCA_OPENCODE_PLUGIN_API: 'v2',
ORCA_OPENCODE_STARTUP_PROMPT_NONCE: 'private-launch',
OPENCODE_CONFIG_DIR: source,
OPENCODE_CONFIG_CONTENT: '{"model":"opencode/model"}'
}
installOpenCodeStartupPromptForLaunch(env)
expect(existsSync(source)).toBe(false)
expect(env.OPENCODE_CONFIG_DIR).toContain(
join(root, 'profile', 'opencode-startup-prompt-overlays')
)
expect(env.ORCA_OPENCODE_CONFIG_DIR).toBe(env.OPENCODE_CONFIG_DIR)
expect(env.OPENCODE_CONFIG_CONTENT).toBe('{"model":"opencode/model"}')
expect(
existsSync(join(env.OPENCODE_CONFIG_DIR, 'plugins', 'orca-opencode-startup-prompt', 'tui.js'))
).toBe(true)
expect(existsSync(join(env.OPENCODE_CONFIG_DIR, 'plugins', 'orca-opencode-status.js'))).toBe(
false
)
})
it('installs only a TUI entry without installing status hooks or a v1/server entry', () => {
const service = createOpenCodeStartupPromptInstaller(() => 'prompt source')
expect(service.buildPtyEnv('pane')).toEqual({})
const plugins = join(root, 'config', 'opencode', 'plugins')
expect(readFileSync(join(plugins, 'orca-opencode-startup-prompt', 'tui.js'), 'utf8')).toBe(
'prompt source'
)
expect(existsSync(join(plugins, 'orca-opencode-startup-prompt.js'))).toBe(false)
expect(existsSync(join(plugins, 'orca-opencode-status.js'))).toBe(false)
expect(existsSync(join(root, 'profile', 'opencode-startup-prompt-hooks'))).toBe(false)
})
it('preserves user config and plugins across source-scoped overlay refreshes', () => {
const config = join(root, 'custom')
mkdirSync(join(config, 'plugins'), { recursive: true })
writeFileSync(join(config, 'opencode.json'), '{"model":"user/model"}')
writeFileSync(join(config, 'plugins', 'user.js'), 'user source')
mkdirSync(join(config, 'plugins', 'orca-opencode-startup-prompt'))
writeFileSync(
join(config, 'plugins', 'orca-opencode-startup-prompt', 'tui.js'),
'user collision'
)
let source = 'first prompt source'
const service = createOpenCodeStartupPromptInstaller(() => source)
const first = service.buildPtyEnv('pane-a', config).OPENCODE_CONFIG_DIR
expect(first).toBeDefined()
source = 'next prompt source'
expect(service.buildPtyEnv('pane-b', config).OPENCODE_CONFIG_DIR).toBe(first)
if (!first) {
throw new Error('Missing overlay')
}
expect(
readFileSync(join(first, 'plugins', 'orca-opencode-startup-prompt', 'tui.js'), 'utf8')
).toBe(source)
expect(readFileSync(join(first, 'opencode.json'), 'utf8')).toContain('user/model')
expect(readFileSync(join(first, 'plugins', 'user.js'), 'utf8')).toBe('user source')
expect(
readFileSync(join(config, 'plugins', 'orca-opencode-startup-prompt', 'tui.js'), 'utf8')
).toBe('user collision')
})
})