import assert from 'node:assert/strict'; import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { spawnOpenCode } from './opencode-cli.js'; const findEnvKey = (name) => Object.keys(process.env).find((key) => key.toLowerCase() === name.toLowerCase()) || name; async function createFakeOpenCodeExecutable(binDir) { const scriptPath = path.join(binDir, 'opencode.js'); await writeFile(scriptPath, ` const events = [ { type: 'text', sessionID: 'open-live-1', text: 'assistant response' }, { type: 'step_finish', sessionID: 'open-live-1' }, ]; for (const event of events) { console.log(JSON.stringify(event)); } `, 'utf8'); if (process.platform === 'win32') { const commandPath = path.join(binDir, 'opencode.cmd'); await writeFile(commandPath, '@echo off\r\nnode "%~dp0opencode.js" %*\r\n', 'utf8'); return; } const commandPath = path.join(binDir, 'opencode'); await writeFile(commandPath, '#!/bin/sh\nnode "$(dirname "$0")/opencode.js" "$@"\n', 'utf8'); await chmod(commandPath, 0o755); } test('spawnOpenCode emits session_created before normalized live messages for new sessions', async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-cli-live-')); const pathKey = findEnvKey('PATH'); const pathExtKey = findEnvKey('PATHEXT'); const previousPath = process.env[pathKey]; const previousPathExt = process.env[pathExtKey]; const messages = []; const writer = { userId: null, sessionId: null, send(message) { messages.push(message); }, setSessionId(sessionId) { this.sessionId = sessionId; }, }; try { await createFakeOpenCodeExecutable(tempRoot); process.env[pathKey] = `${tempRoot}${path.delimiter}${previousPath || ''}`; if (process.platform === 'win32') { process.env[pathExtKey] = previousPathExt?.toUpperCase().includes('.CMD') ? previousPathExt : `.COM;.EXE;.BAT;.CMD${previousPathExt ? `;${previousPathExt}` : ''}`; } await spawnOpenCode('Hi', { cwd: tempRoot }, writer); const sessionCreatedIndex = messages.findIndex((message) => message.kind === 'session_created'); const assistantDeltaIndex = messages.findIndex((message) => message.kind === 'stream_delta' && message.content === 'assistant response', ); const streamEnd = messages.find((message) => message.kind === 'stream_end'); const complete = messages.find((message) => message.kind === 'complete'); assert.notEqual(sessionCreatedIndex, -1); assert.notEqual(assistantDeltaIndex, -1); assert.ok(sessionCreatedIndex < assistantDeltaIndex); assert.equal(messages[sessionCreatedIndex].newSessionId, 'open-live-1'); assert.equal(writer.sessionId, 'open-live-1'); assert.equal(streamEnd?.sessionId, 'open-live-1'); assert.equal(complete?.sessionId, 'open-live-1'); assert.equal(messages.some((message) => message.kind === 'error'), false); } finally { if (previousPath === undefined) { delete process.env[pathKey]; } else { process.env[pathKey] = previousPath; } if (previousPathExt === undefined) { delete process.env[pathExtKey]; } else { process.env[pathExtKey] = previousPathExt; } await rm(tempRoot, { recursive: true, force: true }); } });