Files
Arcrun/cypher-executor/tests/graph-stats-honesty.test.ts
uncle6me-web a5e4caf5cb 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
2026-08-12 13:33:53 +08:00

148 lines
7.5 KiB
TypeScript
Raw Permalink 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#100 — 「畫面上的 0,只准在真的是 0 的時候出現」
*
* 病灶(leo 實遇):總圖頁寫「0 個實體・0 條關聯/知識庫還沒有任何關聯——上傳文件後 AI 會
* 自動織網」,而他庫裡有 1854 條三元組。那句話會叫他去做一件不需要做的事。
*
* 本檔釘住三件事:
* ① kbdb-graph-plugin 的 `/triplets` 前綴掛 Bearer 閘,cypher 打它**一定要帶 token**
* console-dashboard 兩支 stats 原本漏帶 → 永遠 401)。
* ② 三元組數量的真相源=KBDB `/records/triplet-stats`(真 SQL COUNT、依 owner 過濾),
* **不是** plugin `/triplets/stats` 的 `total`——那是分頁長度(KBDB 端上限 100/500),
* 1854 條的庫只會回 100。只修 401 不換來源=把「0」換成「100」,一樣是假的。
* ③ 讀不到一律 null / 502 / empty_confirmed=false**絕不退化成 0**。
*
* KBDBgraph-plugin 都打 fetchMock 假 hostwrangler.test.toml KBDB_BASE_URL=https://kbdb.test、
* KBDB_GRAPH_URL=https://graph.test)+disableNetConnect——絕不外連。
*/
import { SELF, env, fetchMock } from 'cloudflare:test';
import { beforeAll, afterEach, describe, it, expect } from 'vitest';
import { graphHeaders, graphBase } from '../src/routes/kbdb-proxy';
import type { Bindings } from '../src/types';
const KBDB = 'https://kbdb.test';
const GRAPH = 'https://graph.test';
const TENANT = 'leo'; // wrangler.test.toml CONSOLE_TENANT
beforeAll(() => {
fetchMock.activate();
fetchMock.disableNetConnect();
});
afterEach(() => fetchMock.assertNoPendingInterceptors());
/** KBDB `/records/triplet-stats` — 真 COUNT 的形狀:{ success, stats: [{library, triplet_count}] } */
function mockTripletStats(rows: { library: string; triplet_count: number }[] | null, status = 200) {
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(status, rows === null ? { success: false, error: 'boom' } : { success: true, stats: rows });
}
// ═══════════════ 1. graphHeaders:打 plugin 的 header 只有一份 ═══════════════
describe('graphHeaders#100 漂移的根:三處手拼 → 一支函式)', () => {
it('有 KBDB_INTERNAL_TOKEN → 帶 Bearerplugin 的 /triplets /graph /search /entities 全靠它)', () => {
expect(graphHeaders({ KBDB_INTERNAL_TOKEN: 'tok-abc' } as unknown as Bindings)).toEqual({
Authorization: 'Bearer tok-abc',
});
});
it('沒設 token → 空 headersplugin 未設 secret 時本來就開放,不硬塞空 Bearer)', () => {
expect(graphHeaders({} as unknown as Bindings)).toEqual({});
});
it('graphBase 仍照舊(KBDB_GRAPH_URL 優先、去尾斜線)', () => {
expect(graphBase({ KBDB_GRAPH_URL: 'https://graph.test/' } as unknown as Bindings)).toBe('https://graph.test');
});
});
// ═══════════════ 2. /console/kb-scale-data:數字對得上庫裡真正的數量 ═══════════════
describe('GET /console/kb-scale-data — 三元組數=KBDB 真 COUNT', () => {
it('庫裡 1854 條(跨三個庫)→ triplets_total 回 1854,不是 plugin 的分頁長度 100', async () => {
mockTripletStats([
{ library: 'general', triplet_count: 1200 },
{ library: 'finance', triplet_count: 600 },
{ library: 'ops', triplet_count: 54 },
]);
const res = await SELF.fetch('http://localhost/console/kb-scale-data');
expect(res.status).toBe(200);
const d = (await res.json()) as { triplets_total: number | null };
expect(d.triplets_total).toBe(1854);
});
it('反向:triplet-stats 讀不到(500)→ triplets_total = null**不是 0**', async () => {
mockTripletStats(null, 500);
const res = await SELF.fetch('http://localhost/console/kb-scale-data');
expect(res.status).toBe(200);
const d = (await res.json()) as { triplets_total: number | null };
expect(d.triplets_total).toBeNull();
expect(d.triplets_total).not.toBe(0); // 這一行就是 #100 的整個重點
});
it('反向:回應形狀不對(stats 不是陣列)→ null,不半信半疑當 0', async () => {
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(200, { success: true, stats: 'oops' });
const res = await SELF.fetch('http://localhost/console/kb-scale-data');
const d = (await res.json()) as { triplets_total: number | null };
expect(d.triplets_total).toBeNull();
});
it('真的是 0(庫存在但沒有任何三元組)→ 誠實回 0(0 只在這種時候出現)', async () => {
mockTripletStats([]);
const res = await SELF.fetch('http://localhost/console/kb-scale-data');
const d = (await res.json()) as { triplets_total: number | null };
expect(d.triplets_total).toBe(0);
});
it('kb-scale-data 不再打 graph-plugin(沒有 plugin interceptor 也能拿到數字)', async () => {
mockTripletStats([{ library: 'general', triplet_count: 7 }]);
const res = await SELF.fetch('http://localhost/console/kb-scale-data');
const d = (await res.json()) as { triplets_total: number | null };
expect(d.triplets_total).toBe(7); // 打 GRAPH 的話 disableNetConnect 會讓它變 null
});
});
// ═══════════════ 3. /console/dashboard-data:燈號問 plugin、數字問 KBDB ═══════════════
describe('GET /console/dashboard-data — 圖服務健康 vs 三元組數量是兩件事', () => {
it('打 plugin /triplets/stats **有帶 Bearer** → graph.ok=true;數量仍取 KBDB 真 COUNT', async () => {
// headers matcher:漏帶 Authorization 就配不到這個 interceptor → 請求失敗 → graph.ok=false
fetchMock
.get(GRAPH)
.intercept({
path: (p: string) => p.startsWith('/triplets/stats'),
method: 'GET',
headers: { authorization: `Bearer ${env.KBDB_INTERNAL_TOKEN}` },
})
.reply(200, { total: 100 }); // plugin 的分頁長度,故意與真值不同
mockTripletStats([{ library: 'general', triplet_count: 1854 }]);
const res = await SELF.fetch('http://localhost/console/dashboard-data');
expect(res.status).toBe(200);
const d = (await res.json()) as {
system: { graph: { ok: boolean; triplets: number | null } };
kb: { triplets_total: number | null };
};
expect(d.system.graph.ok).toBe(true); // 帶了 token 才會是 true#100 迴歸閘)
expect(d.system.graph.triplets).toBe(1854); // 不是 plugin 的 100
expect(d.kb.triplets_total).toBe(1854);
});
it('反向:plugin 打不通 → graph.ok=false,但三元組數照樣是真的(不被服務狀態吞掉)', async () => {
mockTripletStats([{ library: 'general', triplet_count: 1854 }]);
const res = await SELF.fetch('http://localhost/console/dashboard-data');
const d = (await res.json()) as { system: { graph: { ok: boolean; triplets: number | null } } };
expect(d.system.graph.ok).toBe(false);
expect(d.system.graph.triplets).toBe(1854);
});
it('反向:兩邊都讀不到 → ok=false + triplets=null(不是 0', async () => {
const res = await SELF.fetch('http://localhost/console/dashboard-data');
const d = (await res.json()) as { system: { graph: { ok: boolean; triplets: number | null } } };
expect(d.system.graph.ok).toBe(false);
expect(d.system.graph.triplets).toBeNull();
});
});