test(cache): make the round-2 review findings fail when broken

Round 2 of the independent review proved five things by mutation: it broke the
behavior and the tests stayed green. Every one is now pinned.

The most important invariant in this change was the least guarded. Copilot must
have NO entry in PROVIDER_ENV_VARS - declaring any of its nine reads moves its
fingerprint and re-opens the durable history-loss path - but only one of the
nine was covered, so declaring any of the other eight passed the whole suite.
Now the absence of the entry is asserted directly, and all nine vars are
table-tested for fingerprint stability.

Doctor stops blaming parse-only overrides for a failed discovery.
CODEBURN_CURSOR_MAX_BUBBLES caps how many bubbles Cursor parses and
KIMI_MODEL_NAME renames an attributed model; neither relocates anything, so
"NOTHING FOUND (override CODEBURN_CURSOR_MAX_BUBBLES set...)" pointed the user
at the wrong thing. Both join NON_DISCOVERY_ENV_VARS, which exists for exactly
this, and both still appear in Details - only the verdict's blame line changes.

The secret-redaction and ambient-suppression tests are table-driven over both
names each covers, since removing either second name (VERCEL_OIDC_TOKEN,
LOCALAPPDATA) previously leaked or surfaced it with every test still passing.

The changelog no longer claims a one-time re-parse for the Vercel gateway: it
is a network provider re-fetched on every writable run, so its declaration is a
read-only-path correction, not a migration. Fourteen file-backed providers
migrate once.
This commit is contained in:
ozymandiashh 2026-08-05 05:58:17 +03:00
parent 9c9a37d4bf
commit a67bd279a6
4 changed files with 136 additions and 54 deletions

View file

