test(project-filter): make the durable-headline filter tests time-of-day proof

The tests added in #864 seeded today's live session at a fixed wall-clock
hour (12:00 local). The periods they build end at `new Date()`, and the
suite runs under TZ=UTC, so for any run before 12:00 UTC that timestamp
is in the FUTURE and the range filter correctly drops it. The live half
of the cache/live union then contributes nothing, and the one assertion
that needs a non-zero headline — the unattributed-cost footnote — fell
into renderOverview's "No usage found" early return and went red. Half
of every day was a failing window; a769b50 fixed the start-of-month
flake but this one survived it.

Verified by bisecting the fixture on the unpatched test: moving the
seeded hour from 12:00 to 01:00 (past, at a 03:38 UTC run) turns the
same 12 tests green, so the timestamp's position relative to `now` is
the whole cause.

- Seed the session a few minutes BEFORE now, clamped to today's
  midnight, so it is always both inside today and already in the past.
- Stop the footnote test depending on the live parse at all: seed a
  second, attributable cached day so the headline is non-zero from the
  cache alone. The test now exercises the footnote instead of the
  fixture's timing.
This commit is contained in:
chengchuan.zhou 2026-08-03 11:42:32 +08:00
parent 2de4d100bf
commit 15962202bd

View file

@ -83,13 +83,13 @@ function carriedDayWithoutProjects(date: string): DailyEntry {
return day
}
async function seedCache(day: DailyEntry): Promise<void> {
async function seedCache(...days: DailyEntry[]): Promise<void> {
const cache: DailyCache = {
version: DAILY_CACHE_VERSION,
savingsConfigHash: getDailyCacheConfigHash(),
tzKey: currentTzKey(),
lastComputedDate: daysAgoStr(1),
days: [day],
days,
complete: true,
}
await writeFile(join(ROOT, 'cache', `daily-cache.v${DAILY_CACHE_VERSION}.json`), JSON.stringify(cache), 'utf-8')
@ -100,7 +100,13 @@ async function seedLiveTodaySession(): Promise<void> {
const projectDir = join(ROOT, 'home', '.claude', 'projects', 'live-proj')
await mkdir(projectDir, { recursive: true })
const now = new Date()
const at = (h: number, m: number): string => new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0).toISOString()
// Timestamps a few minutes OLD, clamped into today: a wall-clock hour (12:00)
// is in the future whenever the suite runs before noon, and the periods here
// end at `new Date()`, so a fixed hour made the live half of the union vanish
// for a morning run. Relative-and-clamped keeps the session inside today and
// in the past at every hour.
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
const minutesAgo = (m: number): string => new Date(Math.max(midnight, now.getTime() - m * 60_000)).toISOString()
const line = (id: string, t: string): string => JSON.stringify({
type: 'assistant',
timestamp: t,
@ -111,7 +117,7 @@ async function seedLiveTodaySession(): Promise<void> {
usage: { input_tokens: 90000, output_tokens: 12000, cache_creation_input_tokens: 0, cache_read_input_tokens: 300000 },
},
})
await writeFile(join(projectDir, 's-today.jsonl'), [line('m1', at(12, 0)), line('m2', at(12, 30))].join('\n') + '\n', 'utf-8')
await writeFile(join(projectDir, 's-today.jsonl'), [line('m1', minutesAgo(10)), line('m2', minutesAgo(5))].join('\n') + '\n', 'utf-8')
}
/** What the name-filtered live parse alone reports — the By Project panel's source. */
@ -320,13 +326,18 @@ describe('carried days with no per-project split (pre-v15)', () => {
})
it('says so in the overview instead of just showing a short total', async () => {
await seedCache(carriedDayWithoutProjects(daysAgoStr(10)))
// Two cached days: one that cannot be attributed (the footnote's subject) and
// one that can. The attributable day keeps the headline non-zero from the
// CACHE alone, so the assertion tests the footnote rather than depending on
// the live parse to keep renderOverview off its "No usage found" path.
await seedCache(carriedDayWithoutProjects(daysAgoStr(10)), carriedDayWithProjects(daysAgoStr(5)))
await seedLiveTodaySession()
const range = coveringRange()
clearSessionCache()
const durable = await buildDurablePeriod({ range, label: 'This month' }, { provider: 'all', exclude: ['drop-me'] })
expect(durable.unattributedCostUSD).toBeGreaterThan(0)
expect(durable.data.cost).toBeGreaterThan(0)
const rendered = renderOverview(durable.liveProjects, {
label: 'This month',