Files
orca/src/relay/plugin-overlay.test.ts
T
NeilandXiro The Dev ee354a35d7 feat(agents): add OpenCode 2 beta support (#21418)
* feat(agents): add OpenCode 2 beta support

Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>

* fix(opencode2): support current plugin lifecycle and session storage

* fix(opencode2): preserve lifecycle ordering and full session capture

* test(opencode2): cover setup event bridge

* test(opencode2): cover setup event bridge

* test(browser): satisfy anti-slop naming check

* test(opencode2): cover live form lifecycle

* fix(relay): preserve OMP config directory selection

* test(opencode2): avoid assertions in bridge fixture

* fix(rebase): retain OMP resume and fresh launch behavior

* test: align upstream OMP resume expectations

* test(opencode2): verify rejected form closes waiting state

---------

Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>
2026-09-19 17:49:03 -07:00

433 lines
18 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { PluginOverlayManager } from './plugin-overlay'
import { resolvePiSourceAgentDir } from './plugin-overlay-env'
describe('PluginOverlayManager', () => {
let homeDir: string
let manager: PluginOverlayManager
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), 'plugin-overlay-'))
manager = new PluginOverlayManager({ homeDir })
})
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true })
})
it('reports no source until install runs', () => {
expect(manager.hasOpenCodeSource()).toBe(false)
expect(manager.hasPiSource()).toBe(false)
expect(manager.materializeOpenCode('tab-1:0')).toBeNull()
expect(manager.materializePi('tab-1:0')).toBeNull()
})
it('materializes OpenCode plugin into <overlay>/plugins/<file>', () => {
manager.setSources({ opencodePluginSource: 'export const X = 1' })
const dir = manager.materializeOpenCode('tab-1:0')
expect(dir).not.toBeNull()
const expected = join(dir!, 'plugins', 'orca-opencode-status.js')
expect(existsSync(expected)).toBe(true)
expect(readFileSync(expected, 'utf8')).toBe('export const X = 1')
})
it('keeps the OpenCode 2 plugin in a separate overlay and filename', () => {
manager.setSources({ opencode2PluginSource: 'export const V2 = 1' })
expect(manager.hasOpenCodeSource('opencode2')).toBe(true)
const dir = manager.materializeOpenCode('tab-2:0', undefined, 'opencode2')
expect(dir).not.toBeNull()
expect(readFileSync(join(dir!, 'plugins', 'orca-opencode2-status.js'), 'utf8')).toBe(
'export const V2 = 1'
)
expect(existsSync(join(dir!, 'plugins', 'orca-opencode-status.js'))).toBe(false)
})
it('mirrors a preexisting remote OpenCode config dir before adding Orca plugin', () => {
const userConfigDir = join(homeDir, 'company-opencode')
mkdirSync(join(userConfigDir, 'plugins'), { recursive: true })
writeFileSync(join(userConfigDir, 'opencode.json'), '{"provider":"custom"}')
writeFileSync(join(userConfigDir, 'plugins', 'user-plugin.js'), 'user plugin')
writeFileSync(join(userConfigDir, 'plugins', 'orca-opencode-status.js'), 'user same-name')
manager.setSources({ opencodePluginSource: 'orca plugin' })
const dir = manager.materializeOpenCode('tab-opencode:0', userConfigDir)
expect(dir).not.toBeNull()
expect(readFileSync(join(dir!, 'opencode.json'), 'utf8')).toBe('{"provider":"custom"}')
expect(readFileSync(join(dir!, 'plugins', 'user-plugin.js'), 'utf8')).toBe('user plugin')
expect(readFileSync(join(dir!, 'plugins', 'orca-opencode-status.js'), 'utf8')).toBe(
'orca plugin'
)
expect(readFileSync(join(userConfigDir, 'plugins', 'orca-opencode-status.js'), 'utf8')).toBe(
'user same-name'
)
})
it('does not override a missing preexisting OpenCode config dir', () => {
manager.setSources({ opencodePluginSource: 'orca plugin' })
expect(manager.materializeOpenCode('tab-missing:0', join(homeDir, 'missing'))).toBeNull()
})
it('installs Pi extension into the real agent extensions dir', () => {
manager.setSources({ piExtensionSource: '// pi extension' })
const result = manager.materializePi('tab-2:0')
expect(result?.sourceAgentDir).toBeDefined()
const file = join(result!.sourceAgentDir!, 'extensions', 'orca-agent-status.ts')
expect(result?.statusExtensionPath).toBe(file)
expect(existsSync(file)).toBe(true)
expect(readFileSync(file, 'utf8')).toContain('@orca-managed-pi-extension')
})
it("does not overwrite a user's same-named remote Pi extension file", () => {
const piAgentDir = join(homeDir, '.pi', 'agent')
const extensionFile = join(piAgentDir, 'extensions', 'orca-agent-status.ts')
mkdirSync(join(piAgentDir, 'extensions'), { recursive: true })
writeFileSync(extensionFile, 'user-owned remote status extension')
manager.setSources({ piExtensionSource: '// pi extension' })
expect(manager.materializePi('tab-user-owned-pi:0')).toBeNull()
expect(readFileSync(extensionFile, 'utf8')).toBe('user-owned remote status extension')
})
it('uses the kind-specific Pi-compatible extension source when available', () => {
manager.setSources({
piExtensionSource: '// pi extension',
ompExtensionSource: '// omp extension'
})
const piResult = manager.materializePi('tab-kind-pi:0', undefined, 'pi')
const ompResult = manager.materializePi('tab-kind-omp:0', undefined, 'omp')
expect(piResult?.sourceAgentDir).toBeDefined()
expect(ompResult?.sourceAgentDir).toBeDefined()
expect(
readFileSync(join(piResult!.sourceAgentDir!, 'extensions', 'orca-agent-status.ts'), 'utf8')
).toContain('// pi extension')
expect(
readFileSync(join(ompResult!.sourceAgentDir!, 'extensions', 'orca-agent-status.ts'), 'utf8')
).toContain('// omp extension')
})
it('uses only the Prime-specific source in the default Prime agent dir', () => {
manager.setSources({ piExtensionSource: '// pi extension' })
expect(manager.materializePi('tab-prime-missing:0', undefined, 'prime-agent')).toBeNull()
manager.setSources({ primeAgentExtensionSource: '// prime extension' })
const result = manager.materializePi('tab-prime:0', undefined, 'prime-agent')
expect(result?.sourceAgentDir).toBe(join(homeDir, '.prime', 'agent'))
expect(readFileSync(result!.statusExtensionPath!, 'utf8')).toContain('// prime extension')
expect(readFileSync(result!.statusExtensionPath!, 'utf8')).not.toContain('// pi extension')
})
it('installs Orca status extension into the remote default Pi agent dir', () => {
const piAgentDir = join(homeDir, '.pi', 'agent')
mkdirSync(join(piAgentDir, 'skills', 'my-skill'), { recursive: true })
mkdirSync(join(piAgentDir, 'extensions', 'user-ext'), { recursive: true })
writeFileSync(join(piAgentDir, 'auth.json'), 'secret token')
writeFileSync(join(piAgentDir, 'skills', 'my-skill', 'SKILL.md'), 'critical user skill')
writeFileSync(join(piAgentDir, 'extensions', 'user-ext', 'ext.ts'), 'user extension')
writeFileSync(
join(piAgentDir, 'settings.json'),
JSON.stringify({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
terminal: {
showImages: false,
clearOnShrink: false
}
})
)
manager.setSources({ piExtensionSource: '// pi extension' })
const result = manager.materializePi('tab-pi:0')
const dir = result?.sourceAgentDir
expect(dir).toBeDefined()
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('secret token')
expect(readFileSync(join(dir!, 'skills', 'my-skill', 'SKILL.md'), 'utf8')).toBe(
'critical user skill'
)
expect(readFileSync(join(dir!, 'extensions', 'user-ext', 'ext.ts'), 'utf8')).toBe(
'user extension'
)
expect(readdirSync(join(dir!, 'extensions')).sort()).toEqual([
'orca-agent-status.ts',
'user-ext'
])
expect(JSON.parse(readFileSync(join(dir!, 'settings.json'), 'utf8'))).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
terminal: {
showImages: false,
clearOnShrink: false
}
})
expect(JSON.parse(readFileSync(join(piAgentDir, 'settings.json'), 'utf8'))).toEqual({
defaultProvider: 'amazon-bedrock',
hideThinkingBlock: false,
terminal: {
showImages: false,
clearOnShrink: false
}
})
})
it('mirrors a preexisting remote Pi agent dir instead of the default', () => {
const defaultAgentDir = join(homeDir, '.pi', 'agent')
const customAgentDir = join(homeDir, 'custom-pi-agent')
mkdirSync(defaultAgentDir, { recursive: true })
mkdirSync(join(customAgentDir, 'extensions'), { recursive: true })
writeFileSync(join(defaultAgentDir, 'auth.json'), 'default token')
writeFileSync(join(customAgentDir, 'auth.json'), 'custom token')
writeFileSync(join(customAgentDir, 'extensions', 'custom.ts'), 'custom extension')
manager.setSources({ piExtensionSource: '// pi extension' })
const result = manager.materializePi('tab-custom-pi:0', customAgentDir)
const dir = result?.sourceAgentDir
expect(dir).toBeDefined()
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('custom token')
expect(readFileSync(join(dir!, 'extensions', 'custom.ts'), 'utf8')).toBe('custom extension')
expect(readFileSync(join(dir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toContain(
'// pi extension'
)
})
it('leaves lazy OMP agent.db in the real remote home on the relay', () => {
manager.setSources({ piExtensionSource: '// pi extension' })
const sourceDir = join(homeDir, '.omp', 'agent')
const first = manager.materializePi('tab-relay-omp-sqlite:0', undefined, 'omp')
expect(first?.sourceAgentDir).toBe(sourceDir)
const sourcePath = join(sourceDir, 'agent.db')
const content = 'agent.db relay credentials'
expect(existsSync(sourcePath)).toBe(false)
expect(existsSync(join(homeDir, '.orca-relay', 'omp-overlays'))).toBe(false)
expect(existsSync(join(sourceDir, 'history.db'))).toBe(false)
writeFileSync(sourcePath, content)
expect(readFileSync(sourcePath, 'utf8')).toBe(content)
const second = manager.materializePi('tab-relay-omp-sqlite:0', undefined, 'omp')
expect(second?.sourceAgentDir).toBe(first?.sourceAgentDir)
expect(readFileSync(join(second!.sourceAgentDir!, 'agent.db'), 'utf8')).toBe(content)
})
// Why: per-agent source dir. The renderer picks Pi or OMP per
// launch, and the relay must use the right `~/.<kind>/agent` source —
// disk-presence guessing (always-Pi or first-exists) shadows the other
// agent's user extensions when both dirs exist on the remote disk.
describe('per-agent default source dir (no cross-agent fallback)', () => {
function seedAgentDir(dotDir: '.pi' | '.omp', tag: string): string {
const agentDir = join(homeDir, dotDir, 'agent')
mkdirSync(join(agentDir, 'extensions', `${tag}-ext`), { recursive: true })
writeFileSync(join(agentDir, 'extensions', `${tag}-ext`, 'ext.ts'), `${tag} extension`)
writeFileSync(join(agentDir, 'auth.json'), `${tag} token`)
return agentDir
}
it('launching pi with both ~/.pi/agent and ~/.omp/agent present installs into ~/.pi/agent', () => {
seedAgentDir('.pi', 'pi')
seedAgentDir('.omp', 'omp')
manager.setSources({ piExtensionSource: '// pi extension' })
const result = manager.materializePi('tab-relay-pi-both:0', undefined, 'pi')
const dir = result?.sourceAgentDir
expect(dir).toBe(join(homeDir, '.pi', 'agent'))
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('pi token')
const extensions = readdirSync(join(dir!, 'extensions')).sort()
expect(extensions).toContain('pi-ext')
expect(extensions).toContain('orca-agent-status.ts')
expect(extensions).not.toContain('omp-ext')
})
it('launching omp with both ~/.pi/agent and ~/.omp/agent present installs into ~/.omp/agent', () => {
seedAgentDir('.pi', 'pi')
seedAgentDir('.omp', 'omp')
manager.setSources({ piExtensionSource: '// pi extension' })
const result = manager.materializePi('tab-relay-omp-both:0', undefined, 'omp')
const dir = result?.sourceAgentDir
expect(dir).toBe(join(homeDir, '.omp', 'agent'))
// Even though ~/.pi/agent exists, the OMP launch MUST mirror OMP's
// source dir. Cross-agent fallback would silently shadow the user's
// OMP extensions on the remote.
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('omp token')
const extensions = readdirSync(join(dir!, 'extensions')).sort()
expect(extensions).toContain('omp-ext')
expect(extensions).toContain('orca-agent-status.ts')
expect(extensions).not.toContain('pi-ext')
})
it('launching omp when only ~/.pi/agent exists does NOT mirror Pi state', () => {
// Why: missing OMP source dir on the remote must create only OMP's
// own extension dir. Pi state must never cross-pollinate in.
seedAgentDir('.pi', 'pi')
expect(existsSync(join(homeDir, '.omp'))).toBe(false)
expect(existsSync(join(homeDir, '.prime'))).toBe(false)
manager.setSources({ piExtensionSource: '// pi extension' })
const result = manager.materializePi('tab-relay-omp-empty:0', undefined, 'omp')
const dir = result?.sourceAgentDir
expect(dir).toBe(join(homeDir, '.omp', 'agent'))
// Pi-only home must NOT leak into the OMP home.
expect(existsSync(join(dir!, 'auth.json'))).toBe(false)
const extensions = readdirSync(join(dir!, 'extensions')).sort()
expect(extensions).toEqual(['orca-agent-status.ts'])
})
it('bare-shell prep does not create missing remote agent homes (#10196)', () => {
expect(existsSync(join(homeDir, '.pi'))).toBe(false)
expect(existsSync(join(homeDir, '.omp'))).toBe(false)
manager.setSources({
piExtensionSource: '// pi extension',
ompExtensionSource: '// omp extension',
primeAgentExtensionSource: '// prime extension'
})
expect(
manager.materializePi('tab-bare-pi:0', undefined, 'pi', {
materializeDefaultHome: false
})
).toBeNull()
const bareOmp = manager.materializePi('tab-bare-omp:0', undefined, 'omp', {
materializeDefaultHome: false
})
// Why: bare OMP keeps status via ~/.orca-relay/… without SOURCE_AGENT_DIR or ~/.omp.
expect(bareOmp?.sourceAgentDir).toBeUndefined()
expect(bareOmp?.statusExtensionPath).toEqual(
expect.stringContaining(join('.orca-relay', 'omp-managed-status-extension'))
)
expect(existsSync(bareOmp!.statusExtensionPath!)).toBe(true)
expect(readFileSync(bareOmp!.statusExtensionPath!, 'utf8')).toContain('// omp extension')
expect(existsSync(join(homeDir, '.pi'))).toBe(false)
expect(existsSync(join(homeDir, '.omp'))).toBe(false)
expect(
manager.materializePi('tab-bare-prime:0', undefined, 'prime-agent', {
materializeDefaultHome: false
})
).toBeNull()
expect(existsSync(join(homeDir, '.prime'))).toBe(false)
})
})
it('does not override a missing preexisting Pi agent dir', () => {
manager.setSources({ piExtensionSource: '// pi extension' })
expect(manager.materializePi('tab-missing-pi:0', join(homeDir, 'missing-pi'))).toBeNull()
})
it('clearOverlay removes OpenCode overlays without deleting real Pi/OMP homes', () => {
manager.setSources({
opencodePluginSource: 'opencode',
opencode2PluginSource: 'opencode2',
piExtensionSource: 'pi',
ompExtensionSource: 'omp'
})
const opencodeDir = manager.materializeOpenCode('tab-3:0')!
const opencode2Dir = manager.materializeOpenCode('tab-3:0', undefined, 'opencode2')!
const piDir = manager.materializePi('tab-3:0', undefined, 'pi')!.sourceAgentDir!
const ompDir = manager.materializePi('tab-3:0', undefined, 'omp')!.sourceAgentDir!
expect(piDir).not.toBe(ompDir)
expect(existsSync(opencodeDir)).toBe(true)
expect(existsSync(opencode2Dir)).toBe(true)
expect(existsSync(piDir)).toBe(true)
expect(existsSync(ompDir)).toBe(true)
manager.clearOverlay('tab-3:0')
expect(existsSync(opencodeDir)).toBe(false)
expect(existsSync(opencode2Dir)).toBe(false)
expect(existsSync(piDir)).toBe(true)
expect(existsSync(ompDir)).toBe(true)
})
it.skipIf(process.platform === 'win32')(
'clearOverlay removes OpenCode overlay symlinks without deleting their targets',
() => {
const userConfigDir = join(homeDir, 'company-opencode')
const linkedTarget = join(homeDir, 'linked-plugin-target')
mkdirSync(join(userConfigDir, 'plugins'), { recursive: true })
mkdirSync(linkedTarget, { recursive: true })
writeFileSync(join(linkedTarget, 'keep.js'), 'do not delete')
symlinkSync(linkedTarget, join(userConfigDir, 'plugins', 'linked-plugin'), 'dir')
manager.setSources({ opencodePluginSource: 'orca plugin' })
const dir = manager.materializeOpenCode('tab-opencode-symlink:0', userConfigDir)!
expect(existsSync(join(dir, 'plugins', 'linked-plugin'))).toBe(true)
manager.clearOverlay('tab-opencode-symlink:0')
expect(existsSync(dir)).toBe(false)
expect(readFileSync(join(linkedTarget, 'keep.js'), 'utf8')).toBe('do not delete')
}
)
it('produces stable overlay dirs for a given id (idempotent re-materialization)', () => {
manager.setSources({ opencodePluginSource: 'first' })
const dirA = manager.materializeOpenCode('tab-stable:0')!
manager.setSources({ opencodePluginSource: 'second' })
const dirB = manager.materializeOpenCode('tab-stable:0')!
expect(dirA).toBe(dirB)
expect(readFileSync(join(dirA, 'plugins', 'orca-opencode-status.js'), 'utf8')).toBe('second')
})
it('hashes unsafe pane ids into portable overlay directory names', () => {
manager.setSources({ opencodePluginSource: 'plugin' })
const dir = manager.materializeOpenCode('tab/with\\unsafe:chars\n0')
expect(dir).not.toBeNull()
expect(basename(dir!)).toMatch(/^[a-f0-9]{32}$/)
expect(dir).not.toContain('tab/with')
expect(existsSync(join(dir!, 'plugins', 'orca-opencode-status.js'))).toBe(true)
})
})
describe('resolvePiSourceAgentDir', () => {
it('uses only the selected kind source shadow when resolving inherited overlays', () => {
const env = {
HOME: mkdtempSync(join(tmpdir(), 'plugin-overlay-env-')),
PI_CODING_AGENT_DIR: '/tmp/parent-orca-pi-overlay',
ORCA_PI_CODING_AGENT_DIR: '/tmp/parent-orca-pi-overlay',
ORCA_PI_SOURCE_AGENT_DIR: '/user/.pi/agent'
}
try {
expect(resolvePiSourceAgentDir(env, undefined, 'pi')).toBe('/user/.pi/agent')
expect(resolvePiSourceAgentDir(env, undefined, 'omp')).toBeUndefined()
} finally {
rmSync(env.HOME, { recursive: true, force: true })
}
})
it('keeps explicit PI_CODING_AGENT_DIR values when they are not Orca overlays', () => {
const env = {
HOME: mkdtempSync(join(tmpdir(), 'plugin-overlay-env-')),
PI_CODING_AGENT_DIR: '/user/custom-omp-agent',
ORCA_PI_SOURCE_AGENT_DIR: '/user/.pi/agent'
}
try {
expect(resolvePiSourceAgentDir(env, undefined, 'omp')).toBe('/user/custom-omp-agent')
} finally {
rmSync(env.HOME, { recursive: true, force: true })
}
})
})