mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* perf(ci): reduce test overhead and reuse expensive build work * test(ci): keep worker startup outside failure-case deadlines * test: isolate push fixture advisory locks * test(ci): preserve fixed drain retry allowance * Align CI contracts with current triggers and guide content * Stabilize consolidated reveal scenarios and retain behavioral CI contracts * Fix upstream lint blockers while retaining behavior and assertions * Classify global Jira preferences in the WSL migration inventory * Align Jira test fixtures with startup and build safety checks * Include package-test routing dependency in CI checkouts and fixtures * Keep native stream classes intact in transcript dependency tests
255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { AGENT_SESSION_HOST_STATUS_COPY } from '../../shared/agent-session-host-status-rows'
|
|
import Database from '../sqlite/sync-database'
|
|
import {
|
|
readOpenCodeTranscriptPage,
|
|
readOpenCodeTranscriptSignal
|
|
} from './transcript-opencode-sqlite-query'
|
|
import { subscribeOpenCodeNativeChatTranscript } from './transcript-opencode-subscribe'
|
|
|
|
const fixtures: { db: Database.Database; root: string }[] = []
|
|
afterEach(() => {
|
|
for (const { db, root } of fixtures.splice(0)) {
|
|
db.close()
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'orca-native-opencode2-'))
|
|
const path = join(root, 'opencode.db')
|
|
const db = new Database(path)
|
|
fixtures.push({ db, root })
|
|
db.exec(`CREATE TABLE session_v2 (id TEXT PRIMARY KEY);
|
|
CREATE TABLE session_message (id TEXT PRIMARY KEY, session_id TEXT, type TEXT,
|
|
seq INTEGER, data TEXT, time_created INTEGER, time_updated INTEGER);
|
|
INSERT INTO session_v2 VALUES ('session');`)
|
|
const insertMessage = db.prepare('INSERT INTO session_message VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
let sequence = 0
|
|
const insert = (id: string, type: string, data: unknown, time = 1) =>
|
|
insertMessage.run(id, 'session', type, ++sequence, JSON.stringify(data), time, time)
|
|
return { db, path, insert }
|
|
}
|
|
|
|
describe('OpenCode 2 native transcript', () => {
|
|
it('paginates real v2 user/assistant records past bookkeeping without losing messages', () => {
|
|
const { path, insert } = fixture()
|
|
insert('one', 'user', { text: 'first prompt' })
|
|
insert('empty', 'idle', { outcome: 'succeeded' })
|
|
insert('two', 'assistant', { content: [{ type: 'text', text: 'first reply' }] })
|
|
insert('three', 'user', { text: 'second prompt' })
|
|
insert('last-empty', 'idle', { outcome: 'succeeded' })
|
|
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 2 })
|
|
expect(page?.items.map((item) => item.message.id)).toEqual(['opencode:two', 'opencode:three'])
|
|
expect(page?.hasMore).toBe(true)
|
|
const older = readOpenCodeTranscriptPage({
|
|
dbPath: path,
|
|
sessionId: 'session',
|
|
limit: 2,
|
|
beforeMessageRowId: page?.beforeMessageRowId ?? undefined
|
|
})
|
|
expect(older?.items.map((item) => item.message.id)).toEqual(['opencode:one'])
|
|
expect(older?.hasMore).toBe(false)
|
|
})
|
|
|
|
it('preserves tool identity, inputs, outputs and errors across in-place updates', () => {
|
|
const { db, path, insert } = fixture()
|
|
const content = [
|
|
{
|
|
type: 'tool',
|
|
id: 'call',
|
|
name: 'bash',
|
|
state: { status: 'running', input: { command: 'pwd' } }
|
|
}
|
|
]
|
|
insert('tool', 'assistant', { content })
|
|
const before = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 2 })
|
|
expect(before?.items[0]?.message.blocks).toEqual([
|
|
{
|
|
type: 'tool-call',
|
|
callId: 'call',
|
|
name: 'bash',
|
|
state: 'running',
|
|
input: { command: 'pwd' }
|
|
}
|
|
])
|
|
db.prepare('UPDATE session_message SET data = ?, time_updated = 2 WHERE id = ?').run(
|
|
JSON.stringify({
|
|
content: [
|
|
{
|
|
...content[0],
|
|
state: {
|
|
status: 'error',
|
|
input: { command: 'pwd' },
|
|
error: { message: 'denied' },
|
|
content: []
|
|
}
|
|
}
|
|
]
|
|
}),
|
|
'tool'
|
|
)
|
|
const after = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 2 })
|
|
expect(after?.items[0]?.fingerprint).not.toBe(before?.items[0]?.fingerprint)
|
|
expect(after?.items[0]?.message.blocks[0]).toMatchObject({ state: 'failed', callId: 'call' })
|
|
expect(after?.items[0]?.message.blocks[1]).toMatchObject({ isError: true, callId: 'call' })
|
|
expect(readOpenCodeTranscriptSignal(path, 'session')?.maxPartTimeUpdated).toBe(2)
|
|
})
|
|
|
|
it('surfaces provider errors and explicit interrupted boundaries', () => {
|
|
const { path, insert } = fixture()
|
|
insert('error', 'assistant', { error: { type: 'provider', message: 'Rate limit exceeded' } })
|
|
insert('boundary', 'idle', { outcome: 'interrupted' })
|
|
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 2 })
|
|
expect(page?.items.map((item) => item.message.blocks)).toEqual([
|
|
[{ type: 'text', text: 'Rate limit exceeded' }],
|
|
[{ type: 'text', text: 'Conversation interrupted' }]
|
|
])
|
|
})
|
|
|
|
it.each(['x', '😀'])(
|
|
'omits oversized %s content with a stable cursor and time fingerprint',
|
|
(character) => {
|
|
const { db, path, insert } = fixture()
|
|
insert('before', 'user', { text: 'before' })
|
|
insert('large', 'user', {
|
|
text: '',
|
|
files: [{ mime: 'image/png', data: character.repeat(2 * 1024 * 1024) }]
|
|
})
|
|
insert('after', 'assistant', { text: 'after' })
|
|
const args = { dbPath: path, sessionId: 'session', limit: 2 }
|
|
const page = readOpenCodeTranscriptPage(args)
|
|
expect(page?.items.map((item) => item.message.id)).toEqual([
|
|
'opencode:large',
|
|
'opencode:after'
|
|
])
|
|
expect(page?.items[0]).toMatchObject({
|
|
rowid: 2,
|
|
message: {
|
|
role: 'system',
|
|
timestamp: 1,
|
|
transcriptOffset: 2,
|
|
blocks: [
|
|
{
|
|
type: 'text',
|
|
text: AGENT_SESSION_HOST_STATUS_COPY['history-item-too-large'],
|
|
presentation: 'history-item-too-large'
|
|
}
|
|
]
|
|
}
|
|
})
|
|
expect(readOpenCodeTranscriptPage(args)?.items[0]?.fingerprint).toBe(
|
|
page?.items[0]?.fingerprint
|
|
)
|
|
db.prepare("UPDATE session_message SET time_updated = 2 WHERE id = 'large'").run()
|
|
expect(readOpenCodeTranscriptPage(args)?.items[0]?.fingerprint).not.toBe(
|
|
page?.items[0]?.fingerprint
|
|
)
|
|
expect(
|
|
readOpenCodeTranscriptPage({
|
|
...args,
|
|
beforeMessageRowId: page?.beforeMessageRowId ?? undefined
|
|
})?.items.map((item) => item.message.id)
|
|
).toEqual(['opencode:before'])
|
|
}
|
|
)
|
|
|
|
it('refuses excessive sparse scans instead of returning a partial transcript', () => {
|
|
const { db, path, insert } = fixture()
|
|
// The regression is the scan cap; avoid quadratic fixture scans between batches.
|
|
db.exec('CREATE INDEX session_message_sequence ON session_message (session_id, seq)')
|
|
db.exec('BEGIN')
|
|
for (let index = 0; index < 10001; index++) {
|
|
insert(String(index), 'idle', {})
|
|
}
|
|
db.exec('COMMIT')
|
|
expect(() =>
|
|
readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 1 })
|
|
).toThrow('read limit')
|
|
})
|
|
|
|
it('keeps the aggregate page byte budget when individual rows fit', () => {
|
|
const { db, path, insert } = fixture()
|
|
db.exec('BEGIN')
|
|
for (let index = 0; index < 18; index++) {
|
|
insert(String(index), 'user', { text: 'x'.repeat(1024 * 1024) })
|
|
}
|
|
db.exec('COMMIT')
|
|
expect(() =>
|
|
readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 18 })
|
|
).toThrow('read limit')
|
|
})
|
|
|
|
it('reconciles live streaming updates and stops after unsubscribe', async () => {
|
|
const { db, path, insert } = fixture()
|
|
insert('reply', 'assistant', { content: [{ type: 'text', text: 'partial' }] })
|
|
const snapshots: unknown[] = []
|
|
const replacements: unknown[] = []
|
|
const subscription = subscribeOpenCodeNativeChatTranscript(
|
|
{
|
|
agent: 'opencode',
|
|
sessionId: 'session',
|
|
resolvePollIntervalMs: 5,
|
|
onAppend: () => {},
|
|
onInitialSnapshot: (messages) => snapshots.push(messages),
|
|
onReplace: (messages) => replacements.push(messages)
|
|
},
|
|
undefined,
|
|
{
|
|
resolveDbPath: async () => path,
|
|
readSignal: async (dbPath, sessionId) => readOpenCodeTranscriptSignal(dbPath, sessionId),
|
|
readPage: async (args) => readOpenCodeTranscriptPage(args)
|
|
}
|
|
)
|
|
try {
|
|
await expect.poll(() => snapshots.length).toBe(1)
|
|
db.prepare('UPDATE session_message SET data = ?, time_updated = 2').run(
|
|
JSON.stringify({ content: [{ type: 'text', text: 'complete' }] })
|
|
)
|
|
await expect.poll(() => replacements.length).toBe(1)
|
|
expect(replacements[0]).toMatchObject([{ blocks: [{ text: 'complete' }] }])
|
|
subscription.unsubscribe()
|
|
db.prepare('UPDATE session_message SET time_updated = 3').run()
|
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
expect(replacements).toHaveLength(1)
|
|
} finally {
|
|
subscription.unsubscribe()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('preserves real v2 user image files with or without accompanying text', () => {
|
|
const { path, insert } = fixture()
|
|
const file = { data: 'YQ==', mime: 'image/png', source: { type: 'inline' }, name: 'tiny.png' }
|
|
insert('text-image', 'user', { text: 'see this', files: [file] })
|
|
insert('image-only', 'user', { text: '', files: [file] })
|
|
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 10 })
|
|
expect(page?.items.map((item) => item.message.blocks)).toEqual([
|
|
[
|
|
{ type: 'text', text: 'see this' },
|
|
{ type: 'image-ref', url: `data:${file.mime};base64,${file.data}`, alt: 'tiny.png' }
|
|
],
|
|
[{ type: 'image-ref', url: `data:${file.mime};base64,${file.data}`, alt: 'tiny.png' }]
|
|
])
|
|
})
|
|
|
|
it('prefers the migrated live v2 session over retained legacy tables for pages and signals', () => {
|
|
const { db, path, insert } = fixture()
|
|
db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY);
|
|
CREATE TABLE message (id TEXT, session_id TEXT, time_created INTEGER, time_updated INTEGER, data TEXT);
|
|
CREATE TABLE part (id TEXT, message_id TEXT, session_id TEXT, time_updated INTEGER, data TEXT);
|
|
INSERT INTO session VALUES ('session');
|
|
INSERT INTO message VALUES ('old', 'session', 1, 1, '{"role":"user"}');
|
|
INSERT INTO part VALUES ('part', 'old', 'session', 1, '{"type":"text","text":"frozen legacy"}');`)
|
|
insert('imported', 'user', { text: 'migrated prompt' })
|
|
insert('live', 'assistant', { content: [{ type: 'text', text: 'live reply' }] })
|
|
expect(readOpenCodeTranscriptSignal(path, 'session')?.messageCount).toBe(2)
|
|
expect(
|
|
readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'session', limit: 10 })?.items.map(
|
|
(item) => item.message.id
|
|
)
|
|
).toEqual(['opencode:imported', 'opencode:live'])
|
|
})
|