fix(portal): 讀不到就說讀不到——總圖不再把「讀不到」畫成「你沒有」(Arcrun#100)
leo 2026-08-12 打開總圖看到:「0 個實體 · 0 條關聯/知識庫還沒有任何關聯—— 上傳文件後 AI 會自動織網」。**而他庫裡有 1854 條三元組。** 那句話會叫他去做一件不需要做的事。 三個獨立的洞疊起來才變成那句謊: ① console-dashboard.ts 打 kbdb-graph-plugin 沒帶認證(同段落打 kbdb 的兩支都有帶) ② 那顆 worker 不在更新的部署清單裡 ⇒ token 一換它就落單 ③ **畫面把 null 畫成 0**——後端已經誠實回 null 了,是前端把它變成謊話 為什麼一直沒被發現:graph plugin 原本身上沒有 token ⇒ 門開著 ⇒ 沒帶也進得去。 2026-08-12 輪替後它有了 token,門關上,401 才浮出來。 📍 repo:matrix/arcrun(cypher-executor/src/routes/、console-ui/public/) 📍 票:Leo/Arcrun#100
This commit is contained in:
@@ -942,3 +942,91 @@ describe('GET /portal/daemon/diagnostics(t213 daemon 版)', () => {
|
||||
expect(JSON.stringify(body.notes)).not.toContain('截圖');
|
||||
});
|
||||
});
|
||||
|
||||
// ═══ Arcrun#100: 總圖的「0」只准在真的是 0 的時候出現 ═══
|
||||
|
||||
describe('GET /portal/data/graph/overview(#100 空圖三態)', () => {
|
||||
/** KBDB `/records/triplet-stats`:帶 owner 與不帶 owner 是兩條不同路徑,分別攔。 */
|
||||
function mockCount(scoped: number | null, global?: number | null) {
|
||||
fetchMock
|
||||
.get(KBDB)
|
||||
.intercept({ path: (p: string) => p.startsWith(`/records/triplet-stats?owner_id=${TENANT}`), method: 'GET' })
|
||||
.reply(scoped === null ? 500 : 200, scoped === null ? { error: 'boom' } : { success: true, stats: [{ library: 'general', triplet_count: scoped }] });
|
||||
if (global !== undefined) {
|
||||
fetchMock
|
||||
.get(KBDB)
|
||||
.intercept({ path: (p: string) => p === '/records/triplet-stats?owner_id=', method: 'GET' })
|
||||
.reply(global === null ? 500 : 200, global === null ? { error: 'boom' } : { success: true, stats: [{ library: 'general', triplet_count: global }] });
|
||||
}
|
||||
}
|
||||
function mockTriplets(body: object, status = 200) {
|
||||
fetchMock
|
||||
.get(KBDB)
|
||||
.intercept({ path: (p: string) => p.startsWith('/records/by-template/triplet'), method: 'GET' })
|
||||
.reply(status, body);
|
||||
}
|
||||
async function overview(token: string) {
|
||||
await seedSession(token, `rec_${token}`);
|
||||
mockGetRecord(`rec_${token}`, userValues({ libraries: '["*"]', role: 'admin' }));
|
||||
return get('/portal/data/graph/overview', { Authorization: `Bearer ${token}` });
|
||||
}
|
||||
|
||||
it('有資料 → 照常回圖,並附上全庫真實條數', async () => {
|
||||
mockTriplets({ success: true, records: [{ values: { subject: 'A', predicate: '連到', object: 'B' } }] });
|
||||
mockCount(1854);
|
||||
const res = await overview('tok-ov1');
|
||||
expect(res.status).toBe(200);
|
||||
const d = (await res.json()) as { node_count: number; triplets_total: number; empty_confirmed: boolean };
|
||||
expect(d.node_count).toBe(2);
|
||||
expect(d.triplets_total).toBe(1854);
|
||||
expect(d.empty_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it('真的空(本租戶 0、全庫也 0)→ empty_confirmed=true,畫面才准印 0', async () => {
|
||||
mockTriplets({ success: true, records: [] });
|
||||
mockCount(0, 0);
|
||||
const res = await overview('tok-ov2');
|
||||
const d = (await res.json()) as { node_count: number; empty_confirmed: boolean; empty_reason: string };
|
||||
expect(d.node_count).toBe(0);
|
||||
expect(d.empty_confirmed).toBe(true);
|
||||
expect(d.empty_reason).toBe('confirmed_empty');
|
||||
});
|
||||
|
||||
it('🔴 反向:本租戶查到 0、全庫卻有 1854(t161 owner_id 對不上)→ 不准說空,回 scope_mismatch', async () => {
|
||||
mockTriplets({ success: true, records: [] });
|
||||
mockCount(0, 1854);
|
||||
const res = await overview('tok-ov3');
|
||||
const d = (await res.json()) as { empty_confirmed: boolean; empty_reason: string };
|
||||
expect(d.empty_confirmed).toBe(false);
|
||||
expect(d.empty_reason).toBe('scope_mismatch');
|
||||
});
|
||||
|
||||
it('🔴 反向:條數讀不到 → unreadable(不是 confirmed_empty,畫面顯示「讀不到」)', async () => {
|
||||
mockTriplets({ success: true, records: [] });
|
||||
mockCount(null);
|
||||
const res = await overview('tok-ov4');
|
||||
const d = (await res.json()) as { empty_confirmed: boolean; empty_reason: string; triplets_total: number | null };
|
||||
expect(d.empty_confirmed).toBe(false);
|
||||
expect(d.empty_reason).toBe('unreadable');
|
||||
expect(d.triplets_total).toBeNull();
|
||||
});
|
||||
|
||||
it('🔴 反向:有條數卻一條邊都抽不出來 → scope_mismatch,不是空庫', async () => {
|
||||
mockTriplets({ success: true, records: [{ values: { subject: '', object: '' } }] });
|
||||
mockCount(1854);
|
||||
const res = await overview('tok-ov5');
|
||||
const d = (await res.json()) as { node_count: number; empty_confirmed: boolean; empty_reason: string };
|
||||
expect(d.node_count).toBe(0);
|
||||
expect(d.empty_reason).toBe('scope_mismatch');
|
||||
expect(d.empty_confirmed).toBe(false);
|
||||
});
|
||||
|
||||
it('🔴 反向:KBDB 回應形狀不對(沒有 records 陣列)→ 502,不再回一張空圖', async () => {
|
||||
mockTriplets({ success: true, items: [] }); // 欄位名不對=讀不出來
|
||||
mockCount(1854);
|
||||
const res = await overview('tok-ov6');
|
||||
expect(res.status).toBe(502);
|
||||
const d = (await res.json()) as { error: string };
|
||||
expect(d.error).toContain('三元組讀取失敗');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user