Files
Arcrun/cypher-executor/tests/console-dashboard-model.test.ts
T
Leo 99da35a885 fix(console): 駕駛艙首頁改讀 live 一手資料,stale 殘骸誠實標示
leo 抱怨「等你的事」錯了好幾天、首頁總覽對不上實際進度。診斷結論:
- dash_wait 沒有活的維護管線(最後寫入 07-04;等leo清單#11 已於 07-05
  銷案=誤判,dashboard 卻繼續掛著)——真相源其實是 InkStoneCo sprint 檔
  「## 等 leo 清單」(progress-guard 每日核實維護)
- dash_task 的 scope:today 沒有日期,07-04 的路線到 07-07 還被當今天的
- 燈號吃到 stale 任務;系統狀況/總覽無 live 信號

修法:
- 等你的事:首選 Gitea sprint 等leo清單(合併最新兩檔——07b 開了、🔴 仍
  掛 07a;需 GITEA_TOKEN secret);讀不到 fallback dash_wait 必標 age+stale
- 今日路線:台北日曆日判 is_today,非今日寫入降級「最後路線(N 天前)」
- 燈號只吃今日寫入任務+心跳+KBDB /health,附 light_reason
- 新增系統狀況(KBDB /health、embed enabled:false 誠實顯示、graph
  /triplets/stats、workflow 數)與總庫規模(entries/wiki_card/triplets)全 live
- 聚合判定抽純函式 lib/console-dashboard-model.ts,vitest 15 案例
  (含真實 sprint 檔形態:劃掉列/ 急迫欄/R1 字母編號/🔴 排序)

驗證:tsc --noEmit 0;vitest 65/66(1 失敗為 main 既有 executor.test.ts,
stash 復原後同樣失敗,非本次引入);parser 對 live sprint-2026-07a/07b
dry-run 撈出 🔴#13 等 9 項 open、清單維護於 31 分鐘前。

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

195 lines
10 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 天前');
});
});