Repository navigation
Description
aggregator(token-adapter): the file-export adapter's fail-closed window check throws on a null window element — and accepts a non-string window name — instead of returning the retained unparseable outcome
Summary
pm-stack/aggregator/src/token-adapter.ts is the browser-token adapter framework core (#1153). Its contract is fail-closed: any dashboard/export payload whose shape drifts produces an AdapterOutcome with a failure status and the RETAINED previous value — never a fabricated zero, and (for collectAdapter) never a throw from a parse step, because parse errors are caught and classified unparseable (token-adapter.ts:164-221).
collectFileExportAdapter (token-adapter.ts:229) implements the same discipline for file-export documents, with an explicit fail-closed window validation at :270-283:
for (const w of doc.windows) {
if (!w.name || typeof w.usedPct !== "number" || !Number.isFinite(w.usedPct)) {
return retainedOutcome("unparseable", ...);
}
}Two holes in that check:
- A
null(orundefined) window element throws.w.namedereferences the element before any object check, so a success document withwindows: [null]throwsTypeError: null is not an object (evaluating 'w.name')straight out ofcollectFileExportAdapter. UnlikecollectAdapter, this function has no try/catch around its validation/mapping, so the throw escapes the framework entirely: no retained previous value, nounparseableclassification, and any future caller that trusts the AdapterOutcome contract crashes (or must add its own catch, defeating the framework's purpose). - A non-string window name passes.
!w.nameis a truthiness check only, so{ name: 42, usedPct: 1 }validates and is handed toentryFromFileExport(:285), which stamps a numeric-named window into the published quota-current entry as a successfulokreading — exactly the "partial/fabricated value published instead of retained" outcome the module header says must never happen.
The existing test suite pins the neighboring case — a window missing usedPct is unparseable with the previous entry retained (test/token-adapter.test.ts, "a drifted success document (window without usedPct) is unparseable, previous retained") — but no test covers a null element or a non-string name.
Evidence (verified this run, main 3891731)
Probe: laptop Bun 1.4.2, the real src/token-adapter.ts (verbatim copy of the aggregator src/ tree, md5 334689a9912d2bb86a08ea26b0df8ac4, matching main 3891731) at ~/git/fleet/probe-2026-10-29-laneB-tokenadapter/probe.ts, calling collectFileExportAdapter with a healthy meta file-export doc shape (mirroring the test fixture) and varying only windows:
- Control
[{ name: "5h", usedPct: 12.5 }]→ outcome statusok. - Drift
[{ name: "5h" }](no usedPct) → outcome statusunparseable, detail "file-export drift: window 5h lacks a finite usedPct" (the test-pinned behavior). [null]→ THREWTypeError: null is not an object (evaluating 'w.name')— no outcome at all.[undefined]→ THREWTypeError: undefined is not an object (evaluating 'w.name').["5h"](string element) → outcomeunparseable(benign control: property reads on a string do not throw).[{ name: 42, usedPct: 1 }]→ outcome statusok— a numeric window name is published as a successful reading.
File:line (main 3891731):
pm-stack/aggregator/src/token-adapter.ts:270-283— the window validation loop: no element object check, name checked by truthiness only.pm-stack/aggregator/src/token-adapter.ts:285—entryFromFileExport(doc, ...)runs on whatever passed the loop.pm-stack/aggregator/src/token-adapter.ts:11-19(module header) and:255-269— the stated contract: shape drift isunparseable, the previous value is retained, never a partial/fabricated publish.
Why it matters
This framework is the template every per-provider quota adapter builds on (#1159/#1161/#1163/#1165/#1301 family), and its whole value is that callers can trust the outcome contract: failures arrive as classified outcomes with the last good value retained. A producer whose export gains one null window (a serializer edge, a hand-edited latest.json, a half-written document read mid-replace) currently breaks that contract at the framework's own validation step — the one place written specifically to convert drift into unparseable. The numeric-name hole is the same check under-validating in the other direction: malformed data stamped ok and published as measured quota data.
Acceptance criteria
- Triage pins the validation shape (expected: a window element that is not an object, or whose
nameis not a non-empty string, is drift →retainedOutcome("unparseable", ...)with the previous value retained, matching the existing missing-usedPct behavior — or triage pins a different shape; throwing out ofcollectFileExportAdapterfor a parseable-but-malformed document is the one ruled-out option). - Regression tests in test/token-adapter.test.ts:
windows: [null],windows: [undefined], andwindows: [{ name: 42, usedPct: 1 }]each return statusunparseablewith the previous entry retained and do not throw. - No behavior change for well-formed documents or the already-pinned drift cases: the existing token-adapter.test.ts expectations pass unchanged.
Filed by new-issue-creator-code-b (lane B), run 2026-10-29 22:50 CDT. Dedup: gh searches for collectFileExportAdapter / "token-adapter windows" / file-export drift returned no covering issue; #1153 is the parent framework feature (in-review), not this bug; #2332/#2416 are quota-gather-map/quota-current window handling in different files; the meta-exporter window issues (#1566 closed, #2460) are the producer-side .mjs parser, not this aggregator-side adapter.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
2 remaining items
dezren39 commentedon Nov 1, 2026
agent-note:v1
state: done
task: 3788 (repo-health-code-pilot, lane D)
stage: triage complete
did: findings + pin posted (comment 6556076269); follow-up small_fix enqueued as queue task 5363 (packet: hidden_files/packets/task3788-2551-smallfix.json). Issue stays open for the fix task.
resume: task 5363 implements the element/name validation per the pin.
dezren39 commentedon Nov 1, 2026
agent-note:v1 — queue task 5363 (lane C) — picked up for implementation
Implementing the pin from triage task 3788: in
collectFileExportAdapter's window loop, reject non-object window elements as unparseable-retained instead of throwing onw.name, and require a non-empty string name instead of truthiness. Regression tests for[null],[undefined],[{name: 42, usedPct: 1}]in the token-adapter tests.Merge pull request #3454 from developing-today/queue/5363-2551-window…
dezren39 commentedon Nov 1, 2026
agent-note:v1 — queue task 5363 (lane C) — done: fixed and merged
PR #3454 merged as
d61aec488485f828190ad09e13f0832a6c905e19(head6855d0805).collectFileExportAdapterwindow loop now rejects a non-object element viaisRecordas unparseable-retained (no moreTypeErrorescaping the AdapterOutcome contract) and requires a non-empty string name instead of truthiness; finite-usedPct check unchanged.bun test test/token-adapter.test.ts— 15 pass, 0 fail, including new[null],[undefined], and[{name: 42, usedPct: 1}]cases, each unparseable with the previous entry retained. Codex review on6855d0805: completed, no findings.src/feed.tsdoes not parse on current main (missing closing brace from 4d7f643) — filed separately as aggregator(feed): feed.ts does not parse — isUnmatchedCharge is missing its closing brace (merged in 4d7f643e3, #2445) #3452; not part of this fix.