From 15962202bd85ad6b0d1d12ea0c12d401ffb73b61 Mon Sep 17 00:00:00 2001 From: "chengchuan.zhou" Date: Mon, 3 Aug 2026 11:42:32 +0800 Subject: [PATCH] test(project-filter): make the durable-headline filter tests time-of-day proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/project-filter-durable-totals.test.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/project-filter-durable-totals.test.ts b/tests/project-filter-durable-totals.test.ts index 889e460..ff030aa 100644 --- a/tests/project-filter-durable-totals.test.ts +++ b/tests/project-filter-durable-totals.test.ts @@ -83,13 +83,13 @@ function carriedDayWithoutProjects(date: string): DailyEntry { return day } -async function seedCache(day: DailyEntry): Promise { +async function seedCache(...days: DailyEntry[]): Promise { 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 { 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 { 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',