Gitleaks ORs an allowlist entry's conditions by default, so pairing a
fixture regex with a tests/ path allowlisted every finding under tests/,
regex or not - a real secret committed to a test file would have been
suppressed. condition = AND restores the intended semantics (verified: a
planted AWS/Stripe-shaped canary in tests/ is flagged again), and the
canonical example JWT header fixture that the path condition was silently
covering gets its own scoped entry. Full-history scan stays green.
vitest's default glob reached the Electron app's specs under app/, which carry their
own vitest config and their own jsdom in app/node_modules. From a root install that
fails with ERR_MODULE_NOT_FOUND: jsdom, so the command CONTRIBUTING documents and the
one RELEASING.md names as the pre-release gate both error out.
Move the scoping CI already applies into package.json: test runs tests/ minus the
parallelism-sensitive cache-refresh-lock suites, test:locks runs those three serially,
test:watch keeps watch mode at the same scope. The first two are byte-identical to the
invocations .github/workflows/tests.yml spells out, so the workflow can be pointed at
the scripts to stop the two drifting apart again; that edit is left out of this PR so it
needs no workflow permissions. test plus test:locks together still cover all 192 files
under tests/.
Scoping the script changes what a trailing path argument means: vitest ORs positional
filters, so 'npm test -- tests/providers/hermes.test.ts' would no longer narrow to that
file, it would run the whole suite. Rewrite those to 'npx vitest run <path>' everywhere
they appear - four provider guides and the MCP design plan, thirteen lines in all.
Also refresh the stale test docs: 42 files/568 tests (now 192 under tests/), the
per-directory counts, the line claiming vitest does not run in CI which stopped being
true when tests.yml landed, and the provider test-gap list, which still named
antigravity and gemini after both gained test files.
Record the cache-refresh-lock naming convention in CONTRIBUTING, since the split makes
it load-bearing: a lock test that misses the prefix runs under the full worker pool and
flakes, and one that matches it but is absent from test:locks never runs at all.
Improve dashboard refresh coordination, scrolling, responsive layout, and data-density behavior, including the Windows-safe resize correction validated on the final head.
Add doctor probeRoots coverage for the remaining fixed-location providers while keeping discovery and diagnostics on the same shared root-resolution logic.
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.
Five findings from a cross-model review of the previous two commits, each
verified on the code before acting:
Copilot is no longer declared. Declaring anything for it changes its
fingerprint, and getOrCreateProviderSection keeps only cached entries whose
source path is gone - but OTel discovery returns one source per DB file
(copilot.ts:1935) and that DB keeps existing, so the entry would be dropped and
re-parsed, destroying conversations Copilot has since pruned from the DB that
only the cache still holds. Trading a staleness bug for a data-loss bug is a
bad trade; copilot waits for the durable carry-forward to merge instead of
drop, and its reads are allowlisted with that reason.
The Vercel gateway credentials ARE declared, reversing the previous commit's
reasoning, which was wrong: servedSources is seeded with every discovered
source (parser.ts:2875) before the network branch, and the network re-fetch
(parser.ts:2888) only runs when !readOnly, so a read-only refresh serves the
cached report and an undeclared credential keeps reporting the previous
account's usage after a swap. Doctor redacts credential values so a key can
never reach terminal output or the JSON report.
AMBIENT_ENV_VARS narrows to APPDATA and LOCALAPPDATA. Windows sets those for
every process so they carry no intent, but the XDG vars are opt-in and do:
suppressing them made doctor answer a deliberately relocated XDG_DATA_HOME with
"tool likely not installed", which is worse than the noise it avoided.
The guard's allowlist is keyed by file and var, not var alone - a var
allowlisted for one file silenced every other file's undeclared read of it.
Cursor drops its stale XDG_DATA_HOME declaration, which it never reads; its
fingerprint already changes here, so this costs no extra migration.
cursor-agent keeps its equally stale one, since removing it would force a
re-parse to fix nothing.
The nine undeclared overrides in #920 all slipped through the same way: the
declaration lives in one file and the read in another, and nothing tied them
together. Add the static guard the issue asked for - every process.env read in
src/providers is either declared in PROVIDER_ENV_VARS for the provider(s) that
file serves, or allowlisted with a reason. It resolves bracket literals, dot
access and `process.env[CONST]` indirection (open-design's ENV_DIR), and fails
loudly on any read it cannot resolve to a name rather than skipping it, since a
silently skipped read is how this class of defect survives. A read-bearing
provider file missing from the file-to-provider map fails too, so a new
provider cannot join without being mapped. A second assertion catches a
PROVIDER_ENV_VARS key that is not a registered provider name, which declares
nothing and fails just as silently.
Plus the direct regression: each of the nine reported (provider, var) pairs
must move the fingerprint, with codex/CODEX_HOME as the control the issue used,
and the round trip asserted so the hash stays a pure function of the
environment.
Nine providers honor an env var that relocates where discovery looks, but the
var was never declared in PROVIDER_ENV_VARS, so computeEnvFingerprint() did not
hash it and the provider's cache section survived the change: sessions parsed
from the old root kept being reported and the new root was never read, with no
diagnostic anywhere (#920, same silent-wrong-numbers family as #874).
Declare every env var that changes what a provider discovers or how its
sessions parse, including the platform path vars that resolve a discovery root
on Windows and Linux, and the CodeBurn-side directory overrides.
Ambient platform vars (APPDATA, LOCALAPPDATA, XDG_CONFIG_HOME, XDG_DATA_HOME)
are set by the OS or the desktop session for everyone, so doctor must not name
them as a deliberate override: without the guard every Windows user would be
told Claude and Copilot discovery runs under an override. They stay in the
fingerprint - a change to them does move the discovery root - but doctor skips
them when collecting overrides, and the probed paths it already prints show
where CodeBurn looked.
Closes#918.
The renderer's keydown handler required event.metaKey and explicitly
rejected event.ctrlKey, so every shortcut was dead outside macOS: on
Windows and Linux metaKey is the Super key, which the OS shell takes.
Navigation (1-8), Settings (,) and Refresh (R) all did nothing. The
sidebar and footer hints also hardcoded the Cmd glyph, so a Windows
user was shown chords that could not fire.
Add app/renderer/lib/platform.ts as the single source of truth for
platform-aware shortcuts, reading the platform the preload already
exposes (window.codeburn.platform) with a user-agent fallback for the
non-Electron cases. isModifierChord accepts Cmd-without-Ctrl on darwin
and Ctrl-without-Cmd elsewhere; altKey stays rejected on both, because
AltGr on European Windows layouts arrives as Ctrl+Alt and must not
hijack a typed character. Every visible shortcut label now resolves
through shortcutLabel() at render time, so the sidebar shows Ctrl+1
where macOS shows the Cmd glyph.
The mac chord condition is unchanged: the old guard admitted
metaKey && !altKey && !ctrlKey && !shiftKey, and the new one admits
exactly the same set on darwin.
The Electron application menu is deliberately left alone. It ships no
reload/forceReload role and no CmdOrCtrl+R accelerator, which is what
leaves Ctrl+R free for the renderer to handle on Windows.
Also corrects the Settings navigation hint, which read 1-7 while the
sidebar has eight numbered destinations.
Tests cover both platforms for labels and dispatch, including the
negatives: Meta on win32, Ctrl on darwin, and the Ctrl+Alt AltGr shape.
Reconcile with #914 (which independently fixed the same date-sensitive
fixtures): keep this branch's equivalent date fixes for parser.test.ts and
cli-durable-totals.test.ts, and drop the global vitest retry #914 added now
that this branch fixes the flakes at the root (fs.rm retries, longer timeouts,
serial cache-lock CI step). The targeted cache-lock local retries stay.
Hermes, Cursor, OpenCode and copilot OTel sources all live in SQLite
databases that their agents keep open in WAL mode for the life of the
process. Committed writes park in <db>-wal until a checkpoint, so the
main file's stat can sit hours or days behind the newest committed data.
fingerprintFile only statted the main file, which broke two ways:
- The date-range mtime pre-filter in parseProviderSources read the stale
mtime as "nothing in range" and skipped the source entirely. Every
Hermes session committed after the last checkpoint vanished from
reports: the today-parse skipped the db (mtime < local midnight) while
the backfill only keeps days through yesterday. Exactly the "17
sessions in the DB, 14 reported, the 3 from today missing" report in
issue #913.
- reconcileFile saw an unchanged fingerprint between checkpoints and kept
serving stale cached turns for sessions that had since grown.
Fold the -wal sibling into the fingerprint: newest mtime wins and sizes
add, so both WAL growth and a checkpoint (db grows, wal truncates) move
the fingerprint. -shm is deliberately ignored (it mutates on reads).
Bare SQLite paths get the fold only when the extension says database, so
JSONL transcript fingerprints (offset-based append detection) are
untouched.
Refs #913
parser.test.ts (a)/(f): createJsonlSession stamped events at a fixed 2026-05-01
that aged past the 90-day retention window, pruning to zero; date them relative
to now. cli-durable-totals: seedLiveTodaySession stamped noon, which is in the
future on a pre-noon run so the provider-scoped today slice (ends at now)
dropped it while the all path (ends at range end) kept it; seed a past-today
time. cache-refresh-lock and other integration tests starve under a saturated
parallel run and fail closed; add a small global retry and raise the two most
load-sensitive lock tests. Test-only; no production code changed.
When the project directory IS the home directory, countSkills pushed both
~/.claude/skills and <project>/.claude/skills - the same path - and counted
every skill twice, and scanMemoryFiles read ~/.claude/CLAUDE.md twice,
inflating the context-budget estimate. Dedupe both by resolved path.
Mutation-checked: a single home skill counts 2 before the fix, 1 after.
- vscode-cline-parser: entry.ts was truthy-checked but not validity-checked,
so a garbage timestamp made new Date(ts).toISOString() throw RangeError and
abort the whole session parse. Validate the date, fall back to empty.
- models: parseLiteLLMEntry read fields off its argument with no null/type
guard, so a null value in the remote LiteLLM pricing JSON threw and aborted
the entire live pricing load. Return null for a null/non-object entry.
Both mutation-checked: the tests raise RangeError / TypeError before the fix.
cacheKey fingerprinted only project count + api-call sum, so two datasets
agreeing on those two numbers collided onto one cached OptimizeResult, and
a cost/token change that left call count unchanged (e.g. a re-price) served
stale findings within the 60s TTL - reachable in the long-lived menubar.
Fold total cost, savings and proxied cost (scaled to micro-dollars) into
the key. Exported cacheKey and mutation-checked: the old key collides two
same-shape datasets and a re-price; the new one separates both, while an
identical dataset still keys identically.
parseChatFile estimated input tokens from pendingUserMessage - the last
human turn sliced to 500 chars - while output summed every bot char, so a
multi-turn session or any prompt over 500 chars undercounted input tokens
and therefore costUSD severalfold. Accumulate every human turn's full
length (inputChars), matching the modern-execution path; keep the 500
slice for the display userMessage only. Mutation-checked: a 2400-char
prompt reports 125 tokens before, 600 after.
handle() is dispatched via `void`, so a throw before its try/catch is an
unhandled rejection on a LAN-facing server. A request target the HTTP
parser accepts but the WHATWG URL parser rejects (unterminated IPv6 host
like //[::1) threw at new URL() and hung/killed the process. Parse inside
a guard and answer 400. Mutation-checked: the test times out with an
unhandled error before the fix, passes after.
A full-history secret scan (all refs, 1352 commits) plus trufflehog
--only-verified came back clean: zero live secrets. gitleaks' default
generic-api-key rule flags 8 non-secrets - obviously-fake test fixtures
(sk-live-0123..., sk-live-AKIA...SECRETKEY), the public Claude Code and
Codex OAuth client IDs (PKCE public-client flow, no client_secret), and a
dedup-key string. This encodes exactly those as allowlisted so scans stay
green and a real leak can never hide under recurring false positives.
On a tz-change full re-derive, mergeDayEntries carried a baseline slice
whenever the fresh day had no data slice for that (date, provider), so a
turn that re-bucketed across local midnight left its old day sliceless,
got carried there, AND counted again on its new day. This subtracts from
each carried baseline slice exactly what the fresh parse still attributes
to that (date, provider) under the OLD bucketing (dateKeyInTz): the
re-bucketed turns, nothing else. A sources-gone slice has no such content
and survives untouched; a fully-explained slice is dropped; residual
slices ADD their sessions instead of max-dedup, since the subtraction
already removed the placeholder's share.
The shared base computation guarded hours >= 2 but its h < 2 branch
still subtracted five minutes past midnight, escaping into yesterday
during the first five minutes of UTC hours 0 and 1 and zeroing every
'today' assertion - which is exactly when runs #6 landed. Midnight
clamp replaces the guard at all four sites.
OpenClaude is a Claude Code fork routing to any LLM; transcripts are
Claude-Code-schema JSONL under ~/.openclaude/projects/<slug>/<uuid>.jsonl
with replay.json siblings skipped. Only usage-bearing assistant lines
become calls; sidechain lines are counted as real spend; costs are always
computed (the transcript reports none) through the shared tables.
Real local testing: sessions generated with the actual CLI against
DeepSeek (deepseek-chat), parsed end to end.
server.close() only stops new connections; an in-flight fire-and-forget
cache save can land a file mid-recursive-rm, surfacing as ENOTEMPTY on
slower runners (run #5). fs.rm's built-in retries absorb the window.
Run #4 showed cache-refresh-lock-process racing its own takeover window
even in the serial single-fork step (#904). The enforced signal stays
the main suite; the lock suite reports without gating until the race
semantics are settled.
Every case spawns the real CLI and does genuine multi-provider parse
work; run #3 showed a second sibling crossing the 5s default on the
shared runner. File-level cap replaces the earlier single-test one.
The root vitest glob also matched app/renderer/*.test.tsx, whose jsdom
environment lives in app/node_modules and cannot resolve from the root
install; ERR_MODULE_NOT_FOUND took down the whole parallel step.
- cli-durable-totals: the live fixture session was stamped at noon today,
so every before-noon run saw it in the future; the provider-filtered
path drops future instants while the all-provider path keeps the whole
day, failing the parity assertion. Relative-and-clamped timestamps,
the same fix project-filter-durable-totals got in 1596220.
- parser (copilot, 2 cases): the fixture's fixed 2026-05-01 dates crossed
copilot's durable 90-day age-out on 2026-07-30, so the first parse
pruned the freshly-cached session. Relative timestamps.
- parser-incremental-append: unlink-then-create let ext4 hand the freed
inode straight back, breaking the new-inode premise. The replacement
is now created beside the original and renamed over it.
- parser-proxy-pricing: normalizeProxyPath folds case only on darwin and
win32, deliberately; the test now asserts the platform-correct
behavior on both kinds of filesystem instead of hardcoding macOS.
- cli-status-menubar: the config-source filter case does real multi-parse
work and needs more than the 5s default on shared runners; 30s cap.