Skip to content

test(parsers): a new parser could land with no test and no CI objection - #116

Merged
pitimon merged 2 commits into
mainfrom
test/104-parser-conformance-ratchet
Jul 25, 2026
Merged

test(parsers): a new parser could land with no test and no CI objection#116
pitimon merged 2 commits into
mainfrom
test/104-parser-conformance-ratchet

Conversation

@pitimon

@pitimon pitimon commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the harness half of #104.

Coverage here is inverted relative to risk. CLAUDE.md:169-172 records the historical failure modes — dedup failures, cached-input semantics, snapshot-vs-cumulative confusion — as 1.6× to 7× magnitude errors in the numbers the whole product exists to report. Four parsers (hermes, codebuddy, craft, kimi) had no test at all, and README:116 invites contributors to add more.

The ratchet

test/parser-conformance.test.js enumerates the parsers from source, not from a hand-written list — 24 found. Each must have either a conformance fixture or an allowlist.json entry saying why not. A parser added without either fails; it cannot be silently skipped.

The allowlist fails if it grows past max_size, and fails if it is shorter — telling you to lower max_size in the same commit so the ratchet records the ground gained. An allowlist that can grow is a TODO list.

Per fixture: the column invariant total = input + output + cache_creation + cache_read + reasoning, non-negative counts, a model present, a 30-minute UTC bucket, and that re-parsing unchanged input adds nothing — the "run sync twice" lesson, which is what catches dedup-key instability.

Fixtures for three of the four named parsers: craft, kimi, codebuddy. hermes is on the allowlist because it reads a SQLite database across profile directories, which needs a real schema rather than a JSONL line.

Two things found while building it

Both are recorded in the code rather than smoothed over.

1. The column invariant does not catch cache-read double-counting. A parser that folds cache reads into input_tokens and inflates total_tokens to match is internally consistent — the sum balances while the user's number is ~4× too big. That is precisely the class this issue is about, and this harness does not catch it. There is now a test pinning that blind spot so nobody reads the harness as covering more than it does. Only a real sample log with known-correct expected values catches it, which is what the per-tool tests are for.

2. The kimi fixture initially produced ZERO rows and would have passed as a green test proving nothing. The harness asserts a fixture yields rows before checking them, and that guard caught it. Cause: kimi's timestamp is epoch seconds and the fixture omitted it entirely.

Why the codebuddy fixture is the interesting one

CodeBuddy's prompt_tokens is OpenAI-style and includes cached tokens, so input_tokens = prompt_tokens - cache_read. The fixture feeds 3000 prompt tokens of which 2400 are cache reads, and the parser emits input_tokens: 640 — the subtraction, held still.

Test plan

  • ci:local exit 0 — 918 root tests (+9), 256 dashboard
  • The conformance checker is tested against four known-bad rows plus a valid control, so it is not a checker nothing has ever failed
  • All three fixtures produce rows and pass the invariants; verified the actual emitted rows by hand, not just the exit code
  • CONTRIBUTING.md step 4 documents the fixture requirement and, explicitly, what it does not prove

Still open on #104

A hermes fixture, and fixtures for the 21 allowlisted parsers. The ratchet is what makes those get written as anyone touches them.

itarun.p added 2 commits July 26, 2026 05:08
Closes the harness half of #104.

Coverage here is inverted relative to risk. CLAUDE.md:169-172 records the
historical failure modes — dedup failures, cached-input semantics,
snapshot-vs-cumulative confusion — as 1.6x to 7x magnitude errors in the numbers
the whole product exists to report. Four parsers (hermes, codebuddy, craft,
kimi) had no test at all, and README:116 invites contributors to add more.

test/parser-conformance.test.js ENUMERATES THE PARSERS FROM SOURCE, not from a
hand-written list, so a parser added without a fixture fails rather than being
silently skipped. 24 found. Each must have either a conformance fixture or an
allowlist entry saying why not, and the allowlist is a ratchet: the test fails
if it grows past max_size AND fails if it is shorter, telling you to lower
max_size in the same commit. An allowlist that can grow is a TODO list.

Fixtures for three of the four named parsers: craft, kimi, codebuddy. Hermes is
on the allowlist because it reads a SQLite database across profile directories,
which needs a real schema rather than a JSONL line.

Per fixture: the column invariant, non-negative counts, a model, a 30-minute UTC
bucket, and that re-parsing unchanged input adds nothing (the "run sync twice"
lesson, which is what catches dedup-key instability).

TWO THINGS FOUND WHILE BUILDING IT, both recorded rather than smoothed over:

1. The column invariant does NOT catch cache-read double-counting. A parser that
   folds cache reads into input_tokens and inflates total_tokens to match is
   internally consistent, so the sum balances while the user's number is ~4x too
   big — precisely the class this issue is about. There is now a test pinning
   that blind spot so nobody reads the harness as covering more than it does.
   Only a real sample log with known-correct values catches it.

2. The kimi fixture initially produced ZERO rows and would have passed as a
   green test proving nothing. The harness asserts a fixture yields rows before
   checking them; that guard caught it. Cause: kimi's timestamp is epoch
   SECONDS and the fixture omitted it entirely.

The conformance checker is itself tested against four known-bad rows plus a
valid control, so it is not a checker nothing has ever failed.

ci:local exit 0: 918 root tests, 256 dashboard.
@pitimon
pitimon merged commit 6d9f329 into main Jul 25, 2026
1 check passed
@pitimon
pitimon deleted the test/104-parser-conformance-ratchet branch July 25, 2026 22:15
pitimon added a commit that referenced this pull request Jul 26, 2026
15 commits since v0.39.43. Tracker: #125.

User-facing:
- The Projects panel honours the date range. It read no query parameter at
  all, so picking "24h" narrowed every other card while Projects kept showing
  all-time totals with nothing on screen saying so (#118).
- Per-repo cost, with rows written before the change named as unpriced rather
  than shown as a confident $0 (#121).
- Sources with no per-repo attribution are named, instead of their absence
  reading as "that tool cost nothing here" (#118).
- A plan-value card answering what README:32 already promised, labelled as
  list-price-equivalent rather than a saving (#122).
- The Codex quota chip no longer vanishes on a 401 (#119).
- `sync --compact`, which reclaimed 34,924 lines to 5,636 on a real install
  (#117).

Behind the scenes: outbound-privacy validator hardening (#111, #114), avatar
proxy per-hop revalidation, port-aware allowlisting and a real download cap
(#109, #112), a parser conformance ratchet (#116, #120), scripts/graph out of
the product gate (#115), and a usage-limits fixture capture tool (#124).

Version bumped in all four lockstep locations by the `version` hook:
package.json, package-lock.json, both TokenTrackerBar targets, and the Windows
csproj. validate:version-lockstep passes.

WATCH AFTER SHIPPING: #121 migrates cursors.json on every user's first sync
after upgrading, re-keying project buckets from project|source|hour to
project|source|model|hour. Without that the old bucket strands and its usage is
counted twice. Verified read-only against a real 2,015-bucket state — every key
migrated, total_tokens preserved exactly at 6,281,653,062, and all 2,015
queuedKey markers survived, the loss of which would re-append every row. That
is one machine; a differently-shaped state is the residual risk.

ci:local exit 0: 972 root tests, 302 dashboard.

Co-authored-by: itarun.p <itarun.p@somapait.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant