Files
Arcrun/cypher-executor/tests/console-dashboard-model.test.ts
T
cloud-worker 5690d61438 fix(console): 真實性稽核修正——時區統一台北、精耕層數字、今日完成接 sprint 任務板
稽核輪(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 手法)。
2026-07-07 12:25:31 +00:00

290 lines
15 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.
/**
* console 駕駛艙聚合純函式測試(fix/console-dashboard-live-data2026-07-07
*
* 針對 leo 抱怨「等你的事錯了好幾天」的三個 stale 根因逐一驗證:
* 1. dash_task 的 scope:"today" 沒日期 → 幾天前的任務不得再算「今日」
* 2. dash_wait 殘資料 fallback 必標 stale
* 3. sprint 檔「等 leo 清單」表格解析:已銷案(~~/✅/已完成)要濾掉、🔴 排最前
*
* 樣本資料直接取自 leo21c live KBDB 與 InkStoneCo sprint-2026-07a.md 的真實形態
* epoch 秒 created_at、07-04 殘 entries、含劃掉列的 markdown 表格),非憑空編造。
*/
import { describe, it, expect } from 'vitest';
import {
parseCreatedAtMs,
taipeiDayKey,
buildRouteModel,
buildWaitingFallback,
parseSprintWaitingTable,
sortWaitingItems,
cleanWaitingTitle,
pickLatestSprintFiles,
humanAge,
type KbdbEntry,
} from '../src/lib/console-dashboard-model';
// 2026-07-07 10:00 UTC(台北 18:00)當「現在」
const NOW = Date.parse('2026-07-07T10:00:00Z');
const SEC = (iso: string) => Math.floor(Date.parse(iso) / 1000);
function entry(created: string, content: unknown): KbdbEntry {
return { id: crypto.randomUUID(), entry_type: 'x', content: JSON.stringify(content), created_at: SEC(created) };
}
describe('parseCreatedAtMs / taipeiDayKey', () => {
it('吃 epoch 秒(leo21c 實測形態)、epoch 毫秒、ISO、sqlite 字串', () => {
expect(parseCreatedAtMs(1783413742)).toBe(1783413742000);
expect(parseCreatedAtMs(1783413742000)).toBe(1783413742000);
expect(parseCreatedAtMs('2026-07-07T08:42:22Z')).toBe(Date.parse('2026-07-07T08:42:22Z'));
expect(parseCreatedAtMs('2026-07-07 08:42:22')).toBe(Date.parse('2026-07-07T08:42:22Z'));
expect(parseCreatedAtMs(null)).toBeNull();
});
it('台北日曆日:UTC 20:00 已是台北隔天', () => {
expect(taipeiDayKey(Date.parse('2026-07-06T20:00:00Z'))).toBe('2026-07-07');
expect(taipeiDayKey(Date.parse('2026-07-06T15:00:00Z'))).toBe('2026-07-06');
});
});
describe('buildRouteModel — 今日路線不吃殘資料(stale 根因 2)', () => {
it('07-04 寫入的 scope:today 任務,07-07 看不再算今日(is_today=false、today_total=0', () => {
// live 實態:dash_task 最後寫入 2026-07-0407-07 dashboard 仍把它當「今日 2/4」
const entries = [
entry('2026-07-04T10:22:00Z', { title: 'T-loop4 檢討 routine 首跑', status: 'done', order: 1, scope: 'today' }),
entry('2026-07-04T04:44:00Z', { title: 'T-cockpit 駕駛艙三件套', status: 'doing', order: 2, scope: 'today' }),
entry('2026-07-04T04:44:00Z', { title: 'T-kb-skeleton 總庫 0→1', status: 'todo', order: 1, scope: 'week' }),
];
const m = buildRouteModel(entries, NOW);
expect(m.is_today).toBe(false);
expect(m.today_total).toBe(0); // 不假裝 07-04 的路線是今天的
expect(m.today_done).toBe(0);
expect(m.tasks).toHaveLength(3); // 資料仍在(頁面降級顯示「最後路線(N 天前)」)
expect(m.tasks.every((t) => !t.is_today_write)).toBe(true);
expect(m.updated_ago_minutes).toBeGreaterThan(3 * 24 * 60 - 60); // ≈3 天
});
it('今天寫入的任務照常計入今日(同 title 後寫蓋前寫)', () => {
const entries = [
entry('2026-07-07T09:00:00Z', { title: 'A', status: 'done', order: 1, scope: 'today' }), // 最新(DESC first
entry('2026-07-07T08:00:00Z', { title: 'B', status: 'doing', order: 2, scope: 'today' }),
entry('2026-07-04T04:44:00Z', { title: 'A', status: 'todo', order: 1, scope: 'today' }), // 舊值被蓋
];
const m = buildRouteModel(entries, NOW);
expect(m.is_today).toBe(true);
expect(m.today_total).toBe(2);
expect(m.today_done).toBe(1);
expect(m.tasks.find((t) => t.title === 'A')?.status).toBe('done');
});
it('空庫:誠實回 -1 / false,不編資料', () => {
const m = buildRouteModel([], NOW);
expect(m.tasks).toHaveLength(0);
expect(m.is_today).toBe(false);
expect(m.updated_ago_minutes).toBe(-1);
});
});
describe('buildWaitingFallback — dash_wait 殘資料必標 stalestale 根因 1', () => {
it('07-04 的 open 項在 07-07 仍列出,但 stale=true、age 正確', () => {
// live 實態:等leo清單#11 已於 07-05 銷案(誤判),dash_wait 卻沒人寫 closed
const entries = [
entry('2026-07-04T11:05:00Z', { title: '檢查 Claude Environment 兩個 routine trigger', status: 'open' }),
entry('2026-07-04T10:41:00Z', { title: 'CF token 加 Vectorize:Edit 權限', status: 'closed' }),
entry('2026-07-04T05:56:00Z', { title: 'CF token 加 Vectorize:Edit 權限', status: 'open' }), // 舊 open 被 closed 蓋
];
const m = buildWaitingFallback(entries, NOW);
expect(m.source).toBe('kbdb_dash_wait');
expect(m.items.map((i) => i.title)).toEqual(['檢查 Claude Environment 兩個 routine trigger']);
expect(m.stale).toBe(true); // >24h 沒維護 → 頁面必須警示「可能過時」
expect(m.updated_ago_minutes).toBeGreaterThan(24 * 60);
});
it('完全無資料 → source none + 管線未接 note', () => {
const m = buildWaitingFallback([], NOW);
expect(m.source).toBe('none');
expect(m.items).toHaveLength(0);
expect(m.note).toContain('管線未接');
});
});
describe('parseSprintWaitingTable — 等leo清單活資料源(stale 根因 3 的解)', () => {
// 取自 InkStoneCo sprint-2026-07a.md 真實形態(截短),含:劃掉列、✅ 急迫欄、🔴/🟡/⚪
const MD = `# Sprint 2026-07a
## 任務板
- [x] 東西
## 等 leo 清單(暗待辦歸零區)
| # | 事項 | 急迫 |
|---|------|------|
| 1 | ~~Claude Environment 補 secrets~~ ✅ 07-02 實測確認 | ⚪ 已完成 |
| 3 | Telegram bot token rotate(可與 #1 一起做) | 🟡 建議今天順手 |
| 9 | **credential 路徑1 不成立,SDD 需重新設計**(詳情見 Arcrun#2 | ✅ 已解,T2-T9 解凍 |
| 11 | ~~cloud-worker trigger 連續兩天疑似未觸發~~ **✅ 銷案=誤判** | ✅ 銷案(誤判)+防複發已上線 |
| 14 | **\`NPM_API_TOKEN\` 缺失,acr search 等 CLI 改動全數卡在 npm 未發版**2026-07-05 核實):Environment 現只有五把 | 🟡 建議本週補 |
| 13 | 🔴 **mira \`.env\` 歷史憑證外洩(2026-07-05 發現,已止血未 rotate**mira repo 曾把 .env 誤入 git 追蹤 | 🔴 需 leo 裁(安全事件) |
| 15 | **T-cockpit③(md→Gitea Project 投影)方向選擇**2026-07-06 spike 核實):需 leo 三選一 | 🟡 品味/方向題 |
### 下一段
別的內容`;
it('已銷案列(~~ 或急迫欄 ✅/已完成)全濾掉;open 項留下、🔴 排最前', () => {
const items = parseSprintWaitingTable(MD, 'sprint-2026-07a.md')!;
expect(items).not.toBeNull();
expect(items.map((i) => i.id)).toEqual(['13', '3', '14', '15']); // 🔴 最前,餘依表序
expect(items[0].urgency).toBe('🔴');
expect(items[0].title).toContain('mira');
expect(items[0].sprint).toBe('sprint-2026-07a.md');
expect(items.every((i) => !i.title.includes('~~') && !i.title.includes('**'))).toBe(true);
});
it('07b 形態:段落標題帶註記「(RAG 線)」、編號帶字母前綴 R1', () => {
const md07b = `## 等 leo 清單(RAG 線)
| # | 事項 | 急迫 |
|---|------|------|
| R1 | pilot 客戶挑選(1–2 家)+產品命名/定價區間(rag-wave1 T0 | 🟡 Phase 0 前置 |
| R2 | ~~Gemini key~~ ✅ 已補 | ⚪ 已完成 |
`;
const items = parseSprintWaitingTable(md07b, 'sprint-2026-07b.md')!;
expect(items.map((i) => i.id)).toEqual(['R1']);
expect(items[0].urgency).toBe('🟡');
});
it('跨 sprint 合併排序:現役檔項目先傳,🔴 仍浮最上', () => {
const b = parseSprintWaitingTable('## 等 leo 清單\n| # | 事項 | 急迫 |\n|---|---|---|\n| R1 | RAG pilot | 🟡 x |', 'b.md')!;
const a = parseSprintWaitingTable('## 等 leo 清單\n| # | 事項 | 急迫 |\n|---|---|---|\n| 13 | 憑證外洩 | 🔴 x |', 'a.md')!;
const merged = sortWaitingItems([...b, ...a]);
expect(merged.map((i) => i.id)).toEqual(['13', 'R1']);
});
it('沒有等 leo 段落 → nullcaller 誠實 fallback,不硬湊)', () => {
expect(parseSprintWaitingTable('# 別的檔\n沒有清單')).toBeNull();
expect(parseSprintWaitingTable('## 等 leo 清單\n(還沒建表)')).toBeNull();
});
it('cleanWaitingTitle:去粗體、長標題在「:」截斷、80 字上限', () => {
expect(cleanWaitingTitle('**NPM_API_TOKEN 缺失,CLI 卡在 npm 未發版**07-05 核實):Environment 現只有五把')).toBe(
'NPM_API_TOKEN 缺失,CLI 卡在 npm 未發版(07-05 核實)',
);
expect(cleanWaitingTitle('短標題').length).toBeLessThan(80);
});
it('cleanWaitingTitle:去開頭急迫 emoji(急迫欄已有)、截斷後不留未閉合括號', () => {
// 等leo清單#13 實際形態:事項欄自帶 🔴 開頭
expect(cleanWaitingTitle('🔴 **mira `.env` 歷史憑證外洩(2026-07-05 發現)**mira repo 曾把 .env 誤入 git')).toBe(
'mira .env 歷史憑證外洩(2026-07-05 發現)',
);
// 等leo清單#3 實際形態:「:」在括號內 → 截斷會留半個「(」,要剪掉
expect(cleanWaitingTitle('Telegram bot token rotate(可與 #1 一起做:rotate 後直接填新值進 Environment')).toBe(
'Telegram bot token rotate',
);
});
});
describe('pickLatestSprintFiles / humanAge', () => {
it('sprint-*.md 字典序取最新兩個(新→舊);無 sprint 檔回空陣列', () => {
// live 實態(2026-07-07):07b 已開、07a 的 🔴 未銷案項仍要讀 → 兩檔都拿
expect(pickLatestSprintFiles(['sprint-2026-07a.md', 'sprint-2026-07b.md', 'tasks.md', 'routines'])).toEqual([
'sprint-2026-07b.md',
'sprint-2026-07a.md',
]);
expect(pickLatestSprintFiles(['sprint-2026-06b.md', 'sprint-2026-07a.md', 'sprint-2026-05a.md'])).toEqual([
'sprint-2026-07a.md',
'sprint-2026-06b.md',
]);
expect(pickLatestSprintFiles(['tasks.md'])).toEqual([]);
});
it('humanAge 誠實顯示天級 age', () => {
expect(humanAge(-1)).toBe('時間不明');
expect(humanAge(30)).toBe('30 分鐘前');
expect(humanAge(3 * 24 * 60)).toBe('3 天前');
});
});
// ── sprint 任務板(「今日完成」活資料源,fix/console-truth-audit)────────────────────
// 樣本取自 sprint-2026-07a/07b.md 真實維護慣例:`- [x]`+行內「**完成(YYYY-MM-DD…)**」、
// `- [🔄]` 進行中、`- [!]` 卡住、縮排子彈(「 - 做法:」)非任務、一行多次完成(T6-T9 分兩天)。
import {
parseSprintTaskBoard,
buildSprintRouteModel,
cleanBoardTitle,
} from '../src/lib/console-dashboard-model';
const BOARD_MD = `# Sprint 2026-07x
## 任務板(P1 > P2 > P3worker 由上往下取)
### P1
- [ ] **T5-graphGraph 查詢 workflow 化)**:把 neighbors/traverse 改寫成 workflow。
- 做法:在 \`Leo/arcrun-rag\` 建 workflows/graph-neighbors.yaml
- 驗收:curl 原始輸出貼證據。
- [🔄] **T-cockpit 駕駛艙三件套**:①②完成(2026-07-04[總管]),③ spike 卡方向。
- [x] RECONgit proxy 不攔 Gitea06-30 已驗)
- [x] **T-loop1d 今晚任務**:實作 console v0。**完成(2026-07-02[cloud-worker]**:全綠。
- [x] T6-T907-06~08 提前):**T8/T9 完成(2026-07-04commit 1d19d46****T6/T7 完成(2026-07-05 本機總管)**。
- [!] 附帶:acr search npm 漏發——**卡住(2026-07-05[cloud-worker]**:缺 NPM_API_TOKEN。
### 候補區(本 sprint 不佔額度)
- 短網址通用化——規劃見 content-pipeline.md
## 等 leo 清單
| # | 事項 | 急迫 |
|---|------|------|
| 1 | 假裝這裡有一項 | 🟡 |
`;
describe('parseSprintTaskBoard — 任務板解析(寬鬆容錯,解析不了寧可略過)', () => {
it('只收行首 checkbox;縮排子彈/候補區敘述/等leo清單表格都不是任務', () => {
const tasks = parseSprintTaskBoard(BOARD_MD, 'sprint-2026-07x.md');
expect(tasks).not.toBeNull();
expect(tasks!.map((t) => t.status)).toEqual(['todo', 'doing', 'done', 'done', 'done', 'blocked']);
expect(tasks!.every((t) => t.sprint === 'sprint-2026-07x.md')).toBe(true);
});
it('完成日期:抓行內所有「完成(YYYY-MM-DD」;[x] 無日期=空陣列(不冒領)', () => {
const tasks = parseSprintTaskBoard(BOARD_MD)!;
expect(tasks[2].completed_days).toEqual([]); // RECON:勾了但沒帶日期 → 不歸任何一天
expect(tasks[3].completed_days).toEqual(['2026-07-02']);
expect(tasks[4].completed_days).toEqual(['2026-07-04', '2026-07-05']); // 一行兩次完成
expect(tasks[1].completed_days).toEqual(['2026-07-04']); // doing 項的部分完成標記也記著(狀態仍 doing)
});
it('標題:優先取第一個粗體;無粗體取全形冒號前', () => {
const tasks = parseSprintTaskBoard(BOARD_MD)!;
expect(tasks[0].title).toBe('T5-graphGraph 查詢 workflow 化)');
expect(tasks[2].title).toBe('RECON');
expect(cleanBoardTitle('附帶:`acr search` npm 漏發')).toBe('附帶:acr search npm 漏發'.slice(0, 80));
});
it('沒有任務板段落 → nullcaller 誠實 fallback dash_task', () => {
expect(parseSprintTaskBoard('# 只有標題\n\n## 等 leo 清單\n| 1 | x | 🔴 |')).toBeNull();
});
});
describe('buildSprintRouteModel — 「今日完成」只認今天(台北)的完成標記', () => {
it('NOW=07-0707-04/07-05 完成的不算今日;開項全列(今日路線)', () => {
const tasks = parseSprintTaskBoard(BOARD_MD)!;
const m = buildSprintRouteModel(tasks, NOW);
expect(m.today_done).toBe(0); // 板上今天沒勾任何完成 → 誠實 0
expect(m.done_today_titles).toEqual([]);
// 開項:todo + doing + blocked 3today_total 0 完成 + 3 開
expect(m.today_total).toBe(3);
expect(m.tasks.map((t) => t.status)).toEqual(['todo', 'doing', 'blocked']);
});
it('完成標記日期=今天 → 進今日完成清單;歷史完成項不出現', () => {
const tasks = parseSprintTaskBoard(BOARD_MD)!;
// 模擬 progress-guard 今天勾掉 T5-graph
tasks[0] = { ...tasks[0], status: 'done', completed_days: [taipeiDayKey(NOW)] };
const m = buildSprintRouteModel(tasks, NOW);
expect(m.today_done).toBe(1);
expect(m.done_today_titles).toEqual(['T5-graphGraph 查詢 workflow 化)']);
expect(m.today_total).toBe(1 + 2); // 1 完成 + doing/blocked 2 開項
expect(m.tasks[0].status).toBe('done'); // 今日完成排最前(✓ 給 leo 看「做了什麼」)
// 07-02/07-04/07-05 完成的三項(含無日期 RECON)都不在清單裡
expect(m.tasks.find((t) => t.title === 'RECON')).toBeUndefined();
});
it('跨台北午夜:昨天的完成不會被冒領成今天(快取安全性的模型面保證)', () => {
const tasks = parseSprintTaskBoard(BOARD_MD)!;
tasks[0] = { ...tasks[0], status: 'done', completed_days: ['2026-07-07'] };
// 台北 07-08 00:10UTC 07-07 16:10)再看:07-07 的完成已不是「今日」
const afterMidnight = Date.parse('2026-07-07T16:10:00Z');
expect(buildSprintRouteModel(tasks, afterMidnight).today_done).toBe(0);
});
});