// Verifies the JSON event parser against captured opencode 1.15.11 output. // The schema is internal to opencode; if it changes, these tests fail loudly // rather than the provider silently returning empty reviewer responses. import { describe, it, expect } from 'vitest' import { OpencodeCliProvider } from '../../src/providers/opencode-cli.js' // Real captures from `opencode run --format json -m openrouter/openai/gpt-4o-mini`. const STEP_START_EVENT = '{"type":"step_start","timestamp":1780089625130,"sessionID":"ses_abc","part":{"id":"prt_1","messageID":"msg_1","sessionID":"ses_abc","type":"step-start"}}' const TEXT_EVENT = '{"type":"text","timestamp":1780089625396,"sessionID":"ses_abc","part":{"id":"prt_2","messageID":"msg_1","sessionID":"ses_abc","type":"text","text":"ok","time":{"start":1780089625131,"end":1780089625393}}}' // Access private parser methods. They're pure logic and worth testing directly; // extracting them into a separate module just for visibility would be churn. type ParserHandle = { extractEventText(event: unknown): string parseJsonOutput(output: string): string } function asParser(p: OpencodeCliProvider): ParserHandle { return p as unknown as ParserHandle } describe('OpencodeCliProvider parser', () => { describe('extractEventText', () => { it('returns the text from a text-part event', () => { const parser = asParser(new OpencodeCliProvider()) expect(parser.extractEventText(JSON.parse(TEXT_EVENT))).toBe('ok') }) it('returns empty for a step_start event', () => { const parser = asParser(new OpencodeCliProvider()) expect(parser.extractEventText(JSON.parse(STEP_START_EVENT))).toBe('') }) it('returns empty for an unknown event type', () => { const parser = asParser(new OpencodeCliProvider()) expect(parser.extractEventText({ type: 'tool.use', sessionID: 'ses_abc', tool: 'read' })).toBe('') }) it('returns empty for non-object inputs', () => { const parser = asParser(new OpencodeCliProvider()) expect(parser.extractEventText(null)).toBe('') expect(parser.extractEventText('text')).toBe('') expect(parser.extractEventText(42)).toBe('') }) }) describe('parseJsonOutput', () => { it('concatenates text across multiple events, ignoring others', () => { const provider = new OpencodeCliProvider() const output = [ STEP_START_EVENT, '{"type":"text","sessionID":"ses_abc","part":{"type":"text","text":"hello "}}', '{"type":"tool.use","sessionID":"ses_abc"}', '{"type":"text","sessionID":"ses_abc","part":{"type":"text","text":"world"}}', ].join('\n') expect(asParser(provider).parseJsonOutput(output)).toBe('hello world') }) it('skips blank lines and malformed JSON', () => { const provider = new OpencodeCliProvider() const output = [ '', 'not valid json', TEXT_EVENT, '{ partial', '', ].join('\n') expect(asParser(provider).parseJsonOutput(output)).toBe('ok') }) it('returns empty when no text events are present', () => { const provider = new OpencodeCliProvider() expect(asParser(provider).parseJsonOutput(STEP_START_EVENT)).toBe('') }) }) describe('session id capture', () => { it('does not capture sessionID when sessions are disabled', () => { const provider = new OpencodeCliProvider() asParser(provider).parseJsonOutput(TEXT_EVENT) expect(provider.sessionId).toBeUndefined() }) it('captures sessionID from the first event after startSession', () => { const provider = new OpencodeCliProvider() provider.startSession('reviewer-1') expect(provider.sessionId).toBeUndefined() // not pre-generated asParser(provider).parseJsonOutput(STEP_START_EVENT) expect(provider.sessionId).toBe('ses_abc') }) it('does not overwrite a captured sessionID with a later event', () => { const provider = new OpencodeCliProvider() provider.startSession('reviewer-1') asParser(provider).parseJsonOutput(STEP_START_EVENT) const laterEvent = '{"type":"text","sessionID":"ses_different","part":{"type":"text","text":"x"}}' asParser(provider).parseJsonOutput(laterEvent) expect(provider.sessionId).toBe('ses_abc') }) it('clears sessionID on endSession', () => { const provider = new OpencodeCliProvider() provider.startSession('reviewer-1') asParser(provider).parseJsonOutput(TEXT_EVENT) expect(provider.sessionId).toBe('ses_abc') provider.endSession() expect(provider.sessionId).toBeUndefined() }) }) })