codeburn/tests/sync-cb3-wire.test.ts
iamtoruk 62cb4f8630 test: isolate the real home on Windows and make the CLI suite pass there
os.homedir() reads USERPROFILE on Windows and never HOME, so the
isolation setup and every test that moved home were still resolving the
developer's real home directory. Fixtures came back empty, real sessions
leaked into counts, and the code under test wrote plans, receipts and a
sync ledger into the real ~/.config/codeburn during a run.

USERPROFILE now joins the redirected variables and a shared setHome()
helper moves HOME and USERPROFILE together; every spawn env carries both.
The remaining Windows-only failures were POSIX-shaped fixtures: chmod
tricks that only toggle a read-only attribute on NTFS, pid 1 as an
always-alive lock holder, splitting paths on '/', bare tar, mode-bit
expectations, and per-file timeouts too short for the cost of a tsx spawn.
Each gets a platform-aware equivalent so nothing is skipped on Windows and
the POSIX branches stay byte-for-byte what they were.

Full run on Windows: 267 files passed, 2 skipped, 3592 tests passed.
2026-09-02 13:43:57 -07:00

309 lines
13 KiB
TypeScript

/**
* CB-3 wire tests (Teams boundary spec section 7): the additive usage-span
* attributes and the codeburn.coverage_through batch marker.
*
* Golden-pins the exact attribute set for a fully-proven span plus every
* absent-when-unproven case (no lineage, unresolvable lineage, undecidable
* subscription, zero cache tokens, untrusted watermark value).
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mkdtemp, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import {
buildOtlpPayload,
deriveTraceId,
type CallWithSession,
type OtlpAttribute,
} from '../src/sync/otlp.js'
import type { ParsedApiCall, ProjectSummary, TokenUsage } from '../src/types.js'
import type { PlanMap } from '../src/config.js'
import { setHome } from './setup/home.js'
// ── Helpers ───────────────────────────────────────────────────────────
function makeUsage(overrides?: Partial<TokenUsage>): TokenUsage {
return {
inputTokens: 1000,
outputTokens: 500,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
...overrides,
}
}
function makeCall(overrides?: Partial<ParsedApiCall>): ParsedApiCall {
return {
provider: 'claude',
model: 'claude-sonnet-4-6',
usage: makeUsage(),
costUSD: 0.05,
tools: [],
mcpTools: [],
skills: [],
subagentTypes: [],
hasAgentSpawn: false,
hasPlanMode: false,
speed: 'standard',
timestamp: '2026-07-10T10:00:00.000Z',
bashCommands: [],
deduplicationKey: 'cb3:key:1',
...overrides,
}
}
function spanAttrs(payload: ReturnType<typeof buildOtlpPayload>, index = 0): OtlpAttribute[] {
return payload.resourceSpans[0]!.scopeSpans[0]!.spans[index]!.attributes
}
function attrKeys(attrs: OtlpAttribute[]): string[] {
return attrs.map(a => a.key)
}
// ── buildOtlpPayload: CB-3 attributes ────────────────────────────────
describe('buildOtlpPayload CB-3 attributes', () => {
// GOLDEN - the exact attribute set (and order) of a fully-proven span.
// Add-only contract: receivers ignoring unknown keys must lose nothing, so
// new keys append after the v1 set and none of the v1 keys move.
it('golden: fully-proven span carries the exact CB-3 attribute set', () => {
const cws: CallWithSession = {
call: makeCall({
usage: makeUsage({ cacheReadInputTokens: 800, cacheCreationInputTokens: 200 }),
}),
sessionId: 'root-session',
project: 'my-project',
workingDirectory: '/workspace/my-project',
session: {
callCount: 3,
durationMs: 61_000,
workUnitId: deriveTraceId('root-session'),
sessionRole: 'root',
subscriptionCovered: true,
},
}
const payload = buildOtlpPayload([cws], { coverageThrough: '2026-08-24' })
expect(payload.resourceSpans[0]!.resource.attributes).toEqual([
{ key: 'codeburn.device_id', value: { stringValue: expect.stringMatching(/^[0-9a-f]{16}$/) } },
{ key: 'codeburn.coverage_through', value: { stringValue: '2026-08-24' } },
])
expect(spanAttrs(payload)).toEqual([
{ key: 'ai.provider', value: { stringValue: 'claude' } },
{ key: 'ai.model', value: { stringValue: 'claude-sonnet-4-6' } },
{ key: 'ai.input_tokens', value: { intValue: '1000' } },
// claude bills reasoning inside output, so billable output stays 500
{ key: 'ai.output_tokens', value: { intValue: '500' } },
{ key: 'ai.cost_usd', value: { doubleValue: 0.05 } },
{ key: 'ai.speed', value: { stringValue: 'standard' } },
{ key: 'ai.project', value: { stringValue: 'my-project' } },
{ key: 'ai.cost_estimated', value: { boolValue: false } },
{ key: 'ai.work_unit_id', value: { stringValue: deriveTraceId('root-session') } },
{ key: 'ai.session_role', value: { stringValue: 'root' } },
{ key: 'ai.lineage_evidence', value: { stringValue: 'provider-recorded' } },
{ key: 'ai.cache_read_tokens', value: { intValue: '800' } },
{ key: 'ai.cache_write_tokens', value: { intValue: '200' } },
{ key: 'ai.call_count', value: { intValue: '3' } },
{ key: 'ai.session_duration_ms', value: { intValue: '61000' } },
{ key: 'ai.subscription_covered', value: { boolValue: true } },
])
})
it('no session context: none of the session-level attributes appear', () => {
const payload = buildOtlpPayload([{ call: makeCall(), sessionId: 's1' }])
const keys = attrKeys(spanAttrs(payload))
for (const key of [
'ai.work_unit_id', 'ai.session_role', 'ai.lineage_evidence',
'ai.call_count', 'ai.session_duration_ms', 'ai.subscription_covered',
'ai.cache_read_tokens', 'ai.cache_write_tokens',
]) {
expect(keys).not.toContain(key)
}
})
it('lineage trio falls together when any value fails sanitization', () => {
const payload = buildOtlpPayload([{
call: makeCall(),
sessionId: 's1',
session: { callCount: 1, workUnitId: 'bad id with spaces', sessionRole: 'root' },
}])
const keys = attrKeys(spanAttrs(payload))
expect(keys).not.toContain('ai.work_unit_id')
expect(keys).not.toContain('ai.session_role')
expect(keys).not.toContain('ai.lineage_evidence')
// Unrelated proven fields still emit
expect(keys).toContain('ai.call_count')
})
it('cache_read uses the billable-consistent max of both vocabularies', () => {
const payload = buildOtlpPayload([{
call: makeCall({ usage: makeUsage({ cacheReadInputTokens: 800, cachedInputTokens: 900 }) }),
sessionId: 's1',
}])
expect(spanAttrs(payload)).toContainEqual({ key: 'ai.cache_read_tokens', value: { intValue: '900' } })
})
it('zero cache tokens are absent, not sent as zero', () => {
const payload = buildOtlpPayload([{ call: makeCall(), sessionId: 's1' }])
const keys = attrKeys(spanAttrs(payload))
expect(keys).not.toContain('ai.cache_read_tokens')
expect(keys).not.toContain('ai.cache_write_tokens')
})
it('coverage_through omitted without the option and on an unsafe value', () => {
const without = buildOtlpPayload([{ call: makeCall(), sessionId: 's1' }])
expect(attrKeys(without.resourceSpans[0]!.resource.attributes)).toEqual(['codeburn.device_id'])
const unsafe = buildOtlpPayload([{ call: makeCall(), sessionId: 's1' }], { coverageThrough: '/etc/passwd' })
expect(attrKeys(unsafe.resourceSpans[0]!.resource.attributes)).toEqual(['codeburn.device_id'])
})
it('identity is unchanged: traceId/spanId ignore every new field', () => {
const bare = buildOtlpPayload([{ call: makeCall(), sessionId: 's1' }])
const full = buildOtlpPayload([{
call: makeCall(),
sessionId: 's1',
session: {
callCount: 9,
durationMs: 1000,
workUnitId: deriveTraceId('s1'),
sessionRole: 'root',
subscriptionCovered: false,
},
}])
const bareSpan = bare.resourceSpans[0]!.scopeSpans[0]!.spans[0]!
const fullSpan = full.resourceSpans[0]!.scopeSpans[0]!.spans[0]!
expect(fullSpan.traceId).toBe(bareSpan.traceId)
expect(fullSpan.spanId).toBe(bareSpan.spanId)
})
})
// ── collectUnsentCalls: session wire context ─────────────────────────
let tmpDir: string
const originalHome = process.env.HOME
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'codeburn-cb3-'))
setHome(tmpDir)
process.env.XDG_CACHE_HOME = join(tmpDir, '.cache')
})
afterEach(async () => {
setHome(originalHome)
await rm(tmpDir, { recursive: true, force: true })
})
function makeSession(overrides: Record<string, unknown>): ProjectSummary['sessions'][number] {
return {
sessionId: 'sess-1',
project: 'proj',
firstTimestamp: '2026-07-10T10:00:00.000Z',
lastTimestamp: '2026-07-10T10:05:00.000Z',
turns: [{ assistantCalls: [makeCall({ deduplicationKey: `cb3:${Math.random()}` })], timestamp: '2026-07-10T10:00:00.000Z' }],
...overrides,
} as unknown as ProjectSummary['sessions'][number]
}
function makeProject(sessions: ProjectSummary['sessions'][number][]): ProjectSummary[] {
return [{ project: 'proj', sessions }] as unknown as ProjectSummary[]
}
async function collect(projects: ProjectSummary[], plans?: PlanMap) {
const { collectUnsentCalls } = await import('../src/sync/push.js')
return collectUnsentCalls(projects, Date.now(), plans ? { plans } : undefined)
}
describe('collectUnsentCalls session wire context', () => {
it('root lineage: workUnitId is deriveTraceId of the session itself', async () => {
const { allCalls } = await collect(makeProject([
makeSession({ sessionId: 'root-1', lineage: { role: 'root', evidence: 'provider-recorded' } }),
]))
expect(allCalls[0]!.session).toMatchObject({
workUnitId: deriveTraceId('root-1'),
sessionRole: 'root',
})
})
it('child lineage with in-range parent folds under the parent work unit', async () => {
const { allCalls } = await collect(makeProject([
makeSession({ sessionId: 'root-1', lineage: { role: 'root', evidence: 'provider-recorded' } }),
makeSession({ sessionId: 'child-1', lineage: { role: 'child', evidence: 'provider-recorded', parentSessionId: 'root-1' } }),
]))
const child = allCalls.find(c => c.sessionId === 'child-1')!
expect(child.session).toMatchObject({ workUnitId: deriveTraceId('root-1'), sessionRole: 'child' })
})
it('child lineage naming an out-of-range parent emits nothing (fail closed)', async () => {
const { allCalls } = await collect(makeProject([
makeSession({ sessionId: 'child-1', lineage: { role: 'child', evidence: 'provider-recorded', parentSessionId: 'gone' } }),
]))
expect(allCalls[0]!.session?.workUnitId).toBeUndefined()
expect(allCalls[0]!.session?.sessionRole).toBeUndefined()
})
it('no lineage: none of the three lineage fields', async () => {
const { allCalls } = await collect(makeProject([makeSession({})]))
expect(allCalls[0]!.session?.workUnitId).toBeUndefined()
expect(allCalls[0]!.session?.sessionRole).toBeUndefined()
})
it('call_count counts every span the session contributes', async () => {
const session = makeSession({
turns: [
{ assistantCalls: [makeCall({ deduplicationKey: 'cb3:a' }), makeCall({ deduplicationKey: 'cb3:b' })], timestamp: '2026-07-10T10:00:00.000Z' },
{ assistantCalls: [makeCall({ deduplicationKey: 'cb3:c' })], timestamp: '2026-07-10T10:01:00.000Z' },
],
})
const { allCalls } = await collect(makeProject([session]))
expect(allCalls).toHaveLength(3)
for (const c of allCalls) expect(c.session?.callCount).toBe(3)
})
it('duration derives from provider-recorded first/last timestamps', async () => {
const { allCalls } = await collect(makeProject([makeSession({})]))
expect(allCalls[0]!.session?.durationMs).toBe(5 * 60 * 1000)
})
it('duration omitted when timestamps are missing or unordered', async () => {
const missing = await collect(makeProject([makeSession({ firstTimestamp: '', lastTimestamp: '' })]))
expect(missing.allCalls[0]!.session?.durationMs).toBeUndefined()
const reversed = await collect(makeProject([
makeSession({ firstTimestamp: '2026-07-10T10:05:00.000Z', lastTimestamp: '2026-07-10T10:00:00.000Z' }),
]))
expect(reversed.allCalls[0]!.session?.durationMs).toBeUndefined()
})
it('subscription_covered: plan for the call provider decides true', async () => {
const plans: PlanMap = {
claude: { id: 'claude-pro', monthlyUsd: 20, provider: 'claude', setAt: '2026-07-01T00:00:00Z' },
}
const { allCalls } = await collect(makeProject([makeSession({})]), plans)
expect(allCalls[0]!.session?.subscriptionCovered).toBe(true)
})
it('subscription_covered: no plan, unproxied cwd decides false', async () => {
const { allCalls } = await collect(makeProject([makeSession({ workingDirectory: '/workspace/proj' })]))
expect(allCalls[0]!.session?.subscriptionCovered).toBe(false)
})
it('subscription_covered: no plan and no cwd omits the attribute', async () => {
const { allCalls } = await collect(makeProject([makeSession({})]))
expect(allCalls[0]!.session?.subscriptionCovered).toBeUndefined()
})
it("subscription_covered: a plan recorded as 'none' is not coverage", async () => {
const plans: PlanMap = {
claude: { id: 'none', monthlyUsd: 0, provider: 'claude', setAt: '2026-07-01T00:00:00Z' },
}
const { allCalls } = await collect(makeProject([makeSession({ workingDirectory: '/workspace/proj' })]), plans)
expect(allCalls[0]!.session?.subscriptionCovered).toBe(false)
})
})