c662af7e2a
新 ingest 鏈(rag_ingest_card/rag_ingest_direct)寫的 metadata source 是
kb://<path>,但 portal 溯源只認 gitea://——B4 部好溯源後一經重灌即再斷鏈。
- gitea://<path>(舊 rag_ingest v2):{base}/<path>,行為一字不變(不回歸)
- gitea:<org/repo>@<path>(km-wiki-ingest 實形):
{base}/<org/repo>/src/branch/main/<path>,#segNN 錨點捨棄
- kb://<path>(新 ingest 鏈實形):雲端沒有對應網頁,不造死鏈——
卡片詳頁顯示本地路徑文字+「在你的同步資料夾內」標註(沿用既有
sourceWebBase 未設時的純文字降級樣式);AI 出處列同標註
- 測試:console-ui/tests/portal-source-href.test.mjs(node --test,
抽真身 srcHref/srcLocalPath 求值,6 case 全綠);package.json 加 test script
- public/portal/index.html 用 ARCRUN_API_BASE=https://cypher.arcrun.dev
重 build(與既有產物同參數,diff 僅本次 scheme 改動)
實形出處:kb:// = arcrun-rag workflows/rag-ingest.yaml:115('kb://'+path);
gitea:@ = registry/examples/km-wiki-ingest/workflow.yaml:276。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
5.2 KiB
JavaScript
109 lines
5.2 KiB
JavaScript
/**
|
||
* Portal 溯源 scheme 測試(daemon-beta t21 — kb:// vs gitea:// 斷鏈地雷)
|
||
*
|
||
* 驗的是 src/portal-ui.ts 內嵌客戶端 JS 的 srcHref / srcLocalPath:
|
||
* gitea://<path> → {base}/<path>(舊 rag_ingest v2 實形,行為不回歸)
|
||
* gitea:<org/repo>@<path> → {base}/<org/repo>/src/branch/main/<path>(km-wiki-ingest 實形,
|
||
* 見 registry/examples/km-wiki-ingest/workflow.yaml `gitea:${repo}@${relPath}`)
|
||
* kb://<path> → 不造連結(雲端無對應網頁),srcLocalPath 給本地路徑文字,
|
||
* UI 標「在你的同步資料夾內」(arcrun-rag rag-ingest.yaml `'kb://' + path`)
|
||
*
|
||
* 測法:從 portal-ui.ts 的 template literal 抽出三個函式原始碼(brace 平衡掃描),
|
||
* 還原 template 跳脫(`\\` → `\`)後用 new Function 求值——測的就是真正會被
|
||
* build 進 public/portal/index.html 的那份 JS,零外部依賴(console-ui 哲學)。
|
||
*/
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const SRC = readFileSync(join(HERE, '..', 'src', 'portal-ui.ts'), 'utf8');
|
||
|
||
/** 從原始碼抽出 `function <name>(...) { ... }` 全文(brace 平衡;函式體無字串內大括號)。 */
|
||
function extractFn(name) {
|
||
const start = SRC.indexOf(`function ${name}(`);
|
||
assert.ok(start >= 0, `portal-ui.ts 找不到 function ${name}`);
|
||
const braceStart = SRC.indexOf('{', SRC.indexOf(')', start));
|
||
let i = braceStart + 1;
|
||
let depth = 1;
|
||
while (i < SRC.length && depth > 0) {
|
||
if (SRC[i] === '{') depth++;
|
||
else if (SRC[i] === '}') depth--;
|
||
i++;
|
||
}
|
||
// template literal 內的 `\\` 在產出 HTML 時是 `\`——還原成瀏覽器實際跑的 JS
|
||
return SRC.slice(start, i).replace(/\\\\/g, '\\');
|
||
}
|
||
|
||
/** 用指定 SOURCE_WEB_BASE 建出真正的 srcHref / srcLocalPath。 */
|
||
function makeFns(base) {
|
||
const code = [extractFn('encSrcPath'), extractFn('srcHref'), extractFn('srcLocalPath')].join('\n');
|
||
return new Function('SOURCE_WEB_BASE', `${code}\nreturn { srcHref, srcLocalPath };`)(base);
|
||
}
|
||
|
||
const BASE = 'https://gitea.example.com';
|
||
|
||
test('gitea://<path>(舊 rag_ingest v2)— 現行為不回歸:base + path、#n 錨點捨棄', () => {
|
||
const { srcHref } = makeFns(BASE + '/'); // 尾斜線要被吃掉
|
||
assert.equal(srcHref('gitea://system-dev/wiki/foo.md'), `${BASE}/system-dev/wiki/foo.md`);
|
||
assert.equal(srcHref('gitea://system-dev/wiki/foo.md#3'), `${BASE}/system-dev/wiki/foo.md`);
|
||
// 中文路徑逐段 encode
|
||
assert.equal(srcHref('gitea://docs/員工手冊.md'), `${BASE}/docs/${encodeURIComponent('員工手冊.md')}`);
|
||
});
|
||
|
||
test('gitea:<org/repo>@<path>(km-wiki-ingest)— base/<org/repo>/src/branch/main/<path>', () => {
|
||
const { srcHref } = makeFns(BASE);
|
||
assert.equal(
|
||
srcHref('gitea:Leo/notes@system-dev/wiki/cards/foo.md'),
|
||
`${BASE}/Leo/notes/src/branch/main/system-dev/wiki/cards/foo.md`
|
||
);
|
||
// 分段錨點(#segNN)捨棄
|
||
assert.equal(
|
||
srcHref('gitea:Leo/notes@wiki/foo.md#seg02'),
|
||
`${BASE}/Leo/notes/src/branch/main/wiki/foo.md`
|
||
);
|
||
// 中文路徑逐段 encode
|
||
assert.equal(
|
||
srcHref('gitea:Leo/notes@docs/員工手冊.md'),
|
||
`${BASE}/Leo/notes/src/branch/main/docs/${encodeURIComponent('員工手冊.md')}`
|
||
);
|
||
// 缺 @ 或畸形 → 不造連結
|
||
assert.equal(srcHref('gitea:Leo/notes'), '');
|
||
assert.equal(srcHref('gitea:@wiki/foo.md'), '');
|
||
assert.equal(srcHref('gitea:Leo/notes@'), '');
|
||
});
|
||
|
||
test('kb://<path>(新 ingest 鏈)— 不造死鏈、srcLocalPath 給本地路徑', () => {
|
||
const { srcHref, srcLocalPath } = makeFns(BASE);
|
||
assert.equal(srcHref('kb://人事/員工特休假規定.md'), '');
|
||
assert.equal(srcLocalPath('kb://人事/員工特休假規定.md'), '人事/員工特休假規定.md');
|
||
assert.equal(srcLocalPath('kb://foo.md#seg01'), 'foo.md');
|
||
// 非 kb:// 不給本地路徑
|
||
assert.equal(srcLocalPath('gitea://foo.md'), '');
|
||
assert.equal(srcLocalPath('gitea:Leo/notes@foo.md'), '');
|
||
assert.equal(srcLocalPath('https://example.com/x'), '');
|
||
});
|
||
|
||
test('SOURCE_WEB_BASE 未設 — 全部 scheme 都不給 href(降級純文字),kb:// 顯示不受影響', () => {
|
||
const { srcHref, srcLocalPath } = makeFns('');
|
||
assert.equal(srcHref('gitea://wiki/foo.md'), '');
|
||
assert.equal(srcHref('gitea:Leo/notes@wiki/foo.md'), '');
|
||
assert.equal(srcHref('kb://foo.md'), '');
|
||
assert.equal(srcLocalPath('kb://foo.md'), 'foo.md'); // 本地路徑與 base 無關
|
||
});
|
||
|
||
test('未知 scheme — 誠實維持純文字(不猜連結)', () => {
|
||
const { srcHref, srcLocalPath } = makeFns(BASE);
|
||
assert.equal(srcHref('https://example.com/foo'), '');
|
||
assert.equal(srcHref('random-string'), '');
|
||
assert.equal(srcLocalPath('random-string'), '');
|
||
});
|
||
|
||
test('UI 有 kb:// 的誠實標註(兩個顯示點:卡片詳頁 srcline + AI 出處列)', () => {
|
||
const hits = SRC.split('在你的同步資料夾內').length - 1;
|
||
assert.ok(hits >= 2, `「在你的同步資料夾內」標註應出現在 ≥2 個顯示點,實得 ${hits}`);
|
||
assert.ok(SRC.includes('srcLocalPath(src)'), '卡片詳頁應走 srcLocalPath 降級');
|
||
});
|