mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-03 21:35:13 +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.
76 lines
3.7 KiB
TypeScript
76 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { decodeZcode, toObservations } from '../../src/providers/zcode/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 { ZcodeSessionRecords } from '../../src/providers/zcode/index.js'
|
|
|
|
const context: DecodeContext = { privacyKey: 'k', providerId: 'zcode', sourceRef: 'ref' }
|
|
|
|
function records(): ZcodeSessionRecords {
|
|
return {
|
|
sessionId: 'sess-1',
|
|
usageRows: [
|
|
{ id: 'mu-1', turn_id: 'turn-1', model_id: 'GLM-5.2', input_tokens: 9125, output_tokens: 27, reasoning_tokens: 12, cache_creation_input_tokens: 0, cache_read_input_tokens: 8064, started_at: 1781981181862, completed_at: 1781981202412 },
|
|
{ id: 'mu-2', turn_id: 'turn-1', model_id: 'GLM-5.2', input_tokens: 200, output_tokens: 40, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, started_at: 1781981210000, completed_at: 1781981220000 },
|
|
// Zero-token row: skipped.
|
|
{ id: 'mu-zero', turn_id: 'turn-2', model_id: 'GLM-5.2', input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, started_at: 1781981230000, completed_at: 1781981231000 },
|
|
// No turn_id: no tools attached.
|
|
{ id: 'mu-3', turn_id: null, model_id: 'GLM-5.2', input_tokens: 500, output_tokens: 60, reasoning_tokens: 0, cache_creation_input_tokens: 100, cache_read_input_tokens: 0, started_at: 1781981240000, completed_at: null },
|
|
],
|
|
toolRows: [
|
|
{ turn_id: 'turn-1', tool_name: 'Bash' },
|
|
{ turn_id: 'turn-1', tool_name: 'Read' },
|
|
],
|
|
}
|
|
}
|
|
|
|
describe('zcode rich decode (moved to @codeburn/core)', () => {
|
|
it('decodes usage rows, splitting cached tokens and skipping zero-token rows', () => {
|
|
const { calls } = decodeZcode({ records: [records()], context })
|
|
expect(calls).toHaveLength(3)
|
|
|
|
const [first, second, third] = calls
|
|
expect(first).not.toHaveProperty('costUSD')
|
|
expect(first).not.toHaveProperty('costBasis')
|
|
expect(first!.inputTokens).toBe(1061) // 9125 - 8064 cached
|
|
expect(first!.cacheReadInputTokens).toBe(8064)
|
|
expect(first!.reasoningTokens).toBe(12)
|
|
expect(first!.tools).toEqual(['Bash', 'Read'])
|
|
expect(first!.turnId).toBe('turn-1')
|
|
expect(first!.deduplicationKey).toBe('zcode:mu-1')
|
|
|
|
// Same turn's second row gets no tools (already attached to the first).
|
|
expect(second!.tools).toEqual([])
|
|
expect(second!.turnId).toBe('turn-1')
|
|
|
|
// No turn_id -> no tools, turnId undefined.
|
|
expect(third!.tools).toEqual([])
|
|
expect(third!.turnId).toBeUndefined()
|
|
expect(third!.cacheCreationInputTokens).toBe(100)
|
|
})
|
|
|
|
it('threads a live seenKeys set so a repeated row id across passes drops', () => {
|
|
const seen = new Set<string>()
|
|
const first = decodeZcode({ records: [records()], context, seenKeys: seen }).calls
|
|
expect(first).toHaveLength(3)
|
|
const again = decodeZcode({ records: [records()], context, seenKeys: seen }).calls
|
|
expect(again).toEqual([])
|
|
})
|
|
|
|
it('toObservations produces a schema-valid, content-free envelope', () => {
|
|
const { calls } = decodeZcode({ records: [records()], context })
|
|
const { sessions } = toObservations(
|
|
{ sessionId: 'sess-1', projectPath: '/Users/me/proj', calls },
|
|
{ privacyKey: 'test-privacy-key', provider: 'zcode' },
|
|
)
|
|
const envelope = {
|
|
schemaVersion: OBSERVATION_SCHEMA_VERSION,
|
|
generator: { name: '@codeburn/core', version: '0.0.0-test' },
|
|
sessions,
|
|
}
|
|
expect(ObservationEnvelope.safeParse(envelope).success).toBe(true)
|
|
expect(sessions[0]?.calls.every(c => c.costBasis === 'estimated')).toBe(true)
|
|
})
|
|
})
|