Files
Arcrun/cypher-executor/tests/console-dashboard-cache.test.ts
T
Leo 0d9a6e8eed perf(console): Gitea 等leo清單讀取加 CF Cache 層(TTL 90s,#36 審查①)
- cachedGiteaWaiting:caches.default + Cache-Control max-age=90;前端 60s
  刷新下 Gitea 從每分鐘 3-4 call 降到 ≤1 輪/90s
- hit 回放用 reviveWaitingAges 隨牆鐘補算「維護於 N 分鐘前」(不停走)
- 失敗不快取(不把網路抖動放大成 90 秒盲區);cache.put 走 waitUntil 不阻塞
- waiting_meta.cache 吐 hit/miss,部署後 curl 兩次即得快取生效證據
- 測試:tests/console-dashboard-cache.test.ts 4 案例(真 workerd Cache API,
  注入 fetcher 計數證明 hit 不再打 Gitea);全套 69/70(1 fail=main 既有)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
2026-07-07 11:19:19 +00:00

100 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* cachedGiteaWaiting 快取層測試(總管 #36 審查要求的證據)。
*
* 跑在 vitest-pool-workersworkerd)裡,caches.default 是真的 Cache API——
* 不是 mock cache 物件;fetcher 用注入的假函式計數,證明:
* 1. 第一次 miss(打 fetcher)→ 90s 內第二次 hitfetcher 不再被打)
* 2. hit 回放時 age 隨牆鐘推進(不回放停走的時間)
* 3. fetcher 失敗(回 null)不快取——下次仍會重試,不把抖動放大成 90 秒盲區
* 4. 缺 GITEA_BASE_URL/GITEA_TOKEN 直接 null(快取層不啟動)
*/
import { describe, it, expect } from 'vitest';
import { cachedGiteaWaiting } from '../src/routes/console-dashboard';
import type { WaitingModel } 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 model(): WaitingModel {
return {
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'],
};
}
/** 收集 waitUntil 的 promiseflush 後 cache.put 才保證落地 */
function collector() {
const pending: Promise<unknown>[] = [];
return { waitUntil: (p: Promise<unknown>) => pending.push(p), flush: () => Promise.all(pending) };
}
describe('cachedGiteaWaiting — Gitea 讀取快取(TTL 90s', () => {
it('第一次 miss 打 fetcher90s 內第二次 hit、fetcher 不再被打', async () => {
const env = fakeEnv('Leo/cache-test-1');
let calls = 0;
const fetcher = async () => {
calls++;
return model();
};
const c = collector();
const t0 = Date.now();
const r1 = await cachedGiteaWaiting(env, t0, c.waitUntil, fetcher);
expect(r1?.cache).toBe('miss');
expect(calls).toBe(1);
await c.flush(); // cache.put 落地
const r2 = await cachedGiteaWaiting(env, t0 + 60_000, c.waitUntil, fetcher);
expect(r2?.cache).toBe('hit'); // ← 快取生效的客觀證據
expect(calls).toBe(1); // fetcher 沒有被第二次呼叫=Gitea 沒被打
expect(r2?.items[0].id).toBe('13');
});
it('hit 回放時 age 隨牆鐘推進(存 10 分鐘前、過 60s 讀 → 11 分鐘前),不停走', async () => {
const env = fakeEnv('Leo/cache-test-2');
const c = collector();
const t0 = Date.now();
await cachedGiteaWaiting(env, t0, c.waitUntil, async () => model());
await c.flush();
const r = await cachedGiteaWaiting(env, t0 + 60_000, c.waitUntil, async () => model());
expect(r?.cache).toBe('hit');
expect(r?.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 cachedGiteaWaiting(env, Date.now(), c.waitUntil, failing)).toBeNull();
await c.flush();
expect(await cachedGiteaWaiting(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 model();
};
const c = collector();
expect(await cachedGiteaWaiting(env, Date.now(), c.waitUntil, fetcher)).toBeNull();
expect(calls).toBe(0);
});
});