portal 搜尋濾殘影(#46 治標)+四件套單測

/portal/data/search 拿到 KBDB 回應後 server-side 過濾 metadata_json.status=
'deprecated' 與 content 以「(舊管線產物」開頭的 entries(metadata parse 失敗
視為保留,不誤殺),count 重算。這是 Arcrun#46 上游修好前的 portal 端治標,
上游清完資料後整段(含 filterDeprecatedEntries)可拔。
單測:sanitizeUploadFilename(路徑穿越/副檔名/長度)、filterDeprecatedEntries
(濾殘影/壞 metadata 保留)、mapGraphWorkflowOutput(#57 輸出映射);
/portal/session 補 upload_enabled=false 斷言。另修 portal-ui 一處註解字樣
(誤用產品名,撞零 Mira 斷言)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BH7LdhuCdVUHfHXbM7N8r5
This commit is contained in:
Claude
2026-07-17 06:28:04 +00:00
parent fde1827928
commit 8accdacc3c
3 changed files with 92 additions and 3 deletions
+54 -1
View File
@@ -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 } from '../src/routes/portal-data';
import { entryLibrary, sanitizeUploadFilename, filterDeprecatedEntries, mapGraphWorkflowOutput } from '../src/routes/portal-data';
import type { Bindings } from '../src/types';
const KBDB = 'https://kbdb.test';
@@ -353,5 +353,58 @@ describe('GET /portal/sessionP3 能力欄位)', () => {
const data = (await res.json()) as Record<string, unknown>;
expect(data.graph_allowed).toBe(true);
expect(data.workflows_visible).toBe(true);
// portal-demo-suite:測試環境未設 upload bindings → upload_enabled=falseMira 零影響預設)
expect(data.upload_enabled).toBe(false);
});
});
// ═══════════════ 7. portal-demo-suite 純函式 ═══════════════
describe('sanitizeUploadFilename(上傳檔名驗證)', () => {
it('去路徑分隔(擋穿越)、.txt 改 .md、無副檔名補 .md', () => {
expect(sanitizeUploadFilename('notes.md')).toBe('notes.md');
expect(sanitizeUploadFilename('memo.txt')).toBe('memo.md');
expect(sanitizeUploadFilename('README')).toBe('README.md');
expect(sanitizeUploadFilename('../../etc/passwd')).toBe('passwd.md'); // 只取最後一段,穿越失效
expect(sanitizeUploadFilename('a\\b\\c.md')).toBe('c.md');
expect(sanitizeUploadFilename('中文筆記.txt')).toBe('中文筆記.md');
});
it('空名/純路徑/隱藏檔/超過 100 字/非字串 → null', () => {
expect(sanitizeUploadFilename('')).toBe(null);
expect(sanitizeUploadFilename(' ')).toBe(null);
expect(sanitizeUploadFilename('docs/')).toBe(null);
expect(sanitizeUploadFilename('.env')).toBe(null);
expect(sanitizeUploadFilename('a'.repeat(120) + '.md')).toBe(null);
expect(sanitizeUploadFilename(42)).toBe(null);
expect(sanitizeUploadFilename(undefined)).toBe(null);
});
});
describe('filterDeprecatedEntriesArcrun#46 搜尋殘影治標)', () => {
it('濾 status=deprecated 與「(舊管線產物」開頭;metadata parse 失敗保留', () => {
const keepNormal = { metadata_json: '{"library":"general"}', content: '正常內容' };
const keepBadMeta = { metadata_json: 'not-json{{', content: '壞 metadata 不誤殺' };
const keepNullMeta = { metadata_json: null, content: '無 metadata' };
const dropByStatus = { metadata_json: '{"status":"deprecated"}', content: '看起來正常但已標廢' };
const dropByContent = { metadata_json: '{}', content: '(舊管線產物)殘影條目' };
const out = filterDeprecatedEntries([keepNormal, dropByStatus, keepBadMeta, dropByContent, keepNullMeta]);
expect(out).toEqual([keepNormal, keepBadMeta, keepNullMeta]);
});
it('空陣列 → 空陣列', () => {
expect(filterDeprecatedEntries([])).toEqual([]);
});
});
describe('mapGraphWorkflowOutput#57 workflow 輸出 → plugin 形狀)', () => {
it('本體有 neighbors → 直取;count 重算不信自報', () => {
const out = mapGraphWorkflowOutput({ neighbors: ['a', 'b'], edges: [{ subject: 'a', predicate: 'rel', object: 'b' }], count: 99 });
expect(out.neighbors).toEqual(['a', 'b']);
expect(out.edges.length).toBe(1);
expect(out.count).toBe(2);
});
it('包一層 datahttp_request 慣例)→ 取內層;非物件/缺欄位 → 誠實空集合', () => {
expect(mapGraphWorkflowOutput({ data: { neighbors: ['x'], edges: [] } })).toEqual({ neighbors: ['x'], edges: [], count: 1 });
expect(mapGraphWorkflowOutput(null)).toEqual({ neighbors: [], edges: [], count: 0 });
expect(mapGraphWorkflowOutput('oops')).toEqual({ neighbors: [], edges: [], count: 0 });
});
});