a5e4caf5cb
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
148 lines
7.5 KiB
TypeScript
148 lines
7.5 KiB
TypeScript
/**
|
||
* 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**。
|
||
*
|
||
* KBDB/graph-plugin 都打 fetchMock 假 host(wrangler.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 → 帶 Bearer(plugin 的 /triplets /graph /search /entities 全靠它)', () => {
|
||
expect(graphHeaders({ KBDB_INTERNAL_TOKEN: 'tok-abc' } as unknown as Bindings)).toEqual({
|
||
Authorization: 'Bearer tok-abc',
|
||
});
|
||
});
|
||
|
||
it('沒設 token → 空 headers(plugin 未設 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();
|
||
});
|
||
});
|