5690d61438
稽核輪(fix/console-truth-audit,總管派工+leo 三裁決): B1 時區:console 全站日期/時間顯示統一 Asia/Taipei(新 lib/taipei-time.ts, server 判定與頁面 client JS 同一套、等價性單測互鎖)。原本用瀏覽器本地 getter——換裝置就換日期;憑證頁改顯「日期+時分(台北)」:raw 1783349834 =台北 2026-07-06 22:57,7/6 顯示是真的,缺的是時分與固定時區。 B2 總庫數字(leo 裁:14-E 遺產 deprecated 不再顯示):總庫搜尋/設定頁頭部 統計改讀新 GET /console/kb-scale-data(精耕層 wiki 卡/三元組/已嵌入+最近 寫入,全 live、limit=1/stats 聚合不掃庫);搜尋仍搜全庫、結果 wiki_card 加「精耕」綠標;dashboard-data 的 entries 計數補 owner_id(原混租戶: 459,137 vs leo 458,732)。 B3 設定頁:vectorize 文案改具體事實(config kbdb_embed+acr update,#32 教訓)+live 狀態;新增 MCP token TTL 誠實佔位(GET /console/settings-data, 可調功能=Arcrun#19,不假裝能遠端改)。 C 今日完成/今日路線接 sprint 任務板(leo 拍板):比照 #36 等leo清單模式, 同一輪 Gitea fetch(90s CF cache 共用)解析「## 任務板」checkbox;「今日 完成」只認「完成(今天台北日)」標記,[x] 無日期不冒領;快取存原始 completed_days,跨台北午夜不誤算;板檔今天沒 commit → 頁面誠實標「今日 任務板未更新」;dash_task 降 fallback、燈號判定不變。parser 以 live 07a/07b 檔實測(標題抽取棄「首個粗體」——實讀發現會抽到完成註記)。 驗證:tsc exit 0;vitest 82/83(1 失敗=pre-existing executor「不存在的 零件」,stash 於 main 複驗同樣紅);新增 taipei-time 10 案例(含跨日/跨年/ client-server 等價/leo 實測 epoch)+任務板 parser 7 案例;兩頁 inline JS 渲染後抽 <script> node --check 全過(mistakes #24 手法)。
110 lines
4.6 KiB
TypeScript
110 lines
4.6 KiB
TypeScript
/**
|
||
* 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<unknown>[] = [];
|
||
return { waitUntil: (p: Promise<unknown>) => 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);
|
||
});
|
||
});
|