Files
Arcrun/cypher-executor/tests/console-triage-model.test.ts
T
Leo 5cd790c94a feat(console): 收件夾改裝=全部待辦分流台(Arcrun#9,kb-ingest SDD R7 消費端)
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>
2026-07-08 22:44:54 +08:00

135 lines
6.3 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.
/**
* 分流台純函式測試(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 KBDBepoch 秒 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 歸 collabunclassifiedsource 帶 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 仍標 telegramstatus 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); // AB 是 done
expect(m.counts.collab).toBe(2); // D(未分流)+ Einbox
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 });
});
});