/** * cachedGiteaSprint 快取層測試(總管 #36 審查要求的證據;fix/console-truth-audit 起 * 同一輪快取多帶任務板 board——「等你的事」與「今日完成」共用同一次 Gitea fetch)。 * * 跑在 vitest-pool-workers(workerd)裡,caches.default 是真的 Cache API—— * 不是 mock cache 物件;fetcher 用注入的假函式計數,證明: * 1. 第一次 miss(打 fetcher)→ 90s 內第二次 hit(fetcher 不再被打),board 一起回放 * 2. hit 回放時 age 隨牆鐘推進(不回放停走的時間) * 3. fetcher 失敗(回 null)不快取——下次仍會重試,不把抖動放大成 90 秒盲區 * 4. 缺 GITEA_BASE_URL/GITEA_TOKEN 直接 null(快取層不啟動) */ import { describe, it, expect } from 'vitest'; import { cachedGiteaSprint } from '../src/routes/console-dashboard'; import type { SprintSnapshot } from '../src/lib/console-dashboard-model'; import type { Bindings } from '../src/types'; function fakeEnv(repo: string): Bindings { return { GITEA_BASE_URL: 'https://git.example.test', GITEA_TOKEN: 'test-token', GITEA_SPRINT_REPO: repo, // 每測試一個 repo → cache key 隔離 GITEA_SPRINT_DIR: 'system-dev/docs/3-specs/autonomy-dispatch', } as unknown as Bindings; } function snapshot(): SprintSnapshot { return { waiting: { items: [{ title: 'mira .env 歷史憑證外洩', id: '13', urgency: '🔴', sprint: 'sprint-2026-07a.md' }], source: 'gitea_sprint', updated_ago_minutes: 10, stale: false, sprint_files: ['sprint-2026-07b.md', 'sprint-2026-07a.md'], }, board: [ { title: 'T5-graph(Graph 查詢 workflow 化)', status: 'todo', completed_days: [], sprint: 'sprint-2026-07b.md' }, { title: 'T-kb2 07-07', status: 'done', completed_days: ['2026-07-05'], sprint: 'sprint-2026-07a.md' }, ], }; } /** 收集 waitUntil 的 promise,flush 後 cache.put 才保證落地 */ function collector() { const pending: Promise[] = []; return { waitUntil: (p: Promise) => pending.push(p), flush: () => Promise.all(pending) }; } describe('cachedGiteaSprint — Gitea 讀取快取(TTL 90s,waiting + board 同輪)', () => { it('第一次 miss 打 fetcher;90s 內第二次 hit、fetcher 不再被打,board 一起回放', async () => { const env = fakeEnv('Leo/cache-test-1'); let calls = 0; const fetcher = async () => { calls++; return snapshot(); }; const c = collector(); const t0 = Date.now(); const r1 = await cachedGiteaSprint(env, t0, c.waitUntil, fetcher); expect(r1?.cache).toBe('miss'); expect(calls).toBe(1); await c.flush(); // cache.put 落地 const r2 = await cachedGiteaSprint(env, t0 + 60_000, c.waitUntil, fetcher); expect(r2?.cache).toBe('hit'); // ← 快取生效的客觀證據 expect(calls).toBe(1); // fetcher 沒有被第二次呼叫=Gitea 沒被打 expect(r2?.waiting.items[0].id).toBe('13'); // 任務板同輪回放:存的是原始 completed_days(「今天完成幾件」由請求當下算,跨午夜不冒領) expect(r2?.board?.length).toBe(2); expect(r2?.board?.[1].completed_days).toEqual(['2026-07-05']); }); it('hit 回放時 age 隨牆鐘推進(存 10 分鐘前、過 60s 讀 → 11 分鐘前),不停走', async () => { const env = fakeEnv('Leo/cache-test-2'); const c = collector(); const t0 = Date.now(); await cachedGiteaSprint(env, t0, c.waitUntil, async () => snapshot()); await c.flush(); const r = await cachedGiteaSprint(env, t0 + 60_000, c.waitUntil, async () => snapshot()); expect(r?.cache).toBe('hit'); expect(r?.waiting.updated_ago_minutes).toBe(11); // 10 + 1 分鐘 drift }); it('fetcher 失敗(null)不快取:下一次仍重打 fetcher', async () => { const env = fakeEnv('Leo/cache-test-3'); const c = collector(); let calls = 0; const failing = async () => { calls++; return null; }; expect(await cachedGiteaSprint(env, Date.now(), c.waitUntil, failing)).toBeNull(); await c.flush(); expect(await cachedGiteaSprint(env, Date.now(), c.waitUntil, failing)).toBeNull(); expect(calls).toBe(2); // 失敗沒有被 90 秒快取住 }); it('缺 GITEA_BASE_URL/GITEA_TOKEN → null(快取層不啟動,caller 走 fallback)', async () => { const env = { GITEA_SPRINT_REPO: 'Leo/x' } as unknown as Bindings; let calls = 0; const fetcher = async () => { calls++; return snapshot(); }; const c = collector(); expect(await cachedGiteaSprint(env, Date.now(), c.waitUntil, fetcher)).toBeNull(); expect(calls).toBe(0); }); });