import { EventEmitter } from 'node:events' import { mkdtemp, rm, stat } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { pathToFileURL } from 'node:url' import { build } from 'esbuild' interface FakeChildState { exePath: string args: string[] cwd: string child: EventEmitter & { pid: number exitCode: number | null signalCode: string | null killed: boolean stdin: { write: (payload: string) => boolean; end: () => void } kill: () => boolean } } interface KernelTestState { children: FakeChildState[] readers: EventEmitter[] stdinWrites: string[] autoExitOnEnd: boolean stdinEnds: number readerCloses: number killCalls: number } let failures = 0 function check(label: string, condition: boolean, detail = ''): void { if (condition) console.log(` ✓ ${label}`) else { failures++ console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`) } } async function waitFor(predicate: () => boolean, timeoutMs = 500): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { if (predicate()) return true await new Promise((resolve) => setTimeout(resolve, 5)) } return predicate() } async function buildKernelBundle(outfile: string): Promise { await build({ entryPoints: [join(process.cwd(), 'src', 'main', 'omp.ts')], outfile, bundle: true, platform: 'node', format: 'esm', logLevel: 'silent', plugins: [ { name: 'omp-session-kernel-mocks', setup(pluginBuild) { pluginBuild.onResolve({ filter: /^node:child_process$/ }, () => ({ path: 'mock-child-process', namespace: 'omp-session-mock' })) pluginBuild.onLoad({ filter: /^mock-child-process$/, namespace: 'omp-session-mock' }, () => ({ loader: 'js', contents: ` import { EventEmitter } from 'node:events'; const state = globalThis.__YCODE_OMP_SESSION_TEST__; class FakeChild extends EventEmitter { constructor(exePath, args, cwd) { super(); this.pid = 100 + state.children.length; this.exitCode = null; this.signalCode = null; this.killed = false; this.stdin = { write: (payload) => { state.stdinWrites.push(payload); let command; try { command = JSON.parse(payload); } catch { return true; } if (typeof command.id === 'string' && command.type !== 'host_tool_result') { const reader = state.readers[state.readers.length - 1]; const data = command.type === 'set_host_tools' ? { toolNames: (command.tools || []).map((tool) => tool.name) } : command.type === 'get_branch_messages' ? { messages: [{ entryId: 'user-entry-1', text: 'saved prompt' }] } : command.type === 'get_tree' ? { tree: [], leafId: null } : command.type === 'get_messages' ? { messages: [] } : {}; setImmediate(() => reader?.emit('line', JSON.stringify({ id: command.id, type: 'response', command: command.type, success: true, data }))); } return true; }, end: () => { state.stdinEnds++; if (state.autoExitOnEnd) setTimeout(() => this.finish(0, null), 5); } }; this.stdout = {}; this.stderr = new EventEmitter(); state.children.push({ exePath, args, cwd, child: this }); } finish(code, signal) { if (this.exitCode !== null || this.signalCode !== null) return; this.exitCode = code; this.signalCode = signal; this.emit('exit', code, signal); this.emit('close', code, signal); } kill() { this.killed = true; state.killCalls++; this.finish(null, 'SIGTERM'); return true; } } export function spawn(exePath, args, options) { return new FakeChild(exePath, args, options.cwd); } ` })) pluginBuild.onResolve({ filter: /^node:readline$/ }, () => ({ path: 'mock-readline', namespace: 'omp-session-mock' })) pluginBuild.onLoad({ filter: /^mock-readline$/, namespace: 'omp-session-mock' }, () => ({ loader: 'js', contents: ` import { EventEmitter } from 'node:events'; const state = globalThis.__YCODE_OMP_SESSION_TEST__; export function createInterface() { const reader = new EventEmitter(); reader.close = () => { state.readerCloses++; }; state.readers.push(reader); return reader; } ` })) } } ] }) } async function main(): Promise { const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-omp-session-regression-')) const state: KernelTestState = { children: [], readers: [], stdinWrites: [], autoExitOnEnd: true, stdinEnds: 0, readerCloses: 0, killCalls: 0 } const globalWithMock = globalThis as typeof globalThis & { __YCODE_OMP_SESSION_TEST__?: KernelTestState } const previous = globalWithMock.__YCODE_OMP_SESSION_TEST__ globalWithMock.__YCODE_OMP_SESSION_TEST__ = state try { const outfile = join(tempRoot, 'omp-kernel-regression.mjs') await buildKernelBundle(outfile) const { OmpKernel, parseFirstProfileModel } = await import(pathToFileURL(outfile).href) const yaml = [ 'providers:', ' local-probe:', ' api: openai-completions', ' models:', ' - id: probe-model', ' name: Probe model', ' second-provider:', ' models:', ' - id: second-model' ].join('\n') check('profile model resolution returns provider/model, not profile/model', parseFirstProfileModel(yaml) === 'local-probe/probe-model') check('model resolution returns null when models.yml has no configured provider model', parseFirstProfileModel('providers:\n empty-provider:\n models: []') === null) const sessionPath = join(tempRoot, 'native', 'session-one.jsonl') const selectedModel = 'local-probe/probe-model' const persistent = new OmpKernel(process.execPath, tempRoot, 'ycode', sessionPath, selectedModel) const persistentStart = persistent.start() check('session parent directory is created before spawn', await waitFor(() => state.children.length === 1) && (await stat(dirname(sessionPath))).isDirectory()) check('persistent start reaches the ready frame', state.readers.length === 1) state.readers[0]?.emit('line', JSON.stringify({ type: 'ready' })) await persistentStart const persistentArgs = state.children[0]?.args ?? [] check('persistent mode passes the configured provider/model selection', persistentArgs.includes(`--model=${selectedModel}`)) check('built-in OMP tools are disabled by default so execution stays behind the host broker', persistentArgs.includes('--no-tools')) check('persistent mode passes the native resume path and session directory', persistentArgs.includes(`--resume=${sessionPath}`) && persistentArgs.includes(`--session-dir=${dirname(sessionPath)}`)) check('persistent mode omits --no-session', !persistentArgs.includes('--no-session')) const hostToolsAck = await persistent.setHostTools([{ name: 'read', description: 'Read workspace text', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } }]) const branchMessages = await persistent.getBranchMessages() const branchAck = await persistent.branch('user-entry-1') await persistent.respondToHostTool('host-request-1', 'tool result') const image = { type: 'image' as const, data: 'c21hbGw=', mimeType: 'image/png' } const promptAck = await persistent.prompt('inspect this image', [image]) const writtenFrames = state.stdinWrites.map((payload) => JSON.parse(payload) as Record) const promptFrame = writtenFrames.find((frame) => frame.type === 'prompt') const hostResultFrame = writtenFrames.find((frame) => frame.type === 'host_tool_result') check('host tools, branch history, and images use their typed RPC helpers', hostToolsAck.success === true && branchMessages.data.messages[0]?.entryId === 'user-entry-1' && branchAck.success === true && promptAck.success === true && Array.isArray(promptFrame?.images) && JSON.stringify((promptFrame.images as unknown[])[0]) === JSON.stringify(image) && hostResultFrame?.id === 'host-request-1') let sawExit = false persistent.once('exit', () => { sawExit = true }) await persistent.dispose() check('dispose ends stdin and waits until the child exits', state.stdinEnds === 1 && sawExit) check('kernel event listeners are removed after child exit', persistent.eventNames().length === 0) const ephemeral = new OmpKernel(process.execPath, tempRoot, 'ycode', undefined, selectedModel) const ephemeralStart = ephemeral.start() check('ephemeral start creates a second child', await waitFor(() => state.children.length === 2)) state.readers[1]?.emit('line', JSON.stringify({ type: 'ready' })) await ephemeralStart const ephemeralArgs = state.children[1]?.args ?? [] check('three-argument constructors keep --no-session behavior', ephemeralArgs.includes('--no-session') && !ephemeralArgs.some((arg) => arg.startsWith('--resume=')) && ephemeralArgs.includes('--no-tools')) await ephemeral.dispose() const missingModel = new OmpKernel(process.execPath, tempRoot, 'ycode', undefined, null) let missingModelError: unknown try { await missingModel.start() } catch (error) { missingModelError = error } check('a profile without provider/model fails before spawn', missingModelError instanceof Error && missingModelError.message.includes('provider/model') && state.children.length === 2) state.autoExitOnEnd = false const forceKilled = new OmpKernel(process.execPath, tempRoot, 'ycode', join(tempRoot, 'native', 'session-three.jsonl'), selectedModel) const forceKillStart = forceKilled.start() check('force-kill case starts a third child', await waitFor(() => state.children.length === 3)) state.readers[2]?.emit('line', JSON.stringify({ type: 'ready' })) await forceKillStart check('built-in OMP tools remain disabled for every kernel session', state.children[2]?.args.includes('--no-tools') === true) const killStartedAt = Date.now() const keepAlive = setTimeout(() => undefined, 1800) await forceKilled.dispose() clearTimeout(keepAlive) const killDelay = Date.now() - killStartedAt check('stalled child is killed after the 1500ms grace period', state.killCalls === 1 && state.children[2]?.child.killed === true && killDelay >= 1400 && killDelay < 2500, `${killDelay}ms`) const recoverable = new OmpKernel(process.execPath, tempRoot, 'ycode', sessionPath, selectedModel) const firstStart = recoverable.start() check('recoverable kernel creates a fresh sidecar', await waitFor(() => state.children.length === 4)) state.readers[3]?.emit('line', JSON.stringify({ type: 'ready' })) await firstStart state.children[3]?.child.finish(1, null) const recoveredStart = recoverable.start() check('kernel can restart after its sidecar exits', await waitFor(() => state.children.length === 5)) state.readers[4]?.emit('line', JSON.stringify({ type: 'ready' })) await recoveredStart const recoveredArgs = state.children[4]?.args ?? [] check('restarted sidecar reuses the same resume path and provider/model', recoveredArgs.includes(`--resume=${sessionPath}`) && recoveredArgs.includes(`--model=${selectedModel}`)) await recoverable.dispose() console.log(failures === 0 ? '\nOMP session regression checks passed.\n' : `\n${failures} OMP session regression check(s) failed.\n`) process.exitCode = failures === 0 ? 0 : 1 } finally { globalWithMock.__YCODE_OMP_SESSION_TEST__ = previous await rm(tempRoot, { recursive: true, force: true }) } } main().catch((error: unknown) => { console.error('OMP session regression checks aborted:', error) process.exitCode = 1 })