fix(t128+t129): 圖搜尋補 template:triplet+AI 問答出處按頁去重
t128 真因(總管實測定罪):t116 只補 kbdb_base,同一行 URL 還吃 {{input.template}}
⇒ /records/by-template/?owner_id=... 查不到;手動補 template 即 count=1
(企業版功能解鎖→控制→授權系統)。**同種病三犯,已記 mistakes。**
t129:一卡切 3-5 block 每段都算一筆命中 ⇒ dedupeSourcesByPage 後端去重+hit_count。
vitest 52 passed(1 紅=console HTML 搬遷陳舊測試,非本案)。
(實作=子 CC;驗證+commit=總管)
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
import { SELF, env, fetchMock } from 'cloudflare:test';
|
||||
import { beforeAll, afterEach, describe, it, expect } from 'vitest';
|
||||
import { workflowsVisible } from '../src/routes/portal';
|
||||
import { entryLibrary, sanitizeUploadFilename, filterDeprecatedEntries, mapGraphWorkflowOutput, normalizeCjkQuery, findBestNodeMatch } from '../src/routes/portal-data';
|
||||
import { entryLibrary, sanitizeUploadFilename, filterDeprecatedEntries, mapGraphWorkflowOutput, normalizeCjkQuery, findBestNodeMatch, dedupeSourcesByPage } from '../src/routes/portal-data';
|
||||
import type { Bindings } from '../src/types';
|
||||
|
||||
const KBDB = 'https://kbdb.test';
|
||||
@@ -578,3 +578,134 @@ describe('GET /portal/data/graph/neighbors/:name(t96 fuzzy fallback)', () =>
|
||||
expect(data.neighbors.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 12. t116: graph_neighbors workflow 補傳 kbdb_base ═══════════════
|
||||
|
||||
describe('GET /portal/data/graph/neighbors/:name(t116 kbdb_base 補傳)', () => {
|
||||
it('tenant 有 graph_neighbors workflow → portal 傳入 kbdb_base,workflow 正常執行不崩', async () => {
|
||||
// 設定 session(["*"] 全庫,放行 graph 粗閘)
|
||||
await seedSession('tok-t116', 'rec_t116');
|
||||
mockGetRecord('rec_t116', userValues({ libraries: '["*"]', role: 'admin' }));
|
||||
|
||||
// 在 WEBHOOKS KV 放 graph_neighbors workflow(Input→Output 直通)
|
||||
// 這個 workflow 不用 {{input.kbdb_base}},只驗工作流路徑正常執行(不走 graphBase fallback)
|
||||
// 若沒補傳 kbdb_base 但 workflow 內有 {{input.kbdb_base}} 的節點,URL 解析失敗 → executeWebhookGraph 回 error
|
||||
// 此測試退而求其次:用無外部依賴的直通圖確認整個路徑都通(workflow 取代 plugin fallback)
|
||||
const wfKey = `${TENANT}:wf:graph_neighbors`;
|
||||
await env.WEBHOOKS.put(wfKey, JSON.stringify({
|
||||
graph: {
|
||||
id: 'gn-t116',
|
||||
name: 'graph_neighbors',
|
||||
nodes: [
|
||||
{ id: 'input', type: 'Input' },
|
||||
// comp_passthrough 是內建零件,不需外部 fetch,直接回傳 context
|
||||
{ id: 'pass', type: 'Component', componentId: 'comp_passthrough' },
|
||||
{ id: 'output', type: 'Output' },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'input', to: 'pass', type: 'PIPE' },
|
||||
{ from: 'pass', to: 'output', type: 'PIPE' },
|
||||
],
|
||||
},
|
||||
description: 't116 test',
|
||||
created_at: '2026-07-29T00:00:00.000Z',
|
||||
}));
|
||||
|
||||
const res = await get('/portal/data/graph/neighbors/AI%E5%8D%94%E4%BD%9C', { Authorization: 'Bearer tok-t116' });
|
||||
expect(res.status).toBe(200);
|
||||
const data = (await res.json()) as { neighbors: unknown[]; edges: unknown[]; count: number; kbdb_base?: string };
|
||||
// workflow 走 comp_passthrough,output = 整個 context(含 kbdb_base)
|
||||
// mapGraphWorkflowOutput 只取 neighbors/edges,其他欄位不影響回應
|
||||
expect(Array.isArray(data.neighbors)).toBe(true);
|
||||
expect(Array.isArray(data.edges)).toBe(true);
|
||||
// 確認不是 502(graph_neighbors workflow 執行失敗)
|
||||
expect(res.status).not.toBe(502);
|
||||
|
||||
await env.WEBHOOKS.delete(wfKey);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 13. t128: graph_neighbors workflow 補傳 template ═══════════════
|
||||
|
||||
describe('GET /portal/data/graph/neighbors/:name(t128 template 補傳)', () => {
|
||||
it('tenant 有 graph_neighbors workflow → portal 傳入 template=triplet,workflow 不崩', async () => {
|
||||
await seedSession('tok-t128', 'rec_t128');
|
||||
mockGetRecord('rec_t128', userValues({ libraries: '["*"]', role: 'admin' }));
|
||||
|
||||
const wfKey = `${TENANT}:wf:graph_neighbors`;
|
||||
await env.WEBHOOKS.put(wfKey, JSON.stringify({
|
||||
graph: {
|
||||
id: 'gn-t128',
|
||||
name: 'graph_neighbors',
|
||||
nodes: [
|
||||
{ id: 'input', type: 'Input' },
|
||||
{ id: 'pass', type: 'Component', componentId: 'comp_passthrough' },
|
||||
{ id: 'output', type: 'Output' },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'input', to: 'pass', type: 'PIPE' },
|
||||
{ from: 'pass', to: 'output', type: 'PIPE' },
|
||||
],
|
||||
},
|
||||
}));
|
||||
|
||||
const res = await get('/portal/data/graph/neighbors/AI%E5%8D%94%E4%BD%9C', { Authorization: 'Bearer tok-t128' });
|
||||
// template 有進 context → workflow 執行不崩(非 502)
|
||||
expect(res.status).toBe(200);
|
||||
const data = (await res.json()) as { neighbors: unknown[]; edges: unknown[] };
|
||||
expect(Array.isArray(data.neighbors)).toBe(true);
|
||||
|
||||
await env.WEBHOOKS.delete(wfKey);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 14. t129: dedupeSourcesByPage 純函式 ═══════════════
|
||||
|
||||
describe('dedupeSourcesByPage(t129 出處去重)', () => {
|
||||
it('同 page_name 合併,hit_count 標計數', () => {
|
||||
const srcs = [
|
||||
{ page_name: '企業版功能', mode: 'semantic', source: 'gitea://docs/enterprise.md' },
|
||||
{ page_name: '企業版功能', mode: 'semantic', source: 'gitea://docs/enterprise.md' },
|
||||
{ page_name: '企業版功能', mode: 'keyword', source: 'gitea://docs/enterprise.md' },
|
||||
];
|
||||
const out = dedupeSourcesByPage(srcs) as { page_name: string; hit_count?: number }[];
|
||||
expect(out.length).toBe(1); // 3 筆→1 筆
|
||||
expect(out[0].page_name).toBe('企業版功能');
|
||||
expect(out[0].hit_count).toBe(3);
|
||||
});
|
||||
|
||||
it('不同 page_name 各保留一筆;單筆無 hit_count', () => {
|
||||
const srcs = [
|
||||
{ page_name: 'A 頁', mode: 'semantic' },
|
||||
{ page_name: 'B 頁', mode: 'keyword' },
|
||||
];
|
||||
const out = dedupeSourcesByPage(srcs) as { page_name: string; hit_count?: number }[];
|
||||
expect(out.length).toBe(2);
|
||||
expect(out.every(s => s.hit_count === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('page 欄(備用)也能去重', () => {
|
||||
const srcs = [
|
||||
{ page: '備用頁', mode: 'semantic' },
|
||||
{ page: '備用頁', mode: 'keyword' },
|
||||
];
|
||||
const out = dedupeSourcesByPage(srcs) as { page?: string; hit_count?: number }[];
|
||||
expect(out.length).toBe(1);
|
||||
expect(out[0].hit_count).toBe(2);
|
||||
});
|
||||
|
||||
it('空陣列 → 空陣列;非物件條目跳過', () => {
|
||||
expect(dedupeSourcesByPage([])).toEqual([]);
|
||||
const out = dedupeSourcesByPage([null, 'oops', { page_name: 'X' }]);
|
||||
expect(out.length).toBe(1);
|
||||
});
|
||||
|
||||
it('page_name 優先於 page', () => {
|
||||
const srcs = [
|
||||
{ page_name: '優先頁', page: '備用頁' },
|
||||
{ page_name: '優先頁', page: '備用頁' },
|
||||
];
|
||||
const out = dedupeSourcesByPage(srcs) as { hit_count?: number }[];
|
||||
expect(out.length).toBe(1); // 同 page_name → 合為一筆
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user