* fix: exclude file content from reverted prompt text Revert and fork now restore only the user's original prompt, not server-injected file content Uses existing isSyntheticPart helper for type-safe filtering * fix: keep scrollbar visible when hovering over thumb * fix: prevent ESC abort from triggering when terminal is focused * fix: pass directory to permission/question reply calls so approvals actually resolve * fix: default model selection not responding after Base UI migration * fix: prevent modal content from shifting and clipping footer buttons * fix: improve session switching performance and add sub-agent export with prompt collapse Defer viewport anchor saving to eliminate ~800ms UI freeze when switching sessions Add export dialog to include sub-agent tasks recursively in markdown export Add collapse chevron button for expanded user prompts in sticky header * fix: resolve sidebar scroll and TDZ crash in session sidebar * perf: reduce CPU overhead and re-renders across chat, layout, and settings * fix: position collapse button at top of message and prevent ESC abort in terminal * fix: position collapse button at top and add padding only when expanded * refactor: extract shared PATH utilities and mobile keyboard hook * refactor: import shared path-utils in electron, use module-level style constants - Electron now imports pathLooksUserConfigured/mergePathValues from shared path-utils.js instead of inline duplication - ToolPart collapsedCustomStyle moved from useMemo([]) to module const * fix: resolve remaining merge conflicts and type errors - Remove duplicate variable declarations in SessionNodeItem - Remove orphaned export callback body from conflict resolution - Fix HelpDialog description -> descriptionKey (i18n rename) * fix: resolve type-check and lint errors in session-actions.test.ts - Added missing bun:test type declarations (beforeEach, mock, mock.module) - Removed unused State import - Replaced 'as any' casts with proper OpencodeClient and ChildStoreManager types - Added eslint-disable for unused _ parameter in mock function * fix PR 1028 export and PATH edge cases * fix startup retry exhaustion state * remove opencode package lock change * fix sub-session rename cancellation --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
167 lines
5.2 KiB
JavaScript
167 lines
5.2 KiB
JavaScript
import { EventEmitter } from 'node:events';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const spawnMock = vi.fn();
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
spawn: spawnMock,
|
|
spawnSync: vi.fn(),
|
|
}));
|
|
|
|
const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
|
|
|
|
const originalOpencodeBinary = process.env.OPENCODE_BINARY;
|
|
|
|
afterEach(() => {
|
|
spawnMock.mockReset();
|
|
if (typeof originalOpencodeBinary === 'string') {
|
|
process.env.OPENCODE_BINARY = originalOpencodeBinary;
|
|
return;
|
|
}
|
|
delete process.env.OPENCODE_BINARY;
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
const createRuntime = (overrides = {}) => {
|
|
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,
|
|
};
|
|
|
|
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,
|
|
},
|
|
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'),
|
|
buildWslExecArgs: vi.fn((args) => args),
|
|
resolveWslExecutablePath: vi.fn(),
|
|
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'),
|
|
...overrides,
|
|
});
|
|
};
|
|
|
|
describe('OpenCode lifecycle', () => {
|
|
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');
|
|
expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
|
|
|
|
await server.close();
|
|
});
|
|
|
|
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;
|
|
const originalPath = process.env.PATH;
|
|
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');
|
|
process.env.PATH = originalPath;
|
|
|
|
await server.close();
|
|
});
|
|
});
|