From 57bf732a425b111015bc9da6910a25ff80b6eaee Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:08:34 -0700 Subject: [PATCH] test(opencode): cover the opencode2 host-env branch and stop inheriting ORCA_OPENCODE_AGENT (#22547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(opencode): pin per-major OpenCode overlay selection on WSL and the relay OpenCode 2 dies with "Duplicate plugin ID" when two plugin files share an id, so which overlay a guest or remote pane is pointed at decides whether the agent starts. Nothing failed if that selection regressed: - the WSL spawn path never asserted which major it asks the guest relay for, and the shared pty-ipc mock had no openCode2HookService at all, so no test could reach the opencode2 branch of buildPtyHostEnv; - requestGuestOpenCodeOverlayDir had no coverage for the v2 guest dir; - PluginOverlayManager had no case for a remote config root that still holds the other major's stale Orca plugin. Tests only; no behavior change. Each new case was mutation-checked against the production line it guards. * test(opencode): stop the plugin contract test inheriting ORCA_OPENCODE_AGENT The generated plugin self-disables when ORCA_OPENCODE_AGENT names a different major, and the contract test saved and restored that variable without ever setting it. Run from a shell that has it — which is any shell inside an Orca OpenCode pane, i.e. how this repo is usually developed — the plugin returned an empty hook set and the contract failed for the wrong reason. Delete it in beforeEach, the way the opencode2 setup test already pins it. Verified the file passes with the variable set to either major and unset; before this it failed for two of the three. --- .../wsl-guest-plugin-install.test.ts | 26 ++++++++++++++ .../pty-daemon-spawn-agent-home-env.test.ts | 36 +++++++++++++++++++ src/main/ipc/pty-ipc-mock-registry.ts | 7 ++++ src/main/ipc/pty-ipc-suite-environment.ts | 10 ++++++ .../hook-plugin-module-contract.test.ts | 4 +++ src/relay/plugin-overlay.test.ts | 22 ++++++++++++ 6 files changed, 105 insertions(+) diff --git a/src/main/agent-hooks/wsl-guest-plugin-install.test.ts b/src/main/agent-hooks/wsl-guest-plugin-install.test.ts index 11ec2cae20b..c1a22cf1e96 100644 --- a/src/main/agent-hooks/wsl-guest-plugin-install.test.ts +++ b/src/main/agent-hooks/wsl-guest-plugin-install.test.ts @@ -26,6 +26,32 @@ describe('requestGuestOpenCodeOverlayDir', () => { }) }) + // Why: each OpenCode major gets its own guest overlay. Collapsing them here would + // point an OpenCode 2 pane at the v1 overlay, whose plugin gates itself off. + it('carries the per-major guest overlay dirs independently', async () => { + const { mux } = fakeMux(async () => ({ + overlayDirs: { + opencode: '/home/jin/.orca-relay/opencode-overlays/x', + opencode2: '/home/jin/.orca-relay/opencode2-overlays/y' + } + })) + await expect(requestGuestOpenCodeOverlayDir(mux, deps(), 'Ubuntu')).resolves.toEqual({ + kind: 'dir', + dir: '/home/jin/.orca-relay/opencode-overlays/x', + dir2: '/home/jin/.orca-relay/opencode2-overlays/y' + }) + }) + + it('reports an OpenCode 2 overlay even when the v1 overlay is missing', async () => { + const { mux } = fakeMux(async () => ({ + overlayDirs: { opencode2: '/home/jin/.orca-relay/opencode2-overlays/y' } + })) + await expect(requestGuestOpenCodeOverlayDir(mux, deps(), 'Ubuntu')).resolves.toEqual({ + kind: 'dir', + dir2: '/home/jin/.orca-relay/opencode2-overlays/y' + }) + }) + it("reports 'none' when the guest answered but materialization produced no dir", async () => { // Why: distinct from 'unavailable' — the caller must CLEAR a previously recorded // dir here, since a rebuild that failed after wiping leaves it plugin-less. diff --git a/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts b/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts index 1170c8425ca..f73f11e7f93 100644 --- a/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts +++ b/src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts @@ -330,6 +330,42 @@ describe('registerPtyHandlers', () => { spy.mockRestore() } }) + // Why: the guest materializes one overlay per OpenCode major, each holding only its + // own plugin file. Asking for the wrong one hands the guest the other variant's + // plugin, whose agent gate then registers no hooks at all. + const guestOverlayCases: { + launchAgent?: TuiAgent + expectedAgent: 'opencode' | 'opencode2' + }[] = [ + { expectedAgent: 'opencode' }, + { launchAgent: 'opencode2', expectedAgent: 'opencode2' } + ] + it.each(guestOverlayCases)( + 'selects the $expectedAgent guest overlay for a WSL spawn', + async ({ launchAgent, expectedAgent }) => { + const guestDirs = { + opencode: '/home/jin/.orca-relay/opencode-overlays/abc', + opencode2: '/home/jin/.orca-relay/opencode2-overlays/def' + } + const spy = vi + .spyOn(wslHookRelayManager, 'getOpenCodeOverlayDir') + .mockImplementation((_distro, agent = 'opencode') => guestDirs[agent]) + try { + await withWin32Platform(async () => { + const env = await daemonSpawnAndGetEnv({}, undefined, undefined, undefined, { + shellOverride: 'wsl.exe', + ...(launchAgent ? { launchAgent } : {}) + }) + expect(spy.mock.calls.map(([, agent]) => agent)).toEqual([expectedAgent]) + expect(env.ORCA_OPENCODE_AGENT).toBe(expectedAgent) + expect(env.OPENCODE_CONFIG_DIR).toBe(guestDirs[expectedAgent]) + expect(env.ORCA_OPENCODE_CONFIG_DIR).toBe(guestDirs[expectedAgent]) + }) + } finally { + spy.mockRestore() + } + } + ) it('strips the daemon-inherited Orca-owned CODEX_HOME for real-home routing', async () => { const spawnOptions = await daemonSpawnAndGetOptions( {}, diff --git a/src/main/ipc/pty-ipc-mock-registry.ts b/src/main/ipc/pty-ipc-mock-registry.ts index 11afd2df60a..71b77621b75 100644 --- a/src/main/ipc/pty-ipc-mock-registry.ts +++ b/src/main/ipc/pty-ipc-mock-registry.ts @@ -23,6 +23,7 @@ export const getPathMock: Mock = vi.fn() export const loginPreflightExecFileMock: Mock = vi.fn() export const spawnMock: Mock = vi.fn() export const openCodeBuildPtyEnvMock: Mock = vi.fn() +export const openCode2BuildPtyEnvMock: Mock = vi.fn() export const mimoCodeBuildPtyEnvMock: Mock = vi.fn() export const isPwshAvailableMock: Mock = vi.fn() export const wslUncDirectoryExistsAsyncMock: Mock = vi.fn() @@ -108,6 +109,12 @@ export const openCodeHookServiceModuleMock = () => ({ openCodeHookService: { buildPtyEnv: openCodeBuildPtyEnvMock, clearPty: openCodeClearPtyMock + }, + // Separate mock per variant: assembly.ts picks the service by variant, and a shared + // mock would hide a regression that hands an OpenCode 2 pane the v1 plugin. + openCode2HookService: { + buildPtyEnv: openCode2BuildPtyEnvMock, + clearPty: openCodeClearPtyMock } }) diff --git a/src/main/ipc/pty-ipc-suite-environment.ts b/src/main/ipc/pty-ipc-suite-environment.ts index d372ecb734b..c7256a57f3e 100644 --- a/src/main/ipc/pty-ipc-suite-environment.ts +++ b/src/main/ipc/pty-ipc-suite-environment.ts @@ -22,6 +22,7 @@ import { loginPreflightExecFileMock, spawnMock, openCodeBuildPtyEnvMock, + openCode2BuildPtyEnvMock, mimoCodeBuildPtyEnvMock, openCodeClearPtyMock, buildAgentHookEnvMock, @@ -149,6 +150,7 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment { loginPreflightExecFileMock.mockReset() spawnMock.mockReset() openCodeBuildPtyEnvMock.mockReset() + openCode2BuildPtyEnvMock.mockReset() mimoCodeBuildPtyEnvMock.mockReset() openCodeClearPtyMock.mockReset() buildAgentHookEnvMock.mockReset() @@ -218,6 +220,14 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment { ? '/tmp/orca-opencode-overlay' : '/tmp/orca-opencode-config' })) + openCode2BuildPtyEnvMock.mockImplementation((_ptyId: string, existingConfigDir?: string) => ({ + ORCA_OPENCODE_HOOK_PORT: '4567', + ORCA_OPENCODE_HOOK_TOKEN: 'opencode2-token', + ORCA_OPENCODE_PTY_ID: 'test-pty', + OPENCODE_CONFIG_DIR: existingConfigDir + ? '/tmp/orca-opencode2-overlay' + : '/tmp/orca-opencode2-config' + })) mimoCodeBuildPtyEnvMock.mockImplementation((_ptyId: string, existingHome?: string) => ({ MIMOCODE_HOME: existingHome ? '/tmp/orca-mimocode-overlay' : '/tmp/orca-mimocode-shared' })) diff --git a/src/main/opencode/hook-plugin-module-contract.test.ts b/src/main/opencode/hook-plugin-module-contract.test.ts index 6b3ca68ae5c..668a879f76a 100644 --- a/src/main/opencode/hook-plugin-module-contract.test.ts +++ b/src/main/opencode/hook-plugin-module-contract.test.ts @@ -52,6 +52,10 @@ describe('OpenCode status plugin module contract', () => { for (const key of ENV_KEYS) { savedEnv[key] = process.env[key] } + // Why: the generated plugin self-disables when this names a different major, + // so an inherited value from the developer's own Orca pane would leave + // `hooks.event` undefined and fail the contract for the wrong reason. + delete process.env.ORCA_OPENCODE_AGENT delete process.env.ORCA_AGENT_HOOK_ENDPOINT process.env.ORCA_AGENT_HOOK_PORT = '59999' process.env.ORCA_AGENT_HOOK_TOKEN = 'test-token' diff --git a/src/relay/plugin-overlay.test.ts b/src/relay/plugin-overlay.test.ts index c7c76cb08fb..14feb631876 100644 --- a/src/relay/plugin-overlay.test.ts +++ b/src/relay/plugin-overlay.test.ts @@ -95,6 +95,28 @@ describe('PluginOverlayManager', () => { ) }) + // Why: the remote/guest config root keeps whichever Orca plugin files earlier + // launches installed. Mirroring the other major's file into this overlay would + // hand the agent a plugin whose variant gate registers nothing. + it.each([ + { agent: 'opencode', stale: 'orca-opencode2-status.js', own: 'orca-opencode-status.js' }, + { agent: 'opencode2', stale: 'orca-opencode-status.js', own: 'orca-opencode2-status.js' } + ] as const)( + "keeps the other major's stale plugin out of the $agent overlay", + ({ agent, stale, own }) => { + const userConfigDir = join(homeDir, '.config', 'opencode') + mkdirSync(join(userConfigDir, 'plugins'), { recursive: true }) + writeFileSync(join(userConfigDir, 'plugins', stale), 'stale other-major plugin') + writeFileSync(join(userConfigDir, 'plugins', 'user-plugin.js'), 'user plugin') + + manager.setSources({ opencodePluginSource: 'v1', opencode2PluginSource: 'v2' }) + const dir = manager.materializeOpenCode('tab-1:0', userConfigDir, agent) + + expect(dir).not.toBeNull() + expect(readdirSync(join(dir!, 'plugins')).sort()).toEqual([own, 'user-plugin.js'].sort()) + } + ) + it('does not override a missing preexisting OpenCode config dir', () => { manager.setSources({ opencodePluginSource: 'orca plugin' })