The bug
src/cache/store.ts persists timestamps via toIsoSeconds() as zone-less UTC — "YYYY-MM-DD HH:MM:SS", deliberately matching SQLite's datetime('now') format. But the three read sites parse them back with new Date(str):
isExpired() — store.ts:285
isCacheUsable() — store.ts:297
getCachedSearch() — store.ts:436
JavaScript's Date parser treats the space-separated, zone-less form as local time, so every expiry comparison silently shifts by the host's UTC offset.
Impact
- West of UTC (e.g. UTC-4): every cached row appears to expire 4h later than it should — expired/stale content is served as fresh, and
mode: fast's stale: true marker never fires inside the shifted window. Search cache TTL stretches the same way.
- East of UTC: rows expire early — cache hit rate silently drops.
Not theoretical: on a UTC-4 machine, npm test on current main (185afb5) fails 14 unit tests (tests/unit/tools/fetch-mode.test.ts stale-window tests and friends); all pass under TZ=UTC. CI never sees it because runners are UTC.
Proposed fix
Small and local: a parseUtcTimestamp() helper next to toIsoSeconds() that re-attaches the UTC marker (value.replace(' ', 'T') + 'Z') when the string matches the zone-less format, falls through unchanged otherwise (so any Z-suffixed or ISO rows keep working). Swap the three new Date(...) call sites to it. No schema or stored-format change — existing rows parse correctly immediately.
Plus a regression test that sets process.env.TZ to a non-UTC zone (skipped on win32, where Node ignores runtime TZ changes) so UTC CI runners can still catch a reintroduction.
Happy to PR this right away — branch and tests ready to push. (Flagging per CONTRIBUTING.md: opening the PR means agreeing to the CLA.)
The bug
src/cache/store.tspersists timestamps viatoIsoSeconds()as zone-less UTC —"YYYY-MM-DD HH:MM:SS", deliberately matching SQLite'sdatetime('now')format. But the three read sites parse them back withnew Date(str):isExpired()— store.ts:285isCacheUsable()— store.ts:297getCachedSearch()— store.ts:436JavaScript's
Dateparser treats the space-separated, zone-less form as local time, so every expiry comparison silently shifts by the host's UTC offset.Impact
mode: fast'sstale: truemarker never fires inside the shifted window. Search cache TTL stretches the same way.Not theoretical: on a UTC-4 machine,
npm teston currentmain(185afb5) fails 14 unit tests (tests/unit/tools/fetch-mode.test.tsstale-window tests and friends); all pass underTZ=UTC. CI never sees it because runners are UTC.Proposed fix
Small and local: a
parseUtcTimestamp()helper next totoIsoSeconds()that re-attaches the UTC marker (value.replace(' ', 'T') + 'Z') when the string matches the zone-less format, falls through unchanged otherwise (so any Z-suffixed or ISO rows keep working). Swap the threenew Date(...)call sites to it. No schema or stored-format change — existing rows parse correctly immediately.Plus a regression test that sets
process.env.TZto a non-UTC zone (skipped on win32, where Node ignores runtime TZ changes) so UTC CI runners can still catch a reintroduction.Happy to PR this right away — branch and tests ready to push. (Flagging per CONTRIBUTING.md: opening the PR means agreeing to the CLA.)