Merge origin/main into deferred OpenCode restart branch

This commit is contained in:
Bohdan Triapitsyn
2026-08-07 10:08:50 +03:00
218 changed files with 12131 additions and 1293 deletions
@@ -50,7 +50,7 @@ const createMockChild = () => {
return child;
};
const createRuntime = (overrides = {}, stateOverrides = {}) => {
const createRuntime = (overrides = {}, stateOverrides = {}, envOverrides = {}) => {
const state = {
openCodeWorkingDirectory: '/tmp/project',
openCodeProcess: null,
@@ -83,6 +83,7 @@ const createRuntime = (overrides = {}, stateOverrides = {}) => {
ENV_EFFECTIVE_PORT: 3001,
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
ENV_SKIP_OPENCODE_START: false,
...envOverrides,
},
syncToHmrState: vi.fn(),
syncFromHmrState: vi.fn(),
@@ -286,6 +287,70 @@ describe('OpenCode lifecycle', () => {
expect(spawnMock).toHaveBeenCalledTimes(1);
});
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();
});
it('launches managed OpenCode with the managed PATH', async () => {
delete process.env.OPENCODE_BINARY;
const child = createMockChild();
@@ -312,6 +377,27 @@ describe('OpenCode lifecycle', () => {
expect(server.signalCode).toBe('SIGTERM');
});
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');
});
it('strips AppImage ARGV0 from managed OpenCode launch env', async () => {
delete process.env.OPENCODE_BINARY;
const previousArgv0 = process.env.ARGV0;