codeburn/packages/core/tests/providers/quickdesk-decode.test.ts
iamtoruk 4ef24e9fcd refactor(core): tail migrations — hermes, warp, cursor-agent, quickdesk, devin (phase 8, sqlite batch 2)
Move the pure record decode for five sqlite-backed providers into
@codeburn/core, keeping discovery, sqlite I/O, pricing, and bash
base-name extraction CLI-side behind the dual-registry bridge.

sqlite variant: the host opens the database, runs the SQL, textualizes
BLOBs (warp stylized_command, quickdesk content/tool_names), probes
sqlite_master table variants (quickdesk), and re-throws SQLITE_BUSY so a
transient lock on a live database retries instead of being cached as an
empty result (hermes). The core decoders receive already-normalized rows.

Validator fixes over the drafted migration (original behavior is the
authority in every case):

- warp: restored the `run_command` tool-name check as an identity
  comparison. The draft had rewritten it as a bare Record lookup, so
  'constructor'/'toString'/'__proto__' resolved to inherited Object
  members instead of passing through. Added a regression test.
- warp: restored `safeNumber` to the host's semantics (positive finite
  numbers only). The draft's rewrite accepted numeric strings and
  negative values, changing token budgets and dominant-model selection
  for untrusted conversation_data.
- warp: `estimateTokensFromChars` now mirrors the host helper exactly.
- devin: model DISPLAY names move back CLI-side. The draft shipped a
  7-line regex standing in for the host's `getShortModelName`, which
  mis-rendered every non-Claude model id. The decoder now emits the raw
  generation_model / model_name ids and the CLI formats them.
- devin: dropped the added `costBasis: 'measured'`; the pre-migration
  call carried `costUSD` with no basis marker. Golden corrected.
- quickdesk: fixed an inverted source-variant predicate that routed
  `sourceId: 'metrics'` on a sessions.db path (and `sourceId:
  'sessions-db'` on a metrics path) to the wrong parser.
- cursor-agent: the "unrecognized transcript format" warning is now
  re-emitted from the real decode result via a decode wrapper. The draft
  re-derived `recognized` host-side as `hasUser && hasAssistant`, which
  silently dropped the warning whenever a jsonl transcript had both roles
  but produced no turns.
- cursor-agent: restored the per-conversation summary cache, so the
  attribution database is opened once per conversation id, not per source.
- cursor-agent/devin: the session id is derived once, host-side, and
  passed through, removing a second derivation that could disagree with
  the host's on Windows-style paths.
- hermes: `browser_vision` maps to Vision, matching the original table.

Goldens re-verified by running each bridge fixture through the
pre-migration provider implementation: all five reproduce field-for-field.
2026-07-26 20:26:18 -07:00

