2163ea45d2
The OpenCode provider allows using a variety of models with an agent harness that can gather more information from the codebase as required (like with claude-code, codex, or gemini-cli). This is an alternative to using OpenRouter directly, where the api provider is more like a chatbot and cannot gather any additional context beyond what was handed to it.
110 lines
4.6 KiB
TypeScript
110 lines
4.6 KiB
TypeScript
// 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()
|
|
})
|
|
})
|
|
})
|