Files
ide/test/test.installer.node.ts
jkunz 6f32a206b4 Support remote project tabs with local OpenCode bridge
Keeps provider credentials local while executing OpenCode shell and file tools against the selected remote workspace over SSH.
2026-05-11 14:28:12 +00:00

225 lines
8.8 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as childProcess from 'node:child_process';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import {
createRemoteEphemeralBootstrapCommand,
createRemoteEphemeralReadinessCommand,
createRemoteEphemeralPortAllocationCommand,
createRemoteEphemeralRuntimeCacheCheckCommand,
createRemoteEphemeralRuntimeMarkCommand,
createRemoteOpenCodeToolCommand,
createRemoteProjectListCommand,
createRemoteProjectUpsertCommand,
remoteOpenCodeToolScript,
createRemoteBootstrapCommand,
createRemoteInstallCommand,
createRemoteServerInstallPlan,
joinRemotePath,
quoteRemotePath,
quoteShellArg,
} from '../packages/server-installer/ts/index.js';
tap.test('should create stable remote install paths', async () => {
const plan = createRemoteServerInstallPlan({
serverVersion: '0.1.0',
artifactName: 'remote-theia-linux-x64.tgz',
platform: 'linux',
arch: 'x64',
});
expect(plan.paths.versionRoot).toEqual('~/.git.zone/ide/server/0.1.0');
expect(plan.paths.currentLink).toEqual('~/.git.zone/ide/server/current');
expect(plan.manifest.protocolVersion).toEqual(1);
expect(plan.manifest.artifactName).toEqual('remote-theia-linux-x64.tgz');
});
tap.test('should quote shell arguments safely', async () => {
expect(quoteShellArg("that's it")).toEqual("'that'\"'\"'s it'");
expect(quoteRemotePath('~/.git.zone/ide/server')).toEqual('"$HOME"/\'.git.zone/ide/server\'');
expect(quoteRemotePath('$HOME/work/project')).toEqual('"$HOME"/\'work/project\'');
expect(joinRemotePath('~/.git.zone/', '/ide/', '/0.1.0')).toEqual('~/.git.zone/ide/0.1.0');
});
tap.test('should render install and bootstrap commands', async () => {
const plan = createRemoteServerInstallPlan({
serverVersion: '0.1.0',
artifactName: 'remote-theia.tgz',
});
const installCommand = createRemoteInstallCommand(plan);
const bootstrapCommand = createRemoteBootstrapCommand({
serverVersion: '0.1.0',
workspacePath: '/srv/work/project',
theiaPort: 33990,
opencodePort: 4096,
opencodeUsername: 'opencode',
opencodePassword: 'secret',
});
expect(installCommand).toInclude('GITZONE_IDE_MANIFEST');
expect(installCommand).toInclude('"$HOME"/\'.git.zone/ide/server/0.1.0\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_OPENCODE_PORT');
expect(bootstrapCommand).toInclude('GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART');
expect(bootstrapCommand).toInclude('pnpm --dir');
});
tap.test('should render remote home paths as expandable shell paths', async () => {
const bootstrapCommand = createRemoteBootstrapCommand({
serverVersion: '0.1.0',
workspacePath: '$HOME',
theiaPort: 33990,
opencodePort: 4096,
opencodeUsername: 'opencode',
opencodePassword: 'secret',
});
expect(bootstrapCommand).toInclude('test -d "$HOME"');
expect(bootstrapCommand).toInclude('export GITZONE_IDE_WORKSPACE="$HOME"');
expect(bootstrapCommand).toInclude('"$HOME"/\'.git.zone/ide/server/0.1.0/applications/remote-theia\'');
});
tap.test('should render ephemeral runtime bootstrap without remote pnpm', async () => {
const bootstrapCommand = createRemoteEphemeralBootstrapCommand({
serverVersion: '0.1.0',
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
workspacePath: '$HOME',
theiaPort: 33990,
opencodePort: 4096,
opencodeUsername: 'opencode',
opencodePassword: 'secret',
});
expect(bootstrapCommand).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
expect(bootstrapCommand).toInclude('lib/backend/main.js');
expect(bootstrapCommand).not.toInclude('pnpm');
expect(bootstrapCommand).toInclude('LD_LIBRARY_PATH');
expect(bootstrapCommand).toInclude('THEIA_CONFIG_DIR="$HOME"/\'.git.zone/ide/theia\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_DISABLE_OPENCODE_AUTOSTART=\'1\'');
expect(bootstrapCommand).toInclude('GITZONE_IDE_THEIA_COLOR_THEME=\'dark\'');
expect(bootstrapCommand).toInclude("settings['workbench.colorTheme'] = colorTheme");
expect(bootstrapCommand).toInclude('"$HOME"/\'.git.zone/ide/logs\'');
expect(bootstrapCommand).toInclude('runtimeRoot=');
});
tap.test('should render ephemeral readiness check with remote logs', async () => {
const readinessCommand = createRemoteEphemeralReadinessCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
theiaPort: 33990,
waitSeconds: 2,
});
expect(readinessCommand).toInclude('fetch');
expect(readinessCommand).toInclude('theia-33990.log');
expect(readinessCommand).toInclude('"$HOME"/\'.git.zone/ide/logs/theia-33990.log\'');
expect(readinessCommand).toInclude('LD_LIBRARY_PATH');
});
tap.test('should render ephemeral runtime cache check command', async () => {
const cacheCheckCommand = createRemoteEphemeralRuntimeCacheCheckCommand({
runtimeRoot: '/tmp/gitzone-ide-0.1.0-deadbeef',
runtimeSha256: 'deadbeef',
});
expect(cacheCheckCommand).toInclude('.gitzone-runtime-sha256');
expect(cacheCheckCommand).toInclude('test "$(cat');
expect(cacheCheckCommand).toInclude("'deadbeef'");
expect(cacheCheckCommand).toInclude('runtimeCache=hit');
});
tap.test('should render ephemeral runtime mark command', async () => {
const markCommand = createRemoteEphemeralRuntimeMarkCommand({
runtimeRoot: '/tmp/gitzone-ide-0.1.0-deadbeef',
runtimeSha256: 'deadbeef',
});
expect(markCommand).toInclude('.gitzone-runtime-sha256.tmp');
expect(markCommand).toInclude("printf '%s");
expect(markCommand).toInclude("'deadbeef'");
expect(markCommand).toInclude('runtimeCache=stored');
});
tap.test('should render remote port allocation command', async () => {
const portCommand = createRemoteEphemeralPortAllocationCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
count: 2,
});
expect(portCommand).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
expect(portCommand).toInclude('ports=');
expect(portCommand).toInclude('LD_LIBRARY_PATH');
});
tap.test('should render remote OpenCode tool bridge command', async () => {
const command = createRemoteOpenCodeToolCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
workspacePath: '$HOME/project',
toolName: 'read',
});
expect(command).toInclude('/tmp/gitzone-ide-runtime-test/node/bin/node');
expect(command).toInclude('GITZONE_IDE_TOOL_NAME=\'read\'');
expect(command).toInclude('GITZONE_IDE_WORKSPACE="$HOME"/\'project\'');
expect(command).toInclude('GITZONE_IDE_RG_PATH');
expect(command).toInclude('fs.readFileSync(0');
});
tap.test('should execute remote OpenCode tool script with stdin payloads', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-opencode-tool-'));
const filePath = path.join(tempDir, 'sample.txt');
await fs.writeFile(filePath, 'hello\nworld\n');
const readResult = runRemoteOpenCodeToolScript('read', tempDir, { filePath: 'sample.txt' });
expect(readResult.output).toInclude('1: hello');
const editResult = runRemoteOpenCodeToolScript('edit', tempDir, {
filePath: 'sample.txt',
oldString: 'world',
newString: 'remote',
});
expect(editResult.output).toInclude('Edit applied successfully');
expect(await fs.readFile(filePath, 'utf8')).toEqual('hello\nremote\n');
const patchResult = runRemoteOpenCodeToolScript('apply_patch', tempDir, {
patchText: '*** Begin Patch\n*** Add File: nested/new.txt\n+created remotely\n*** End Patch',
});
expect(patchResult.output).toInclude('A nested/new.txt');
expect(await fs.readFile(path.join(tempDir, 'nested', 'new.txt'), 'utf8')).toEqual('created remotely\n');
});
tap.test('should render remote project registry commands', async () => {
const listCommand = createRemoteProjectListCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
});
const upsertCommand = createRemoteProjectUpsertCommand({
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
projectPath: '$HOME/project',
title: 'Project',
});
expect(listCommand).toInclude('projects.json');
expect(listCommand).toInclude('{"projects":[]}');
expect(upsertCommand).toInclude('GITZONE_IDE_PROJECT_PATH="$HOME"/\'project\'');
expect(upsertCommand).toInclude('test -d "$GITZONE_IDE_PROJECT_PATH"');
expect(upsertCommand).toInclude('crypto.createHash');
});
const runRemoteOpenCodeToolScript = (toolName: string, workspacePath: string, args: Record<string, unknown>) => {
const result = childProcess.spawnSync(process.execPath, ['-e', remoteOpenCodeToolScript], {
input: JSON.stringify({ args }),
encoding: 'utf8',
env: {
...process.env,
GITZONE_IDE_TOOL_NAME: toolName,
GITZONE_IDE_WORKSPACE: workspacePath,
GITZONE_IDE_RG_PATH: '/not-found/rg',
},
});
if (result.status !== 0) {
throw new Error(result.stderr || `tool script failed with ${result.status}`);
}
return JSON.parse(result.stdout) as { output: string; metadata?: Record<string, unknown> };
};
export default tap.start();