Replace OpenCode with SmartAgent runtime
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { GitZoneAgentRuntime, type IAgentEventEnvelope, type IAgentProjectContext } from '../applications/electron-shell/ts/agent-runtime.js';
|
||||
|
||||
const createProjectContext = (instanceId: string): IAgentProjectContext => ({
|
||||
instanceId,
|
||||
title: 'Persisted Project',
|
||||
path: '/srv/work/persisted-project',
|
||||
runtimeRoot: '/tmp/gitzone-ide-runtime-test',
|
||||
target: {
|
||||
id: 'dev-box',
|
||||
hostAlias: 'dev-box',
|
||||
hostName: 'dev.example.com',
|
||||
user: 'deploy',
|
||||
port: 2222,
|
||||
},
|
||||
batchMode: true,
|
||||
});
|
||||
|
||||
tap.test('should persist agent sessions by remote project context', async () => {
|
||||
const persistenceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-agent-runtime-'));
|
||||
try {
|
||||
const events: IAgentEventEnvelope[] = [];
|
||||
const firstRuntime = new GitZoneAgentRuntime(
|
||||
(payload) => events.push(payload),
|
||||
(instanceId) => createProjectContext(instanceId),
|
||||
persistenceRoot,
|
||||
);
|
||||
const createdSession = await firstRuntime.createSession({
|
||||
instanceId: 'first-instance',
|
||||
title: 'Persisted Chat',
|
||||
});
|
||||
firstRuntime.dispose();
|
||||
|
||||
const secondRuntime = new GitZoneAgentRuntime(
|
||||
() => undefined,
|
||||
(instanceId) => createProjectContext(instanceId),
|
||||
persistenceRoot,
|
||||
);
|
||||
const sessions = await secondRuntime.listSessions({ instanceId: 'second-instance' });
|
||||
secondRuntime.dispose();
|
||||
|
||||
expect(createdSession.title).toEqual('Persisted Chat');
|
||||
expect(events.some((payload) => payload.event.type === 'session.created')).toEqual(true);
|
||||
expect(sessions).toHaveLength(1);
|
||||
expect(sessions[0]!.id).toEqual(createdSession.id);
|
||||
expect(sessions[0]!.title).toEqual('Persisted Chat');
|
||||
} finally {
|
||||
await fs.rm(persistenceRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -1,18 +1,12 @@
|
||||
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,
|
||||
@@ -52,15 +46,10 @@ tap.test('should render install and bootstrap commands', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -69,9 +58,6 @@ tap.test('should render remote home paths as expandable shell paths', async () =
|
||||
serverVersion: '0.1.0',
|
||||
workspacePath: '$HOME',
|
||||
theiaPort: 33990,
|
||||
opencodePort: 4096,
|
||||
opencodeUsername: 'opencode',
|
||||
opencodePassword: 'secret',
|
||||
});
|
||||
|
||||
expect(bootstrapCommand).toInclude('test -d "$HOME"');
|
||||
@@ -85,9 +71,6 @@ tap.test('should render ephemeral runtime bootstrap without remote pnpm', async
|
||||
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');
|
||||
@@ -95,7 +78,6 @@ tap.test('should render ephemeral runtime bootstrap without remote pnpm', async
|
||||
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\'');
|
||||
@@ -150,43 +132,6 @@ tap.test('should render remote port allocation command', async () => {
|
||||
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',
|
||||
@@ -204,21 +149,4 @@ tap.test('should render remote project registry commands', async () => {
|
||||
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();
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
openCodeBridgeToolNames,
|
||||
renderOpenCodeBridgeConfigContent,
|
||||
renderOpenCodeBridgeToolFile,
|
||||
renderOpenCodeBridgeToolFiles,
|
||||
} from '../packages/opencode-bridge/ts/index.js';
|
||||
|
||||
tap.test('should render managed OpenCode bridge config', async () => {
|
||||
const config = JSON.parse(renderOpenCodeBridgeConfigContent());
|
||||
|
||||
expect(config.snapshot).toEqual(false);
|
||||
expect(config.autoupdate).toEqual(false);
|
||||
expect(config.permission.lsp).toEqual('deny');
|
||||
expect(config.permission.skill).toEqual('deny');
|
||||
});
|
||||
|
||||
tap.test('should render tool overrides for remote bridge', async () => {
|
||||
const files = renderOpenCodeBridgeToolFiles();
|
||||
|
||||
for (const toolName of openCodeBridgeToolNames) {
|
||||
expect(files[`tools/${toolName}.js`]).toInclude(`forwardTool(${JSON.stringify(toolName)}`);
|
||||
}
|
||||
expect(files['tools/bash.js']).toInclude('GITZONE_IDE_TOOL_BRIDGE_URL');
|
||||
expect(files['tools/apply_patch.js']).toInclude('patchText');
|
||||
});
|
||||
|
||||
tap.test('should allow custom bridge environment names', async () => {
|
||||
const toolFile = renderOpenCodeBridgeToolFile('read', {
|
||||
bridgeUrlEnvName: 'CUSTOM_BRIDGE_URL',
|
||||
bridgeTokenEnvName: 'CUSTOM_BRIDGE_TOKEN',
|
||||
});
|
||||
|
||||
expect(toolFile).toInclude('CUSTOM_BRIDGE_URL');
|
||||
expect(toolFile).toInclude('CUSTOM_BRIDGE_TOKEN');
|
||||
expect(toolFile).toInclude('filePath');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -1,40 +0,0 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { parseServerSentEvent, sanitizeOpenCodeEventForRenderer } from '../packages/opencode-bridge/ts/index.js';
|
||||
|
||||
tap.test('should parse named opencode sse events', async () => {
|
||||
const event = parseServerSentEvent('id: 1\nevent: server.connected\ndata: {"type":"server.connected"}\n');
|
||||
expect(event!.id).toEqual('1');
|
||||
expect(event!.type).toEqual('server.connected');
|
||||
expect(event!.data).toEqual({ type: 'server.connected' });
|
||||
});
|
||||
|
||||
tap.test('should infer opencode event type from json data', async () => {
|
||||
const event = parseServerSentEvent('data: {"type":"session.updated","properties":{"id":"abc"}}\n');
|
||||
expect(event!.type).toEqual('session.updated');
|
||||
expect(event!.data).toEqual({ type: 'session.updated', properties: { id: 'abc' } });
|
||||
});
|
||||
|
||||
tap.test('should sanitize opencode events for renderer delivery', async () => {
|
||||
const event = parseServerSentEvent('id: 2\nretry: 1000\nevent: permission.asked\ndata: {"permissionID":"perm-1"}\n')!;
|
||||
const sanitized = sanitizeOpenCodeEventForRenderer(event);
|
||||
|
||||
expect(sanitized).toEqual({
|
||||
type: 'permission.asked',
|
||||
id: '2',
|
||||
retry: 1000,
|
||||
data: { permissionID: 'perm-1' },
|
||||
});
|
||||
expect(Object.prototype.hasOwnProperty.call(sanitized, 'raw')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('should keep electron shell opencode resolution IDE-local', async () => {
|
||||
const source = await fs.readFile(new URL('../applications/electron-shell/ts/main.ts', import.meta.url), 'utf8');
|
||||
|
||||
expect(source.includes('process.env.OPENCODE_BINARY')).toEqual(false);
|
||||
expect(source.includes("'.opencode', 'bin', 'opencode'")).toEqual(false);
|
||||
expect(source.includes('/usr/local/bin/opencode')).toEqual(false);
|
||||
expect(source.includes('/usr/bin/opencode')).toEqual(false);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user