mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-04 05:41:29 +00:00
Category B (sqlite) variant of the bridge migration: the sqlite driver and every
SQL query stay CLI-side. Each provider's `readRecords` opens the database, runs
the same queries as before, and hands the resulting rows (blob and all) to a
pure core decoder; `toProviderCall` maps the rich, cost-free decode back onto
ParsedProviderCall, where cost re-enters via the parser.ts pricing pass.
Per provider:
- crush: session row + dominant-model query -> one combined record. Crush stores
cost in dollars, so a row with cost > 0 carries `measuredCostUSD` (costBasis
'measured'); a zero-cost row falls back to token estimation, arm order intact.
- zcode: model_usage + tool_usage row sets -> one composite record. Each turn's
tools still attach to the first non-skipped usage row of that turn only.
- zed: threads rows handed over compressed; zstd decompression, JSON parsing and
per-request/cumulative-remainder accounting are pure. The Node >= 22.15 zstd
capability check stays host-side.
- forge: conversation row handed over with `context` still serialized; JSON
parsing and per-message decode are pure. Bash base-name extraction (and its
strip-ansi dependency) stays CLI-side over the decoder's raw command strings.
- goose: session + assistant tool-message + first-user-message rows, BLOB
columns pre-converted to text host-side, bundled into one composite record.
Validator fixes (original behavior is the authority):
- forge: the draft replaced the pre-migration `mapToolName` switch with an
object-literal lookup. Tool names come straight from conversation JSON, so
names colliding with Object.prototype members ("constructor", "toString",
"__proto__", "hasOwnProperty") resolved to inherited Functions / the prototype
object and were pushed into `tools` as non-strings instead of falling through
to the identity default. Restored the switch and pinned the arm in the fixture.
- zed: the draft routed the "skipped N unreadable Zed threads" notice into
record diagnostics, which the bridge discards, silently dropping a warning the
pre-migration decode printed. Re-emitted host-side from the diagnostics count
and pinned with a stderr assertion.
- Fixture coverage extended for the arms that were regression-blind: forge's
prototype-named tool calls, zed's aggregate stderr line, and goose's
single-turn `toolSequence` omission plus the unparseable-timestamp fallback.
Parity was verified independently of the bridge tests with a git-show harness
that runs the same fixtures through the pre-migration provider files and asserts
field-for-field equality, including the extra arms above.
106 lines
5.1 KiB
TypeScript
106 lines
5.1 KiB
TypeScript
import zlib from 'node:zlib'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { decodeZed, toObservations } from '../../src/providers/zed/index.js'
|
|
import { ObservationEnvelope } from '../../src/observations.js'
|
|
import { OBSERVATION_SCHEMA_VERSION } from '../../src/schema.js'
|
|
import type { DecodeContext } from '../../src/contracts.js'
|
|
import type { ZedThreadRow } from '../../src/providers/zed/index.js'
|
|
|
|
const context: DecodeContext = { privacyKey: 'k', providerId: 'zed', sourceRef: 'ref' }
|
|
|
|
const zstd = (zlib as { zstdCompressSync?: (buf: Buffer) => Buffer }).zstdCompressSync
|
|
const skipReason = !zstd ? 'zlib zstd not available — needs Node 22.15+; skipping' : null
|
|
|
|
function zstdRow(id: string, thread: unknown, opts: Partial<ZedThreadRow> = {}): ZedThreadRow {
|
|
return {
|
|
id,
|
|
summary: opts.summary ?? 'a thread',
|
|
updated_at: opts.updated_at ?? '2026-06-20T10:00:00Z',
|
|
data_type: 'zstd',
|
|
data: zstd!(Buffer.from(JSON.stringify(thread))),
|
|
...opts,
|
|
}
|
|
}
|
|
|
|
describe.skipIf(skipReason !== null)('zed rich decode (moved to @codeburn/core)', () => {
|
|
it('emits one call per request, plus a cumulative-remainder entry when the map undercounts', () => {
|
|
const row = zstdRow('thread-1', {
|
|
model: { provider: 'anthropic', model: 'claude-opus-4-8' },
|
|
request_token_usage: {
|
|
'req-1': { input_tokens: 1200, output_tokens: 300, cache_creation_input_tokens: 5000, cache_read_input_tokens: 90000 },
|
|
'req-2': { input_tokens: 800, output_tokens: 150 },
|
|
},
|
|
cumulative_token_usage: { input_tokens: 2000, output_tokens: 450 },
|
|
}, { summary: 'refactor the parser', updated_at: '2026-06-21T09:30:00Z' })
|
|
|
|
const { calls } = decodeZed({ records: [row], context })
|
|
expect(calls).toHaveLength(2)
|
|
const first = calls.find(c => c.deduplicationKey === 'zed:thread-1:req-1')!
|
|
expect(first.inputTokens).toBe(1200)
|
|
expect(first.cacheCreationInputTokens).toBe(5000)
|
|
expect(first.model).toBe('claude-opus-4-8')
|
|
expect(first.userMessage).toBe('refactor the parser')
|
|
expect(first.timestamp).toBe('2026-06-21T09:30:00.000Z')
|
|
expect(calls.reduce((s, c) => s + c.inputTokens, 0)).toBe(2000)
|
|
})
|
|
|
|
it('skips non-zstd/json rows and malformed blobs, recording a diagnostic without dropping healthy threads', () => {
|
|
const badType: ZedThreadRow = { id: 'bad-type', summary: null, updated_at: '2026-06-20T10:00:00Z', data_type: 'protobuf', data: Buffer.from('{}') }
|
|
const badBlob: ZedThreadRow = { id: 'bad-blob', summary: null, updated_at: '2026-06-20T10:00:00Z', data_type: 'zstd', data: Buffer.from('not zstd at all') }
|
|
const good = zstdRow('good', { model: { model: 'claude-opus-4-8' }, request_token_usage: { 'req-1': { input_tokens: 10, output_tokens: 5 } } })
|
|
|
|
const { calls, diagnostics } = decodeZed({ records: [badType, badBlob, good], context })
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.sessionId).toBe('good')
|
|
expect(diagnostics.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('reads legacy uncompressed json rows', () => {
|
|
const row: ZedThreadRow = {
|
|
id: 'legacy', summary: null, updated_at: '2026-06-22T11:00:00Z', data_type: 'json',
|
|
data: Buffer.from(JSON.stringify({ model: { model: 'claude-sonnet-4-6' }, request_token_usage: { 'req-1': { input_tokens: 40, output_tokens: 8 } } })),
|
|
}
|
|
const { calls } = decodeZed({ records: [row], context })
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.inputTokens).toBe(40)
|
|
expect(calls[0]!.model).toBe('claude-sonnet-4-6')
|
|
})
|
|
|
|
it('skips threads whose usage is entirely zero', () => {
|
|
const row = zstdRow('zero', {
|
|
model: { model: 'claude-opus-4-8' },
|
|
request_token_usage: { 'req-1': { input_tokens: 0, output_tokens: 0 } },
|
|
cumulative_token_usage: { input_tokens: 0, output_tokens: 0 },
|
|
})
|
|
expect(decodeZed({ records: [row], context }).calls).toEqual([])
|
|
})
|
|
|
|
it('threads a live seenKeys set so a repeated thread across passes drops', () => {
|
|
const row = zstdRow('thread-3', { model: { model: 'claude-opus-4-8' }, request_token_usage: { 'req-1': { input_tokens: 100, output_tokens: 50 } } })
|
|
const seen = new Set<string>()
|
|
expect(decodeZed({ records: [row], context, seenKeys: seen }).calls).toHaveLength(1)
|
|
expect(decodeZed({ records: [row], context, seenKeys: seen }).calls).toEqual([])
|
|
})
|
|
|
|
it('toObservations produces a schema-valid, content-free envelope', () => {
|
|
const row = zstdRow('thread-4', {
|
|
model: { model: 'claude-opus-4-8' },
|
|
request_token_usage: { 'req-1': { input_tokens: 100, output_tokens: 50 } },
|
|
}, { summary: 'do the thing' })
|
|
const { calls } = decodeZed({ records: [row], context })
|
|
const { sessions } = toObservations(
|
|
{ sessionId: 'thread-4', projectPath: '/Users/t/proj', calls },
|
|
{ privacyKey: 'test-privacy-key', provider: 'zed' },
|
|
)
|
|
const envelope = {
|
|
schemaVersion: OBSERVATION_SCHEMA_VERSION,
|
|
generator: { name: '@codeburn/core', version: '0.0.0-test' },
|
|
sessions,
|
|
}
|
|
expect(ObservationEnvelope.safeParse(envelope).success).toBe(true)
|
|
// The thread summary must never cross into the envelope.
|
|
expect(JSON.stringify(envelope)).not.toContain('do the thing')
|
|
})
|
|
})
|