agent-ecosystem/test/main/services/runtime/OpenCodeRuntimeNvmResolution.safe-e2e.test.ts
2026-05-21 13:38:40 +03:00

122 lines
4 KiB
TypeScript

// @vitest-environment node
/* eslint-disable security/detect-non-literal-fs-filename */
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
OpenCodeRuntimeInstallerService,
resolveVerifiedOpenCodeRuntimeBinaryPath,
} from '../../../../src/main/services/infrastructure/OpenCodeRuntimeInstallerService';
import { ensureOpenCodeBridgeRuntimeBinaryEnv } from '../../../../src/main/services/runtime/openCodeBridgeRuntimeEnv';
import { execCli } from '../../../../src/main/utils/childProcess';
import { setAppDataBasePath } from '../../../../src/main/utils/pathDecoder';
import { clearShellEnvCache } from '../../../../src/main/utils/shellEnv';
const describePosix = process.platform === 'win32' ? describe.skip : describe;
describePosix('OpenCode nvm runtime resolution safe e2e', () => {
let tempDir: string | null = null;
let originalHome: string | undefined;
let originalPath: string | undefined;
let originalShell: string | undefined;
beforeEach(async () => {
tempDir = await mkdtemp(path.join(os.tmpdir(), 'opencode-nvm-resolution-e2e-'));
setAppDataBasePath(path.join(tempDir, 'app-data'));
clearShellEnvCache();
originalHome = process.env.HOME;
originalPath = process.env.PATH;
originalShell = process.env.SHELL;
process.env.HOME = tempDir;
process.env.PATH = '';
process.env.SHELL = path.join(tempDir, 'missing-shell');
});
afterEach(async () => {
clearShellEnvCache();
setAppDataBasePath(null);
restoreEnvValue('HOME', originalHome);
restoreEnvValue('PATH', originalPath);
restoreEnvValue('SHELL', originalShell);
if (tempDir) {
await rm(tempDir, { recursive: true, force: true });
tempDir = null;
}
});
it('reports and launches an npm global OpenCode binary installed under nvm when GUI PATH is empty', async () => {
await createFakeNvmOpenCodeBinary('v23.0.0', { broken: true });
const binaryPath = await createFakeNvmOpenCodeBinary('v22.22.1');
const binDir = path.dirname(binaryPath);
await expect(resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 })).resolves.toBe(
binaryPath
);
await expect(new OpenCodeRuntimeInstallerService().getStatus()).resolves.toMatchObject({
installed: true,
source: 'path',
state: 'ready',
binaryPath,
version: 'opencode 1.15.6',
});
const bridgeEnv: NodeJS.ProcessEnv = { PATH: '' };
await ensureOpenCodeBridgeRuntimeBinaryEnv({
targetEnv: bridgeEnv,
bridgeEnv,
resolveVerifiedOpenCodeRuntimeBinaryPath,
});
expect(bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
expect(bridgeEnv.OPENCODE_BIN_PATH).toBe(binaryPath);
expect(bridgeEnv.PATH?.split(path.delimiter)[0]).toBe(binDir);
const version = await execCli('opencode', ['--version'], {
env: bridgeEnv,
timeout: 2_000,
windowsHide: true,
});
expect(version.stdout.trim()).toBe('opencode 1.15.6');
});
async function createFakeNvmOpenCodeBinary(
version: string,
options: { broken?: boolean } = {}
): Promise<string> {
const binDir = path.join(tempDir!, '.nvm', 'versions', 'node', version, 'bin');
const binaryPath = path.join(binDir, 'opencode');
await mkdir(binDir, { recursive: true });
await writeFile(
binaryPath,
options.broken
? ['#!/bin/sh', 'echo "broken opencode" >&2', 'exit 2'].join('\n')
: [
'#!/bin/sh',
'if [ "$1" = "--version" ]; then',
' echo "opencode 1.15.6"',
' exit 0',
'fi',
'echo "unexpected opencode args: $*" >&2',
'exit 2',
].join('\n'),
'utf8'
);
await chmod(binaryPath, 0o755);
return binaryPath;
}
});
function restoreEnvValue(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}