mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
* feat(agents): add OpenCode 2 beta support Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com> * fix(opencode2): support current plugin lifecycle and session storage * fix(opencode2): preserve lifecycle ordering and full session capture * test(opencode2): cover setup event bridge * test(opencode2): cover setup event bridge * test(browser): satisfy anti-slop naming check * test(opencode2): cover live form lifecycle * fix(relay): preserve OMP config directory selection * test(opencode2): avoid assertions in bridge fixture * fix(rebase): retain OMP resume and fresh launch behavior * test: align upstream OMP resume expectations * test(opencode2): verify rejected form closes waiting state --------- Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>
174 lines
5.4 KiB
TypeScript
174 lines
5.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { _internals } from './server'
|
|
import { buildBody } from './server.test-fixtures'
|
|
|
|
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
|
|
getCohortAtEmitMock: vi.fn(),
|
|
trackMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../telemetry/client', () => ({
|
|
track: trackMock
|
|
}))
|
|
|
|
vi.mock('../telemetry/cohort-classifier', () => ({
|
|
getCohortAtEmit: getCohortAtEmitMock
|
|
}))
|
|
|
|
beforeEach(() => {
|
|
_internals.resetCachesForTests()
|
|
trackMock.mockReset()
|
|
getCohortAtEmitMock.mockReset()
|
|
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe.each(['opencode', 'opencode2'] as const)('%s hook normalization', (source) => {
|
|
it('SessionBusy maps to working', () => {
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'SessionBusy' }),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('working')
|
|
expect(result?.payload.agentType).toBe(source)
|
|
})
|
|
|
|
it('SessionBusy does NOT clear the cached user prompt', () => {
|
|
// Why: OpenCode caches the user's MessagePart before SessionBusy fires, so the cached prompt is this turn's; clearing it would clobber the dashboard.
|
|
_internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'MessagePart', role: 'user', text: 'new prompt' }),
|
|
'production'
|
|
)
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'SessionBusy' }),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('working')
|
|
expect(result?.payload.prompt).toBe('new prompt')
|
|
})
|
|
|
|
it('SessionIdle maps to done', () => {
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'SessionIdle' }),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('done')
|
|
expect(result?.payload.agentType).toBe(source)
|
|
})
|
|
|
|
it('PermissionRequest maps to waiting', () => {
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'PermissionRequest' }),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('waiting')
|
|
})
|
|
|
|
it('AskUserQuestion maps to waiting', () => {
|
|
// Why: AskUserQuestion leaves the agent idle-but-waiting on a human, so it must map to `waiting` (red dot) like permission.asked, not stay `working`.
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'AskUserQuestion' }),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('waiting')
|
|
expect(result?.payload.agentType).toBe(source)
|
|
})
|
|
|
|
it('unknown event name returns null', () => {
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'SomeOtherEvent' }),
|
|
'production'
|
|
)
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('MessagePart with role=user surfaces text as the prompt and stays working', () => {
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({
|
|
hook_event_name: 'MessagePart',
|
|
role: 'user',
|
|
text: 'hi there',
|
|
messageID: 'msg-1'
|
|
}),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('working')
|
|
expect(result?.payload.prompt).toBe('hi there')
|
|
expect(result?.hasExplicitPrompt).toBe(true)
|
|
expect(result?.promptInteractionKey).toBe(`${source}-message-msg-1`)
|
|
})
|
|
|
|
it('MessagePart with role=assistant populates lastAssistantMessage', () => {
|
|
const result = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({
|
|
hook_event_name: 'MessagePart',
|
|
role: 'assistant',
|
|
text: 'Hello! How can I help?'
|
|
}),
|
|
'production'
|
|
)
|
|
expect(result?.payload.state).toBe('working')
|
|
expect(result?.payload.lastAssistantMessage).toBe('Hello! How can I help?')
|
|
})
|
|
|
|
it('caps oversized MessagePart text from stale (pre-throttle) plugin builds', () => {
|
|
// Why: stale plugin builds re-post the full reply on every part update, so the listener must cap the text to keep per-event work O(cap).
|
|
const assistant = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({
|
|
hook_event_name: 'MessagePart',
|
|
role: 'assistant',
|
|
text: 'a'.repeat(500_000)
|
|
}),
|
|
'production'
|
|
)
|
|
expect(assistant?.payload.lastAssistantMessage?.length).toBe(8_000)
|
|
|
|
// Why: prompt is capped at 200 by normalizeAgentStatusObject; assert oversized input still stays within that bound.
|
|
const user = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({
|
|
hook_event_name: 'MessagePart',
|
|
role: 'user',
|
|
text: 'u'.repeat(500_000),
|
|
messageID: 'msg-cap'
|
|
}),
|
|
'production'
|
|
)
|
|
expect(user?.payload.prompt?.length).toBe(200)
|
|
})
|
|
|
|
it('subsequent SessionIdle preserves cached prompt + assistant message', () => {
|
|
_internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'MessagePart', role: 'user', text: 'hi' }),
|
|
'production'
|
|
)
|
|
_internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'MessagePart', role: 'assistant', text: 'hello back' }),
|
|
'production'
|
|
)
|
|
const done = _internals.normalizeHookPayload(
|
|
source,
|
|
buildBody({ hook_event_name: 'SessionIdle' }),
|
|
'production'
|
|
)
|
|
expect(done?.payload.state).toBe('done')
|
|
expect(done?.payload.prompt).toBe('hi')
|
|
expect(done?.payload.lastAssistantMessage).toBe('hello back')
|
|
})
|
|
})
|