268 lines
10 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { decodeQuickdesk, toObservations } from '../../src/providers/quickdesk/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 {
QuickdeskDatabaseInput,
QuickdeskMetricsInput,
QuickdeskSessionMetadata,
} from '../../src/providers/quickdesk/types.js'
const context: DecodeContext = { privacyKey: 'k', providerId: 'quickdesk', sourceRef: 'ref' }
function makeSession(overrides: Partial<QuickdeskSessionMetadata> = {}): QuickdeskSessionMetadata {
return {
id: 'sess-a',
title: 'Test',
agentMode: 'agent',
createdAt: 1783987200,
deleted: false,
firstUserMessage: 'hello',
inputChars: 0,
outputChars: 0,
tools: [],
...overrides,
}
}
function makeMetricsInput(overrides: Partial<QuickdeskMetricsInput> = {}): QuickdeskMetricsInput {
return {
variant: 'metrics',
records: [],
sessions: new Map(),
project: 'default',
projectPath: '/Users/me/projects/codeburn',
fileId: 'metrics-2026-07-14.jsonl',
...overrides,
}
}
function makeDatabaseInput(overrides: Partial<QuickdeskDatabaseInput> = {}): QuickdeskDatabaseInput {
return {
variant: 'database',
sessions: [],
meteredSessionIds: new Set(),
project: 'default',
projectPath: '/Users/me/projects/codeburn',
...overrides,
}
}
describe('quickdesk rich decode (moved to @codeburn/core)', () => {
it('decodes a metrics record into a cost-free rich call', () => {
const input = makeMetricsInput({
records: [
{ record: { Model: 'claude-sonnet-4-5', InputTokens: 120, OutputTokens: 30, CostUSD: 0.0042 } },
],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
const call = calls[0]!
expect(call.provider).toBe('quickdesk')
expect(call.model).toBe('claude-sonnet-4-5')
expect(call.inputTokens).toBe(120)
expect(call.outputTokens).toBe(30)
expect(call.recordedCost).toBe(0.0042)
expect(call.project).toBe('default')
expect(call.projectPath).toBe('/Users/me/projects/codeburn')
})
it('estimates cost when metrics record lacks CostUSD', () => {
const input = makeMetricsInput({
records: [{ record: { Model: 'claude-sonnet-4-5', InputTokens: 40, OutputTokens: 10 } }],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
expect(calls[0]!).not.toHaveProperty('recordedCost')
})
it('links sqlite session metadata to metrics by session_id', () => {
const sessions = new Map<string, QuickdeskSessionMetadata>()
sessions.set('linked', makeSession({
id: 'linked',
firstUserMessage: 'linked prompt',
tools: ['read_file'],
}))
const input = makeMetricsInput({
records: [{ record: { session_id: 'linked', Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } }],
sessions,
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
expect(calls[0]!.userMessage).toBe('linked prompt')
expect(calls[0]!.tools).toEqual(['Read'])
expect(calls[0]!.sessionId).toBe('linked')
})
it('merges tools from metrics ToolName and linked session metadata', () => {
const sessions = new Map<string, QuickdeskSessionMetadata>()
sessions.set('merged', makeSession({ id: 'merged', tools: ['write_file'] }))
const input = makeMetricsInput({
records: [
{ record: { session_id: 'merged', ToolName: 'read_file' } },
{ record: { session_id: 'merged', Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } },
],
sessions,
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
expect(calls[0]!.tools).toEqual(['Read', 'Edit'])
})
it('dedups metrics calls via the live seenKeys set', () => {
const input = makeMetricsInput({
records: [{ record: { Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } }],
})
const seen = new Set<string>()
const first = decodeQuickdesk({ records: [input], context, seenKeys: seen }).calls
expect(first).toHaveLength(1)
const again = decodeQuickdesk({ records: [input], context, seenKeys: seen }).calls
expect(again).toEqual([])
})
it('skips deleted linked sessions', () => {
const sessions = new Map<string, QuickdeskSessionMetadata>()
sessions.set('deleted', makeSession({ id: 'deleted', deleted: true }))
const input = makeMetricsInput({
records: [{ record: { session_id: 'deleted', Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } }],
sessions,
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(0)
})
it('keeps metrics records with zero tokens because usageRecord does not filter them', () => {
const input = makeMetricsInput({
records: [{ record: { Model: 'claude-sonnet-4-5', InputTokens: 0, OutputTokens: 0 } }],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
expect(calls[0]!.inputTokens).toBe(0)
expect(calls[0]!.outputTokens).toBe(0)
})
it('skips metrics records missing required fields', () => {
const input = makeMetricsInput({
records: [
{ record: { Model: 'claude-sonnet-4-5', InputTokens: 10 } },
{ record: { InputTokens: 10, OutputTokens: 5 } },
],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(0)
})
it('decodes database sessions into estimated rich calls', () => {
const input = makeDatabaseInput({
sessions: [makeSession({ id: 'db-sess', inputChars: 12, outputChars: 8, firstUserMessage: 'db prompt', tools: ['Bash'] })],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('quickdesk-auto')
expect(calls[0]!.inputTokens).toBe(3)
expect(calls[0]!.outputTokens).toBe(2)
expect(calls[0]!.tools).toEqual(['Bash'])
expect(calls[0]!.userMessage).toBe('db prompt')
expect(calls[0]!).not.toHaveProperty('recordedCost')
})
it('skips database sessions with zero estimated tokens', () => {
const input = makeDatabaseInput({
sessions: [makeSession({ id: 'empty', inputChars: 0, outputChars: 0 })],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(0)
})
it('skips metered database sessions', () => {
const input = makeDatabaseInput({
sessions: [makeSession({ id: 'metered', inputChars: 12, outputChars: 8 })],
meteredSessionIds: new Set(['metered']),
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(0)
})
it('skips deleted database sessions', () => {
const input = makeDatabaseInput({
sessions: [makeSession({ id: 'deleted', inputChars: 12, outputChars: 8, deleted: true })],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(0)
})
it('treats millisecond created_at values as milliseconds', () => {
const input = makeDatabaseInput({
sessions: [makeSession({ id: 'ms', createdAt: 1783987200000, inputChars: 4, outputChars: 4 })],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls).toHaveLength(1)
expect(calls[0]!.timestamp).toBe('2026-07-14T00:00:00.000Z')
})
it('dedups database calls via the live seenKeys set', () => {
const input = makeDatabaseInput({
sessions: [makeSession({ id: 'db-sess', inputChars: 12, outputChars: 8 })],
})
const seen = new Set<string>()
const first = decodeQuickdesk({ records: [input], context, seenKeys: seen }).calls
expect(first).toHaveLength(1)
const again = decodeQuickdesk({ records: [input], context, seenKeys: seen }).calls
expect(again).toEqual([])
})
it('maps tool names through the quickdesk tool-name map', () => {
const input = makeMetricsInput({
records: [
{ record: { session_id: 'tools', ToolName: 'readFile' } },
{ record: { session_id: 'tools', ToolName: 'runCommand' } },
{ record: { session_id: 'tools', ToolName: 'unknownTool' } },
{ record: { session_id: 'tools', Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } },
],
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls[0]!.tools).toEqual(['Read', 'Bash', 'unknownTool'])
})
it('falls back to file-date timestamp when _aws.Timestamp is absent', () => {
const input = makeMetricsInput({
records: [{ record: { Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } }],
fileId: 'metrics-2026-05-01.jsonl',
})
const { calls } = decodeQuickdesk({ records: [input], context })
expect(calls[0]!.timestamp).toBe('2026-05-01T00:00:00.000Z')
})
it('toObservations produces a schema-valid, content-free envelope', () => {
const input = makeMetricsInput({
records: [{ record: { Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5 } }],
})
const { calls } = decodeQuickdesk({ records: [input], context })
const { sessions } = toObservations(
{ sessionId: 'sess-a', projectPath: '/Users/me/projects/codeburn', calls },
{ privacyKey: 'test-privacy-key', provider: 'quickdesk' },
)
const envelope = {
schemaVersion: OBSERVATION_SCHEMA_VERSION,
generator: { name: '@codeburn/core', version: '0.0.0-test' },
sessions,
}
expect(ObservationEnvelope.safeParse(envelope).success).toBe(true)
})
it('toObservations emits measured cost when recordedCost is present', () => {
const input = makeMetricsInput({
records: [{ record: { Model: 'claude-sonnet-4-5', InputTokens: 10, OutputTokens: 5, CostUSD: 0.001 } }],
})
const { calls } = decodeQuickdesk({ records: [input], context })
const { sessions } = toObservations(
{ sessionId: 'sess-a', projectPath: '/Users/me/projects/codeburn', calls },
{ privacyKey: 'test-privacy-key', provider: 'quickdesk' },
)
const call = sessions[0]!.calls[0]!
expect(call.costBasis).toBe('measured')
expect(call.measuredCostUSD).toBe(0.001)
})
})