5cd790c94a
console 頁 7 從 Telegram 訊息清單改裝成三欄分流台: - 資料源二合一:entry_type=todo(Logseq 萃取,Arcrun#8 ingest 線)+ entry_type=inbox (Telegram),/console/inbox-data → /console/triage-data(唯一消費者是本頁,一起改 裝不留死端點;仍鎖 console session——待辦原文屬機敏) - 三欄 80/15/5:ai=AI 會幫我搞定/collab=協作/leo=非我不可;owner_tier 缺席或看 不懂(triage engine 還沒判)→ 歸 collab 欄+「未分流」徽章,不冒領 ai、不誤判 leo - 各項顯示 text/project 徽章/source/marker;status:done 預設隱藏可切換 - per-project chips 過濾(全部/各 project/未分項),前端同包資料過濾零額外請求 - 手機直排沿 console 單檔薄殼;讀 KBDB 走既有 kbdbBase HTTP 慣例,不新增 binding 契約解析/分欄/計數=純函式 lib/console-triage-model.ts(比照 console-dashboard-model 模式,route 只做 IO),tests/console-triage-model.test.ts 11 測全綠(紅→綠驗證:抽掉 實作模組同測試即紅)。全套 vitest 98 passed,唯一失敗為 pre-existing executor 「不存在的零件」訊息斷言(main 同紅,與本 PR 無關)。 [總管-subagent] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
6.3 KiB
TypeScript
135 lines
6.3 KiB
TypeScript
/**
|
||
* 分流台純函式測試(Arcrun#9 收件夾改裝=全部待辦分流台,2026-07-08)
|
||
*
|
||
* 驗證兩份 content 契約 → 統一分流項的判定:
|
||
* 1. todo 契約 {"text","marker","owner_tier","project","source","status"} 照 render
|
||
* 2. inbox 契約 {"text","from","status"} 無 tier → 歸 collab 欄+unclassified 標記
|
||
* 3. 三欄計數只算未完成(status:done 不佔欄)、per-project 清單去重排序
|
||
* 4. 防禦:owner_tier 看不懂/content 非 JSON 不得讓項目蒸發或冒領 ai 欄
|
||
*
|
||
* 樣本形態對照 leo21c live KBDB(epoch 秒 created_at)與 kb-ingest SDD R5/R7 的
|
||
* Logseq marker 詞彙(TODO/DOING/LATER/NOW),非憑空編造。
|
||
*/
|
||
import { describe, it, expect } from 'vitest';
|
||
import { todoToTriageItem, inboxToTriageItem, buildTriageModel } from '../src/lib/console-triage-model';
|
||
import type { KbdbEntry } from '../src/lib/console-dashboard-model';
|
||
|
||
const SEC = (iso: string) => Math.floor(Date.parse(iso) / 1000);
|
||
|
||
function entry(created: string, content: unknown, type = 'todo'): KbdbEntry {
|
||
return {
|
||
id: crypto.randomUUID(),
|
||
entry_type: type,
|
||
content: typeof content === 'string' ? content : JSON.stringify(content),
|
||
created_at: SEC(created),
|
||
};
|
||
}
|
||
|
||
describe('todoToTriageItem — Logseq todo 契約', () => {
|
||
it('契約全欄照收(text/marker/owner_tier/project/source/status)', () => {
|
||
const it_ = todoToTriageItem(
|
||
entry('2026-07-08T02:00:00Z', {
|
||
text: '把 KBDB 憑證輪替',
|
||
marker: 'TODO',
|
||
owner_tier: 'ai',
|
||
project: 'Arcrun',
|
||
source: 'notes',
|
||
status: 'new',
|
||
}),
|
||
);
|
||
expect(it_.text).toBe('把 KBDB 憑證輪替');
|
||
expect(it_.marker).toBe('TODO');
|
||
expect(it_.tier).toBe('ai');
|
||
expect(it_.unclassified).toBe(false);
|
||
expect(it_.project).toBe('Arcrun');
|
||
expect(it_.source).toBe('notes');
|
||
expect(it_.status).toBe('new');
|
||
expect(it_.origin).toBe('todo');
|
||
expect(it_.at_ms).toBe(Date.parse('2026-07-08T02:00:00Z'));
|
||
});
|
||
it('三欄值 ai/collab/leo 都認;issue 舊詞 mira80/collab15/leo5 當同義收', () => {
|
||
const tier = (v: unknown) => todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', owner_tier: v }));
|
||
expect(tier('collab').tier).toBe('collab');
|
||
expect(tier('leo').tier).toBe('leo');
|
||
expect(tier('leo').unclassified).toBe(false);
|
||
expect(tier('mira80').tier).toBe('ai');
|
||
expect(tier('collab15').tier).toBe('collab');
|
||
expect(tier('leo5').tier).toBe('leo');
|
||
});
|
||
it('owner_tier 缺席/看不懂 → 歸 collab 欄+unclassified,不冒領 ai、不蒸發', () => {
|
||
const noTier = todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: '還沒 triage 的', marker: 'LATER' }));
|
||
expect(noTier.tier).toBe('collab');
|
||
expect(noTier.unclassified).toBe(true);
|
||
const weird = todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', owner_tier: 'boss' }));
|
||
expect(weird.tier).toBe('collab');
|
||
expect(weird.unclassified).toBe(true);
|
||
});
|
||
it('content 非 JSON(不合契約殘資料)→ 原文當 text 誠實顯示,不丟棄', () => {
|
||
const it_ = todoToTriageItem(entry('2026-07-08T02:00:00Z', 'TODO 裸文字殘資料'));
|
||
expect(it_.text).toBe('TODO 裸文字殘資料');
|
||
expect(it_.tier).toBe('collab');
|
||
expect(it_.unclassified).toBe(true);
|
||
expect(it_.status).toBe('new');
|
||
});
|
||
it('status 只有 done 算完成,其餘(含缺席/怪值)一律 new', () => {
|
||
expect(todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', status: 'done' })).status).toBe('done');
|
||
expect(todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x', status: 'wip' })).status).toBe('new');
|
||
expect(todoToTriageItem(entry('2026-07-08T02:00:00Z', { text: 'x' })).status).toBe('new');
|
||
});
|
||
});
|
||
|
||
describe('inboxToTriageItem — Telegram inbox 契約', () => {
|
||
it('{"text","from","status"} 沿用:無 tier 歸 collab+unclassified,source 帶 from', () => {
|
||
const it_ = inboxToTriageItem(entry('2026-07-08T03:00:00Z', { text: '幫我查憑證', from: 'leo', status: 'new' }, 'inbox'));
|
||
expect(it_.text).toBe('幫我查憑證');
|
||
expect(it_.tier).toBe('collab');
|
||
expect(it_.unclassified).toBe(true);
|
||
expect(it_.source).toBe('telegram・leo');
|
||
expect(it_.project).toBe('');
|
||
expect(it_.marker).toBe('');
|
||
expect(it_.origin).toBe('inbox');
|
||
});
|
||
it('無 from → source 仍標 telegram;status done 照收', () => {
|
||
const it_ = inboxToTriageItem(entry('2026-07-08T03:00:00Z', { text: 'x', status: 'done' }, 'inbox'));
|
||
expect(it_.source).toBe('telegram');
|
||
expect(it_.status).toBe('done');
|
||
});
|
||
});
|
||
|
||
describe('buildTriageModel — 二源合一 / 三欄計數 / per-project', () => {
|
||
const todo = [
|
||
entry('2026-07-08T02:00:00Z', { text: 'A', owner_tier: 'ai', project: 'Arcrun', status: 'new' }),
|
||
entry('2026-07-08T01:00:00Z', { text: 'B', owner_tier: 'ai', project: 'Arcrun', status: 'done' }),
|
||
entry('2026-07-07T23:00:00Z', { text: 'C', owner_tier: 'leo', project: 'mira', status: 'new' }),
|
||
entry('2026-07-07T22:00:00Z', { text: 'D', project: '', status: 'new' }), // 未分流+未分項
|
||
];
|
||
const inbox = [
|
||
entry('2026-07-08T04:00:00Z', { text: 'E', from: 'leo', status: 'new' }, 'inbox'),
|
||
entry('2026-07-07T21:00:00Z', { text: 'F', status: 'done' }, 'inbox'),
|
||
];
|
||
|
||
it('合併排序=created_at 新→舊,跨兩源交錯', () => {
|
||
const m = buildTriageModel(todo, inbox);
|
||
expect(m.items.map((i) => i.text)).toEqual(['E', 'A', 'B', 'C', 'D', 'F']);
|
||
expect(m.counts.total).toBe(6);
|
||
});
|
||
it('三欄計數只算未完成:done 不佔欄;未分流(todo 無 tier + inbox 全部)歸 collab', () => {
|
||
const m = buildTriageModel(todo, inbox);
|
||
expect(m.counts.ai).toBe(1); // A(B 是 done)
|
||
expect(m.counts.collab).toBe(2); // D(未分流)+ E(inbox)
|
||
expect(m.counts.leo).toBe(1); // C
|
||
expect(m.counts.unclassified).toBe(2); // D + E
|
||
expect(m.counts.done).toBe(2); // B + F
|
||
});
|
||
it('projects 去重、字典序、不含空字串(未分項由 UI 承接)', () => {
|
||
const m = buildTriageModel(todo, inbox);
|
||
expect(m.projects).toEqual(['Arcrun', 'mira']);
|
||
});
|
||
it('空庫誠實回空,不編資料', () => {
|
||
const m = buildTriageModel([], []);
|
||
expect(m.items).toHaveLength(0);
|
||
expect(m.projects).toEqual([]);
|
||
expect(m.counts).toEqual({ ai: 0, collab: 0, leo: 0, unclassified: 0, done: 0, total: 0 });
|
||
});
|
||
});
|