2026-04-25 20:44:07 +03:00
import { EventEmitter } from 'node:events' ;
import { afterEach , describe , expect , it , vi } from ' vitest ';
const spawnMock = vi.fn();
2026-07-31 12:51:15 +03:00
const recordStartupPerformanceMock = vi.fn();
2026-04-25 20:44:07 +03:00
vi.mock(' node : child_process ', () => ({
spawn: spawnMock,
spawnSync: vi.fn(),
}));
2026-07-31 12:51:15 +03:00
vi.mock(' . / startup - performance . js ', () => ({
recordStartupPerformance: recordStartupPerformanceMock,
}));
2026-04-25 20:44:07 +03:00
const { createOpenCodeLifecycleRuntime } = await import(' . / lifecycle . js ');
const originalOpencodeBinary = process.env.OPENCODE_BINARY;
2026-05-12 03:57:14 -04:00
const originalPath = process.env.PATH;
2026-07-29 17:26:11 +03:00
const originalFetch = globalThis.fetch;
2026-04-25 20:44:07 +03:00
afterEach(() => {
spawnMock.mockReset();
2026-07-31 12:51:15 +03:00
recordStartupPerformanceMock.mockReset();
2026-07-29 17:26:11 +03:00
globalThis.fetch = originalFetch;
2026-04-25 20:44:07 +03:00
if (typeof originalOpencodeBinary === ' string ') {
process.env.OPENCODE_BINARY = originalOpencodeBinary;
2026-05-12 03:57:14 -04:00
} else {
delete process.env.OPENCODE_BINARY;
}
if (typeof originalPath === ' string ') {
process.env.PATH = originalPath;
} else {
delete process.env.PATH;
2026-04-25 20:44:07 +03:00
}
});
const createMockChild = () => {
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.exitCode = null;
child.signalCode = null;
child.pid = 12345;
child.kill = vi.fn(() => {
child.signalCode = ' SIGTERM ';
queueMicrotask(() => child.emit(' close ', null, ' SIGTERM '));
return true;
});
return child;
};
2026-08-05 11:24:14 +03:00
const createRuntime = (overrides = {}, stateOverrides = {}, envOverrides = {}) => {
2026-04-25 20:44:07 +03:00
const state = {
openCodeWorkingDirectory: ' / tmp / project ',
openCodeProcess: null,
openCodePort: null,
openCodeBaseUrl: null,
currentRestartPromise: null,
isRestartingOpenCode: false,
openCodeApiPrefix: '',
openCodeApiPrefixDetected: false,
openCodeApiDetectionTimer: null,
lastOpenCodeError: null,
isOpenCodeReady: false,
openCodeNotReadySince: 0,
isExternalOpenCode: false,
isShuttingDown: false,
healthCheckInterval: null,
expressApp: null,
useWslForOpencode: false,
resolvedWslBinary: null,
resolvedWslOpencodePath: null,
resolvedWslDistro: null,
2026-07-29 17:26:11 +03:00
...stateOverrides,
2026-04-25 20:44:07 +03:00
};
return createOpenCodeLifecycleRuntime({
state,
env: {
ENV_CONFIGURED_OPENCODE_PORT: 45678,
ENV_CONFIGURED_OPENCODE_HOST: null,
ENV_EFFECTIVE_PORT: 3001,
ENV_CONFIGURED_OPENCODE_HOSTNAME: ' 127.0 . 0.1 ',
ENV_SKIP_OPENCODE_START: false,
2026-08-05 11:24:14 +03:00
...envOverrides,
2026-04-25 20:44:07 +03:00
},
syncToHmrState: vi.fn(),
syncFromHmrState: vi.fn(),
getOpenCodeAuthHeaders: () => ({}),
buildOpenCodeUrl: (route) => `http://127.0.0.1:45678${route}`,
waitForReady: vi.fn(async () => true),
normalizeApiPrefix: vi.fn(() => ''),
applyOpencodeBinaryFromSettings: vi.fn(async () => null),
ensureOpencodeCliEnv: vi.fn(),
ensureLocalOpenCodeServerPassword: vi.fn(async () => ' password '),
resolveManagedOpenCodeLaunchSpec: vi.fn((binary) => ({ binary, args: [], wrapperType: null })),
setOpenCodePort: vi.fn((port) => {
state.openCodePort = port;
}),
setDetectedOpenCodeApiPrefix: vi.fn(),
setupProxy: vi.fn(),
ensureOpenCodeApiPrefix: vi.fn(),
clearResolvedOpenCodeBinary: vi.fn(),
buildAugmentedPath: vi.fn(() => ' / home / user / . bun / bin : /usr/local/bin:/usr/bin'),
buildManagedOpenCodePath : vi . fn (() => '/home/user/.bun/bin:/usr/local/bin:/usr/bin' ),
2026-04-28 12:35:00 +03:00
getManagedOpenCodeShellEnvSnapshot : vi . fn (() => ({
PATH : '/home/user/.bun/bin:/usr/local/bin:/usr/bin' ,
SHELL_ONLY : 'yes' ,
OPENCODE_SERVER_PASSWORD : 'shell-password' ,
})),
2026-04-25 20:44:07 +03:00
... overrides ,
});
};
describe ( 'OpenCode lifecycle' , () => {
2026-07-31 12:51:15 +03:00
it ( 'records an authoritative ready terminal event for external startup' , async () => {
globalThis . fetch = vi . fn ( async () => ({
ok : true ,
json : async () => ({ healthy : true }),
}));
const runtime = createRuntime ({
env : {
ENV_CONFIGURED_OPENCODE_PORT : 45678 ,
ENV_CONFIGURED_OPENCODE_HOST : null ,
ENV_EFFECTIVE_PORT : 45678 ,
ENV_CONFIGURED_OPENCODE_HOSTNAME : '127.0.0.1' ,
ENV_SKIP_OPENCODE_START : true ,
},
reapManagedOrphanedProcesses : vi . fn ( async () => ({ reaped : 0 })),
});
await runtime . bootstrapOpenCodeAtStartup ();
expect ( recordStartupPerformanceMock ). toHaveBeenCalledWith ( 'opencode.bootstrap.ready' , {
totalDurationMs : expect . any ( Number ),
outcome : 'ready' ,
});
expect ( recordStartupPerformanceMock ). not . toHaveBeenCalledWith (
'opencode.bootstrap.error' ,
expect . anything (),
);
const terminalEvents = recordStartupPerformanceMock . mock . calls . filter (([ phase ]) => (
phase === 'opencode.bootstrap.ready' || phase === 'opencode.bootstrap.error'
));
expect ( terminalEvents ). toHaveLength ( 1 );
});
it ( 'warms recently used directories after a successful bootstrap' , async () => {
const fetchMock = vi . fn ( async () => ({
ok : true ,
json : async () => ({ healthy : true }),
}));
globalThis . fetch = fetchMock ;
const runtime = createRuntime ({
env : {
ENV_CONFIGURED_OPENCODE_PORT : 45678 ,
ENV_CONFIGURED_OPENCODE_HOST : null ,
ENV_EFFECTIVE_PORT : 45678 ,
ENV_CONFIGURED_OPENCODE_HOSTNAME : '127.0.0.1' ,
ENV_SKIP_OPENCODE_START : true ,
},
reapManagedOrphanedProcesses : vi . fn ( async () => ({ reaped : 0 })),
getWarmupDirectories : vi . fn ( async () => [ '/tmp/worktree-a' , '/tmp/project-b' ]),
});
await runtime . bootstrapOpenCodeAtStartup ();
await new Promise (( resolve ) => setTimeout ( resolve , 0 ));
const warmupUrls = fetchMock . mock . calls
. map (([ url ]) => String ( url ))
. filter (( url ) => url . includes ( '/session/status' ));
expect ( warmupUrls ). toEqual ([
'http://127.0.0.1:45678/session/status?directory=%2Ftmp%2Fworktree-a' ,
'http://127.0.0.1:45678/session/status?directory=%2Ftmp%2Fproject-b' ,
]);
});
it ( 'records an authoritative error terminal event when bootstrap fails' , async () => {
const runtime = createRuntime ({
syncFromHmrState : vi . fn (() => {
throw new Error ( 'bootstrap failed' );
}),
reapManagedOrphanedProcesses : vi . fn ( async () => ({ reaped : 0 })),
});
await runtime . bootstrapOpenCodeAtStartup ();
expect ( recordStartupPerformanceMock ). toHaveBeenCalledWith ( 'opencode.bootstrap.error' , {
totalDurationMs : expect . any ( Number ),
outcome : 'error' ,
});
expect ( recordStartupPerformanceMock ). not . toHaveBeenCalledWith (
'opencode.bootstrap.ready' ,
expect . anything (),
);
const terminalEvents = recordStartupPerformanceMock . mock . calls . filter (([ phase ]) => (
phase === 'opencode.bootstrap.ready' || phase === 'opencode.bootstrap.error'
));
expect ( terminalEvents ). toHaveLength ( 1 );
});
2026-07-29 17:26:11 +03:00
it ( 'does not count rapid transport-triggered checks as independent health failures' , async () => {
const close = vi . fn ( async () => {});
const warn = vi . spyOn ( console , 'warn' ). mockImplementation (() => {});
let now = 1 ;
globalThis . fetch = vi . fn ( async () => ({
ok : false ,
json : async () => null ,
}));
const runtime = createRuntime ({ now : () => now }, {
openCodePort : 45678 ,
openCodeProcess : {
pid : null ,
exitCode : null ,
signalCode : null ,
close ,
},
isOpenCodeReady : true ,
});
for ( let attempt = 0 ; attempt < 25 ; attempt += 1 ) {
await runtime . triggerHealthCheck ();
}
expect ( close ). not . toHaveBeenCalled ();
expect ( warn ). toHaveBeenCalledTimes ( 1 );
now += 15_000 ;
await runtime . triggerHealthCheck ();
expect ( warn ). toHaveBeenCalledTimes ( 2 );
expect ( warn ). toHaveBeenLastCalledWith ( expect . stringContaining ( '(2/20)' ));
warn . mockRestore ();
});
2026-08-03 13:23:42 +03:00
it ( 'does not mistake a live managed process wrapper for an exited child' , async () => {
const close = vi . fn ( async () => {});
const warn = vi . spyOn ( console , 'warn' ). mockImplementation (() => {});
globalThis . fetch = vi . fn ( async () => ({
ok : false ,
json : async () => null ,
}));
const runtime = createRuntime ({}, {
openCodePort : 45678 ,
openCodeProcess : {
pid : process . pid ,
close ,
},
isOpenCodeReady : true ,
});
await runtime . triggerHealthCheck ();
expect ( close ). not . toHaveBeenCalled ();
expect ( spawnMock ). not . toHaveBeenCalled ();
expect ( warn ). toHaveBeenCalledWith ( expect . stringContaining ( '(1/20)' ));
warn . mockRestore ();
});
2026-07-29 17:26:11 +03:00
it ( 'restarts an exited managed process without waiting for the failure interval' , async () => {
const close = vi . fn ( async () => {});
const replacement = createMockChild ();
globalThis . fetch = vi . fn ( async () => ({
ok : false ,
json : async () => null ,
}));
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
replacement . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return replacement ;
});
const runtime = createRuntime ({}, {
openCodePort : 45678 ,
openCodeProcess : {
pid : null ,
exitCode : 1 ,
signalCode : null ,
close ,
},
});
await runtime . triggerHealthCheck ();
expect ( close ). toHaveBeenCalledTimes ( 1 );
expect ( spawnMock ). toHaveBeenCalledTimes ( 1 );
});
2026-08-05 13:59:17 +03:00
it ( 'calls onOpenCodeRestarted after a successful managed restart' , async () => {
const close = vi . fn ( async () => {});
const replacement = createMockChild ();
const onOpenCodeRestarted = vi . fn ();
globalThis . fetch = vi . fn ( async () => ({
ok : false ,
json : async () => null ,
}));
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
replacement . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return replacement ;
});
const runtime = createRuntime ({ onOpenCodeRestarted }, {
openCodePort : 45678 ,
openCodeProcess : {
pid : null ,
exitCode : 1 ,
signalCode : null ,
close ,
},
});
await runtime . triggerHealthCheck ();
expect ( close ). toHaveBeenCalledTimes ( 1 );
expect ( spawnMock ). toHaveBeenCalledTimes ( 1 );
// The restart completed on a (possibly new) port — the event-stream
// upstreams must rebind so the UI keeps receiving events (#2638).
expect ( onOpenCodeRestarted ). toHaveBeenCalledTimes ( 1 );
});
it ( 'does not call onOpenCodeRestarted when a managed restart fails' , async () => {
const close = vi . fn ( async () => {});
const onOpenCodeRestarted = vi . fn ();
globalThis . fetch = vi . fn ( async () => ({
ok : false ,
json : async () => null ,
}));
spawnMock . mockImplementation (() => {
const child = createMockChild ();
queueMicrotask (() => {
child . emit ( 'error' , new Error ( 'spawn failed' ));
});
return child ;
});
const runtime = createRuntime ({ onOpenCodeRestarted }, {
openCodePort : 45678 ,
openCodeProcess : {
pid : null ,
exitCode : 1 ,
signalCode : null ,
close ,
},
});
// triggerHealthCheck logs instead of rethrowing; call restartOpenCode
// directly to observe the failure result.
await expect ( runtime . restartOpenCode ()). rejects . toThrow ();
expect ( onOpenCodeRestarted ). not . toHaveBeenCalled ();
});
2026-04-25 20:44:07 +03:00
it ( 'launches managed OpenCode with the managed PATH' , async () => {
delete process . env . OPENCODE_BINARY ;
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return child ;
});
const runtime = createRuntime ();
const server = await runtime . startOpenCode ();
const [ binary , args , options ] = spawnMock . mock . calls [ 0 ];
expect ( binary ). toBe ( 'opencode' );
expect ( args ). toEqual ([ 'serve' , '--hostname' , '127.0.0.1' , '--port' , '45678' ]);
expect ( options . env . PATH ). toBe ( '/home/user/.bun/bin:/usr/local/bin:/usr/bin' );
2026-04-28 12:35:00 +03:00
expect ( options . env . SHELL_ONLY ). toBe ( 'yes' );
2026-04-25 20:44:07 +03:00
expect ( options . env . OPENCODE_SERVER_PASSWORD ). toBe ( 'password' );
2026-08-03 13:23:42 +03:00
expect ( server . exitCode ). toBeNull ();
expect ( server . signalCode ). toBeNull ();
2026-04-25 20:44:07 +03:00
await server . close ();
2026-08-03 13:23:42 +03:00
expect ( server . signalCode ). toBe ( 'SIGTERM' );
2026-04-25 20:44:07 +03:00
});
2026-04-26 16:24:07 +03:00
2026-08-05 11:24:14 +03:00
it ( 'launches managed OpenCode on the configured bind hostname' , async () => {
delete process . env . OPENCODE_BINARY ;
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://0.0.0.0:45678\n' );
});
return child ;
});
const runtime = createRuntime ({}, {}, { ENV_CONFIGURED_OPENCODE_HOSTNAME : '0.0.0.0' });
const server = await runtime . startOpenCode ();
const [ binary , args ] = spawnMock . mock . calls [ 0 ];
expect ( binary ). toBe ( 'opencode' );
expect ( args ). toEqual ([ 'serve' , '--hostname' , '0.0.0.0' , '--port' , '45678' ]);
await server . close ();
expect ( server . signalCode ). toBe ( 'SIGTERM' );
});
2026-08-03 08:54:39 +00:00
it ( 'strips AppImage ARGV0 from managed OpenCode launch env' , async () => {
delete process . env . OPENCODE_BINARY ;
const previousArgv0 = process . env . ARGV0 ;
process . env . ARGV0 = '/path/to/OpenChamber/OpenChamber-1.17.2-linux-x86_64.AppImage' ;
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return child ;
});
try {
const runtime = createRuntime ({
getManagedOpenCodeShellEnvSnapshot : vi . fn (() => ({
PATH : '/home/user/.bun/bin:/usr/local/bin:/usr/bin' ,
ARGV0 : '/leaked/from/shell/snapshot.AppImage' ,
SHELL_ONLY : 'yes' ,
})),
});
const server = await runtime . startOpenCode ();
const [, , options ] = spawnMock . mock . calls [ 0 ];
expect ( options . env ). not . toHaveProperty ( 'ARGV0' );
expect ( options . env . SHELL_ONLY ). toBe ( 'yes' );
expect ( options . env . PATH ). toBe ( '/home/user/.bun/bin:/usr/local/bin:/usr/bin' );
await server . close ();
} finally {
if ( previousArgv0 === undefined ) delete process . env . ARGV0 ;
else process . env . ARGV0 = previousArgv0 ;
}
2026-04-25 20:44:07 +03:00
});
2026-04-26 16:24:07 +03:00
2026-07-24 21:54:28 +03:00
it ( 'adds managed OpenChamber tool environment without allowing it to replace launch invariants' , async () => {
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return child ;
});
const getManagedOpenCodeEnv = vi . fn ( async () => ({
OPENCODE_CONFIG_CONTENT : '{"plugin":["file:///tool.js"]}' ,
OPENCHAMBER_AGENT_TOOL_TOKEN : 'ephemeral' ,
PATH : '/untrusted/path' ,
OPENCODE_SERVER_PASSWORD : 'untrusted-password' ,
}));
const runtime = createRuntime ({ getManagedOpenCodeEnv });
const server = await runtime . startOpenCode ();
const [, , options ] = spawnMock . mock . calls [ 0 ];
expect ( getManagedOpenCodeEnv ). toHaveBeenCalledOnce ();
expect ( options . env . OPENCODE_CONFIG_CONTENT ). toBe ( '{"plugin":["file:///tool.js"]}' );
expect ( options . env . OPENCHAMBER_AGENT_TOOL_TOKEN ). toBe ( 'ephemeral' );
expect ( options . env . PATH ). toBe ( '/home/user/.bun/bin:/usr/local/bin:/usr/bin' );
expect ( options . env . OPENCODE_SERVER_PASSWORD ). toBe ( 'password' );
await server . close ();
});
2026-08-03 13:18:27 +00:00
it ( 'mirrors Google credential env aliases into the managed OpenCode environment' , async () => {
const previousGemini = process . env . GEMINI_API_KEY ;
const previousGoogleGen = process . env . GOOGLE_GENERATIVE_AI_API_KEY ;
const previousGoogle = process . env . GOOGLE_API_KEY ;
process . env . GEMINI_API_KEY = 'AIza-from-gemini' ;
delete process . env . GOOGLE_GENERATIVE_AI_API_KEY ;
delete process . env . GOOGLE_API_KEY ;
try {
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return child ;
});
const runtime = createRuntime ();
const server = await runtime . startOpenCode ();
const [, , options ] = spawnMock . mock . calls [ 0 ];
expect ( options . env . GEMINI_API_KEY ). toBe ( 'AIza-from-gemini' );
expect ( options . env . GOOGLE_API_KEY ). toBe ( 'AIza-from-gemini' );
expect ( options . env . GOOGLE_GENERATIVE_AI_API_KEY ). toBe ( 'AIza-from-gemini' );
await server . close ();
} finally {
if ( typeof previousGemini === 'string' ) {
process . env . GEMINI_API_KEY = previousGemini ;
} else {
delete process . env . GEMINI_API_KEY ;
}
if ( typeof previousGoogleGen === 'string' ) {
process . env . GOOGLE_GENERATIVE_AI_API_KEY = previousGoogleGen ;
} else {
delete process . env . GOOGLE_GENERATIVE_AI_API_KEY ;
}
if ( typeof previousGoogle === 'string' ) {
process . env . GOOGLE_API_KEY = previousGoogle ;
} else {
delete process . env . GOOGLE_API_KEY ;
}
}
});
2026-04-26 16:24:07 +03:00
it ( 'falls back to buildAugmentedPath when buildManagedOpenCodePath is not provided' , async () => {
delete process . env . OPENCODE_BINARY ;
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return child ;
});
const runtime = createRuntime ({
buildManagedOpenCodePath : undefined ,
buildAugmentedPath : vi . fn (() => '/home/user/.cargo/bin:/usr/local/bin' ),
});
const server = await runtime . startOpenCode ();
const [, , options ] = spawnMock . mock . calls [ 0 ];
expect ( options . env . PATH ). toBe ( '/home/user/.cargo/bin:/usr/local/bin' );
await server . close ();
});
it ( 'falls back to process.env.PATH when neither build function is provided' , async () => {
delete process . env . OPENCODE_BINARY ;
process . env . PATH = '/usr/bin:/bin' ;
const child = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
child . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return child ;
});
const runtime = createRuntime ({
buildManagedOpenCodePath : undefined ,
buildAugmentedPath : undefined ,
});
const server = await runtime . startOpenCode ();
const [, , options ] = spawnMock . mock . calls [ 0 ];
expect ( options . env . PATH ). toBe ( '/usr/bin:/bin' );
await server . close ();
});
2026-04-28 12:35:00 +03:00
2026-05-06 02:06:16 +03:00
it ( 'reports the binary when managed OpenCode exits before becoming ready' , async () => {
2026-04-28 12:35:00 +03:00
delete process . env . OPENCODE_BINARY ;
const firstChild = createMockChild ();
const secondChild = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
firstChild . emit ( 'exit' , null , 'SIGTERM' );
});
return firstChild ;
});
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
secondChild . emit ( 'exit' , null , 'SIGTERM' );
});
return secondChild ;
});
const runtime = createRuntime ();
2026-05-06 02:06:16 +03:00
await expect ( runtime . startOpenCode ()). rejects . toThrow ( 'OpenCode process exited before serving with signal SIGTERM. Binary used: opencode. No stdout/stderr captured' );
2026-04-28 12:35:00 +03:00
expect ( spawnMock ). toHaveBeenCalledTimes ( 2 );
});
2026-05-06 02:06:16 +03:00
it ( 'does not retry managed startup when the configured OpenCode binary is invalid' , async () => {
delete process . env . OPENCODE_BINARY ;
const error = new Error ( 'Configured OpenCode binary not found: /missing/opencode' );
error . code = 'OPENCODE_BINARY_INVALID' ;
const applyOpencodeBinaryFromSettings = vi . fn ( async () => {
throw error ;
});
const runtime = createRuntime ({ applyOpencodeBinaryFromSettings });
await expect ( runtime . startOpenCode ()). rejects . toThrow ( 'Configured OpenCode binary not found: /missing/opencode' );
expect ( applyOpencodeBinaryFromSettings ). toHaveBeenCalledTimes ( 1 );
expect ( applyOpencodeBinaryFromSettings ). toHaveBeenCalledWith ({ strict : true });
expect ( spawnMock ). not . toHaveBeenCalled ();
});
2026-04-28 12:35:00 +03:00
it ( 'retries managed OpenCode startup once after a pre-ready exit' , async () => {
delete process . env . OPENCODE_BINARY ;
const firstChild = createMockChild ();
const secondChild = createMockChild ();
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
firstChild . emit ( 'exit' , null , 'SIGTERM' );
});
return firstChild ;
});
spawnMock . mockImplementationOnce (() => {
queueMicrotask (() => {
secondChild . stdout . emit ( 'data' , 'opencode server listening on http://127.0.0.1:45678\n' );
});
return secondChild ;
});
const runtime = createRuntime ();
const server = await runtime . startOpenCode ();
expect ( spawnMock ). toHaveBeenCalledTimes ( 2 );
await server . close ();
});
2026-04-25 20:44:07 +03:00
});