@ -19,7 +19,7 @@
- **`--provider <x>` no longer leaks Claude spend into the detail panels.** A provider-filtered run still ran the Claude scan, whose orphan pass re-injected every cached Claude session, so By Project / By Model / By Activity showed Claude usage under, e.g., `--provider cursor` while the headline was correct. (#872, thanks @ozymandiashh)
- **A degraded session parse no longer freezes daily history.** A read-only parse that served a stale or missing session file was treated as complete and finalized days it never covered, freezing warm-cache ingestion; a corrupt refresh lock is now recovered rather than ending ingestion, and a legitimately idle tail is no longer re-derived on every launch. (#856, thanks @avs-io)
- **Pi / Oh My Pi transcripts with a leading title record are discovered.** OMP writes a `type: "title"` line before the session header; discovery now scans a bounded number of leading lines for the first session record instead of requiring it on the first physical line. (#846, #859, thanks @jbspeakr, @avs-io)
- **Nine providers served silently stale numbers after you pointed their env override at a different profile or root.** Kiro, Grok, Kimi, Mux, Mistral Vibe, Zerostack, Codebuff, Goose and Crush each honor an env var that relocates where discovery looks, but the var was never declared in the provider env fingerprint, so the cache section survived the change and kept reporting sessions parsed from the old root — with no diagnostic anywhere. The fix declares those vars, the adjacent OS-set path variables that resolve a discovery root for Claude, IBM Bob, Open Design and Kilo Code on Windows and Linux, Cursor's parse-budget override, and the Vercel AI Gateway credential — which must invalidate the fingerprint because a read-only refresh serves the cached report and would otherwise keep reporting the previous account's usage after a swap. Your next run re-parses all fifteen providers whose declarations changed — the nine above plus Claude, Cursor, Open Design, IBM Bob, Kilo Code and Vercel AI Gateway — once, and only once; Copilot is deliberately NOT included, because declaring its overrides would force a re-parse that can drop OTel history only the cache still holds; `codeburn doctor` names deliberate overrides including the XDG_* vars, never the Windows ambient APPDATA / LOCALAPPDATA, and redacts credential values. (#920)
- **Nine providers served silently stale numbers after you pointed their env override at a different profile or root.** Kiro, Grok, Kimi, Mux, Mistral Vibe, Zerostack, Codebuff, Goose and Crush each honor an env var that relocates where discovery looks, but the var was never declared in the provider env fingerprint, so the cache section survived the change and kept reporting sessions parsed from the old root — with no diagnostic anywhere. The fix declares those vars, the adjacent OS-set path variables that resolve a discovery root for Claude, IBM Bob, Open Design and Kilo Code on Windows and Linux, Cursor's parse-budget override, and the Vercel AI Gateway credential — which must invalidate the fingerprint because a read-only refresh serves the cached report and would otherwise keep reporting the previous account's usage after a swap. Your next run re-parses the fourteen file-backed providers whose declarations changed — the nine above plus Claude, Cursor, Open Design, IBM Bob and Kilo Code — once, and only once; the Vercel AI Gateway declaration is a read-only-path correction, not a migration (its report is re-fetched on every writable run anyway); Copilot is deliberately NOT included, because declaring its overrides would force a re-parse that can drop OTel history only the cache still holds; `codeburn doctor` names deliberate overrides including the XDG_* vars, never the Windows ambient APPDATA / LOCALAPPDATA, and redacts credential values. (#920)
### Fixed
- Claude Desktop and Cowork sessions are discovered for Windows Microsoft Store (MSIX) installs. (#611)

View file

@ -99,10 +99,18 @@ const PARSE_CALL_CAP = 500
// (readdir/stat only) still runs, so session counts stay meaningful.
const PARSE_SPAWNS = new Set(['antigravity'])
// CodeBurn's own cache location: listed in PROVIDER_ENV_VARS for cache
// fingerprinting, but it is not a discovery path, so it must never be blamed
// in a NOTHING FOUND hint.
const NON_DISCOVERY_ENV_VARS = new Set(['CODEBURN_CACHE_DIR'])
// Vars listed in PROVIDER_ENV_VARS for cache fingerprinting that are NOT
// discovery paths: a change to them can never explain "nothing was
// discovered", so they must never be blamed in a NOTHING FOUND hint.
// - CODEBURN_CACHE_DIR: CodeBurn's own cache location — where the cache
// file lives, not where sessions are discovered.
// - CODEBURN_CURSOR_MAX_BUBBLES: caps how many bubbles Cursor parses
// (src/providers/cursor.ts:692) — a parse budget, not a discovery root.
// - KIMI_MODEL_NAME: renames the model attributed to Kimi sessions
// (src/providers/kimi.ts:155) — attribution, not discovery.
// All three still appear in the Details block; only the verdict's blame line
// is cleared of them.
const NON_DISCOVERY_ENV_VARS = new Set(['CODEBURN_CACHE_DIR', 'CODEBURN_CURSOR_MAX_BUBBLES', 'KIMI_MODEL_NAME'])
// Ambient platform paths (set by the OS or desktop session for everyone), not
// deliberate user overrides: Windows sets APPDATA and LOCALAPPDATA for every

View file

@ -167,46 +167,93 @@ describe('collectDoctorReport - env override', () => {
}
})
it('does not name APPDATA as an override for a provider that declares it', async () => {
const prev = process.env['APPDATA']
process.env['APPDATA'] = join(tmpDir, 'appdata')
try {
const provider = fakeProvider({ name: 'claude', displayName: 'Claude' })
const report = await collectDoctorReport('all', { providers: [provider], cache: emptyCache() })
const r = only(report, 'claude')
// Windows sets APPDATA and LOCALAPPDATA for every process, so neither
// carries user intent: both are fingerprinted (a change moves the discovery
// root) but must never be named as a deliberate override (Ruling 3 of lane
// 04). Table-driven over both so removing either from AMBIENT_ENV_VARS
// fails a test instead of leaking it into the overrides list.
for (const varName of ['APPDATA', 'LOCALAPPDATA']) {
it(`does not name ${varName} as an override for a provider that declares it`, async () => {
const prev = process.env[varName]
process.env[varName] = join(tmpDir, varName.toLowerCase())
try {
const provider = fakeProvider({ name: 'claude', displayName: 'Claude' })
const report = await collectDoctorReport('all', { providers: [provider], cache: emptyCache() })
const r = only(report, 'claude')
// Windows sets APPDATA for every process, so it carries no user intent:
// it is fingerprinted (a change moves the discovery root) but must never
// be named as a deliberate override (Ruling 3 of lane 04).
expect(r.envOverrides.some(o => o.name === 'APPDATA')).toBe(false)
} finally {
if (prev === undefined) delete process.env['APPDATA']
else process.env['APPDATA'] = prev
}
})
expect(r.envOverrides.some(o => o.name === varName)).toBe(false)
} finally {
if (prev === undefined) delete process.env[varName]
else process.env[varName] = prev
}
})
}
it('redacts credential values (AI_GATEWAY_API_KEY) from overrides, the table render, and the JSON report', async () => {
const secret = 'sk-live-very-secret-value-12345'
const prev = process.env['AI_GATEWAY_API_KEY']
process.env['AI_GATEWAY_API_KEY'] = secret
try {
const provider = fakeProvider({ name: 'vercel-gateway', displayName: 'Vercel AI Gateway', network: true })
const report = await collectDoctorReport('all', { providers: [provider], cache: emptyCache() })
const r = only(report, 'vercel-gateway')
// Every credential in SECRET_ENV_VARS must be redacted at collect time so
// neither the text render nor the JSON report can leak it (Ruling 2 of lane
// 04). Table-driven over both, so a credential added to the set without a
// redaction test fails here instead of leaking into a bug report.
for (const varName of ['AI_GATEWAY_API_KEY', 'VERCEL_OIDC_TOKEN']) {
it(`redacts credential values (${varName}) from overrides, the table render, and the JSON report`, async () => {
const secret = `sk-live-${varName}-value-12345`
const prev = process.env[varName]
const sibling = varName === 'AI_GATEWAY_API_KEY' ? 'VERCEL_OIDC_TOKEN' : 'AI_GATEWAY_API_KEY'
const prevSibling = process.env[sibling]
process.env[varName] = secret
// Isolate the case under test: a stray ambient sibling must not change
// what this case observes.
delete process.env[sibling]
try {
const provider = fakeProvider({ name: 'vercel-gateway', displayName: 'Vercel AI Gateway', network: true })
const report = await collectDoctorReport('all', { providers: [provider], cache: emptyCache() })
const r = only(report, 'vercel-gateway')
// The "is this credential set?" diagnostic is useful; the value is a
// live secret and must never leave doctor (Ruling 2 of lane 04).
expect(r.envOverrides).toContainEqual({ name: 'AI_GATEWAY_API_KEY', value: '<set>' })
expect(r.envOverrides.some(o => o.value.includes(secret))).toBe(false)
const table = renderDoctorTable(report, { color: false })
expect(table).toContain('AI_GATEWAY_API_KEY=<set>')
expect(table).not.toContain(secret)
expect(renderDoctorJson(report)).not.toContain(secret)
} finally {
if (prev === undefined) delete process.env['AI_GATEWAY_API_KEY']
else process.env['AI_GATEWAY_API_KEY'] = prev
}
})
// The "is this credential set?" diagnostic is useful; the value is a
// live secret and must never leave doctor (Ruling 2 of lane 04).
expect(r.envOverrides).toContainEqual({ name: varName, value: '<set>' })
expect(r.envOverrides.some(o => o.value.includes(secret))).toBe(false)
const table = renderDoctorTable(report, { color: false })
expect(table).toContain(`${varName}=<set>`)
expect(table).not.toContain(secret)
expect(renderDoctorJson(report)).not.toContain(secret)
} finally {
if (prev === undefined) delete process.env[varName]
else process.env[varName] = prev
if (prevSibling === undefined) delete process.env[sibling]
else process.env[sibling] = prevSibling
}
})
}
// CODEBURN_CURSOR_MAX_BUBBLES caps how many bubbles Cursor parses
// (src/providers/cursor.ts:692) and KIMI_MODEL_NAME renames the model
// attributed to Kimi sessions (src/providers/kimi.ts:155): both are
// fingerprinted but cannot explain why nothing was discovered, so the
// verdict must not name them — while Details still lists them, because they
// ARE overrides in force. Each is asserted through the provider that
// declares it.
for (const [varName, providerName, displayName, value] of [
['CODEBURN_CURSOR_MAX_BUBBLES', 'cursor', 'Cursor', '5000'],
['KIMI_MODEL_NAME', 'kimi', 'Kimi', 'kimi-latest-920'],
] as const) {
it(`does not blame ${varName} for an empty ${displayName} (not a discovery path)`, async () => {
const prev = process.env[varName]
process.env[varName] = value
try {
const provider = fakeProvider({ name: providerName, displayName })
const report = await collectDoctorReport('all', { providers: [provider], cache: emptyCache() })
const r = only(report, providerName)
expect(r.envOverrides).toContainEqual({ name: varName, value })
expect(r.verdict).not.toContain(varName)
const table = renderDoctorTable(report, { color: false })
expect(table).toContain(`${varName}=${value}`)
} finally {
if (prev === undefined) delete process.env[varName]
else process.env[varName] = prev
}
})
}
})
// ── Synthetic edge cases ───────────────────────────────────────────────────

View file

@ -6,6 +6,7 @@ import { basename, join } from 'path'
import {
CACHE_VERSION,
PROVIDER_ENV_VARS,
type CachedCall,
type CachedFile,
type CachedTurn,
@ -362,18 +363,44 @@ describe('provider env overrides invalidate the fingerprint (#920)', () => {
// declaring any CODEBURN_COPILOT_* var would force a re-parse that destroys
// conversations Copilot has since pruned from the DB, which only the cache
// still holds. The fingerprint must therefore NOT move when one is set.
// This reads as intent, not as an oversight.
it('does not move the copilot fingerprint when CODEBURN_COPILOT_OTEL_DB is set (deliberately undeclared)', () => {
const prev = process.env['CODEBURN_COPILOT_OTEL_DB']
try {
const before = computeEnvFingerprint('copilot')
process.env['CODEBURN_COPILOT_OTEL_DB'] = '/tmp/codeburn-copilot-otel'
expect(computeEnvFingerprint('copilot')).toBe(before)
delete process.env['CODEBURN_COPILOT_OTEL_DB']
expect(computeEnvFingerprint('copilot')).toBe(before)
} finally {
if (prev === undefined) delete process.env['CODEBURN_COPILOT_OTEL_DB']
else process.env['CODEBURN_COPILOT_OTEL_DB'] = prev
// This reads as intent, not as an oversight — and the assertions below pin
// the WHOLE invariant (no entry at all, plus every one of the nine deferred
// reads), so a future "completing" edit fails a test instead of silently
// re-opening the durable history-loss path.
describe('copilot is deliberately undeclared in PROVIDER_ENV_VARS', () => {
it('has no PROVIDER_ENV_VARS entry at all', () => {
expect(PROVIDER_ENV_VARS['copilot']).toBeUndefined()
})
// The nine reads copilot.ts performs whose declaration is deferred (each
// is allowlisted in tests/provider-env-declarations.test.ts): setting any
// of them must leave the copilot fingerprint untouched.
const DEFERRED_COPILOT_VARS = [
'CODEBURN_COPILOT_SESSION_STATE_DIR',
'CODEBURN_COPILOT_OTEL_DB',
'CODEBURN_COPILOT_JETBRAINS_DIR',
'CODEBURN_COPILOT_WS_STORAGE_DIR',
'CODEBURN_COPILOT_GLOBAL_STORAGE_DIR',
'CODEBURN_COPILOT_DISABLE_OTEL',
'APPDATA',
'LOCALAPPDATA',
'XDG_CONFIG_HOME',
]
for (const varName of DEFERRED_COPILOT_VARS) {
it(`does not move the copilot fingerprint when ${varName} is set (deliberately undeclared)`, () => {
const prev = process.env[varName]
try {
const before = computeEnvFingerprint('copilot')
process.env[varName] = `/tmp/codeburn-copilot-920/${varName}`
expect(computeEnvFingerprint('copilot')).toBe(before)
delete process.env[varName]
expect(computeEnvFingerprint('copilot')).toBe(before)
} finally {
if (prev === undefined) delete process.env[varName]
else process.env[varName] = prev
}
})
}
})
})