fix(t130 🔴🔴): PORTAL_TEMPLATE_SEEDS 補 triplet——新實例總圖不再永遠空

真兇(總管探針定罪):seeds 只有 portal_user/portal_library,寫三元組回 400
'template not found: triplet' ⇒ 每個新用戶(含封測者)總圖必空。
geek6688 有是舊實例早期流程建過=拿它驗會假綠(已記 mistakes)。
呼叫路徑已驗:daemon/libraries、admin/libraries、init/seed 皆會 ensurePortalTemplates(冪等)。
vitest 24/24 綠(總管親跑)。(實作=子 CC;驗證+commit=總管)
This commit is contained in:
uncle6me-web
2026-07-29 14:35:36 +08:00
parent ccb86481ae
commit 6d4980d3d7
4 changed files with 84 additions and 2 deletions
+17
View File
@@ -37,4 +37,21 @@ export const PORTAL_TEMPLATE_SEEDS: PortalTemplateSeed[] = [
slots: ['name', 'display_name', 'description', 'status', 'graph_source'],
created_by: 'system',
},
{
// t130rag_ingest_card.post_triplet 寫 POST /records {template:'triplet'}。
// 新實例若無此 template 回 400「template not found: triplet」→ 三元組全滅。
// slots 來源:kbdb_list_templates 核實(2026-07-19library-map.test.ts PROD_TRIPLET_SLOTS
// + librarylibrary-map.ts M1 預案:recompute 歸庫用,ensurePortalTemplates 若缺則 PATCH 補入)。
name: 'triplet',
description: 'KBDB 知識圖譜三元組(kbdb-graph-plugin 寫入;portal 讀此 template 建鄰接圖)',
slots: [
'subject', 'predicate', 'object',
'source_block_id', 'confidence', 'clusters_json',
'bridge_score', 'subject_entity_type', 'object_entity_type',
'status', 'superseded_by',
'source_uri', 'content_hash', 'source_anchor', 'predicate_embed',
'library',
],
created_by: 'system',
},
];
+1 -1
View File
@@ -66,7 +66,7 @@ function mockListByTemplate(template: string, records: { record_id: string; valu
}
function mockTemplatesExist() {
for (const name of ['portal_user', 'portal_library']) {
for (const name of ['portal_user', 'portal_library', 'triplet']) {
fetchMock
.get(KBDB)
.intercept({ path: `/templates/${name}`, method: 'GET' })
+51 -1
View File
@@ -19,6 +19,7 @@
import { SELF, env, fetchMock } from 'cloudflare:test';
import { beforeAll, beforeEach, afterEach, describe, it, expect } from 'vitest';
import { hashPassword, verifyPassword, PBKDF2_ITERATIONS } from '../src/lib/portal-auth';
import { PORTAL_TEMPLATE_SEEDS } from '../src/lib/portal-seeds';
const KBDB = 'https://kbdb.test';
const NS = 'leo::portal'; // wrangler.test.toml CONSOLE_TENANT=leo → 子 namespace
@@ -73,7 +74,7 @@ function mockListByTemplate(template: string, records: { record_id: string; valu
}
function mockTemplatesExist() {
for (const name of ['portal_user', 'portal_library']) {
for (const name of ['portal_user', 'portal_library', 'triplet']) {
fetchMock
.get(KBDB)
.intercept({ path: `/templates/${name}`, method: 'GET' })
@@ -414,3 +415,52 @@ describe('admin 端點 role 閘', () => {
expect(res.status).toBe(404);
});
});
// ═══════════════ t130 — triplet template seed ═══════════════
describe('t130 — triplet template seedPORTAL_TEMPLATE_SEEDS 補 tripletensurePortalTemplates 冪等)', () => {
it('PORTAL_TEMPLATE_SEEDS 含 triplet 且必要 slots 齊備(pure data', () => {
const seed = PORTAL_TEMPLATE_SEEDS.find((s) => s.name === 'triplet');
expect(seed).toBeDefined();
for (const slot of ['subject', 'predicate', 'object', 'source_uri', 'status', 'library']) {
expect(seed!.slots).toContain(slot);
}
});
it('POST /init/seed — triplet 已存 → existing(冪等,不重建)', async () => {
for (const name of ['portal_user', 'portal_library', 'triplet']) {
fetchMock
.get(KBDB)
.intercept({ path: `/templates/${name}`, method: 'GET' })
.reply(200, { success: true, template: { id: `tpl-${name}`, name } });
}
const res = await SELF.fetch('http://localhost/init/seed', { method: 'POST' });
expect(res.status).toBe(200);
const data = (await res.json()) as { portal_templates: { created: string[]; existing: string[] } };
expect(data.portal_templates.existing).toContain('triplet');
expect(data.portal_templates.created).not.toContain('triplet');
});
it('POST /init/seed — triplet 缺 → 自動補建(新實例首次 seed)', async () => {
for (const name of ['portal_user', 'portal_library']) {
fetchMock
.get(KBDB)
.intercept({ path: `/templates/${name}`, method: 'GET' })
.reply(200, { success: true, template: { id: `tpl-${name}`, name } });
}
fetchMock
.get(KBDB)
.intercept({ path: '/templates/triplet', method: 'GET' })
.reply(404, { success: false, error: 'template not found: triplet' });
fetchMock
.get(KBDB)
.intercept({ path: '/templates', method: 'POST' })
.reply(200, { success: true, template: { id: 'tpl-triplet-new', name: 'triplet' } });
const res = await SELF.fetch('http://localhost/init/seed', { method: 'POST' });
expect(res.status).toBe(200);
const data = (await res.json()) as { portal_templates: { created: string[]; existing: string[] } };
expect(data.portal_templates.created).toContain('triplet');
expect(data.portal_templates.existing).not.toContain('triplet');
});
});
@@ -254,6 +254,21 @@
- rag_chatquestion ✅;namespace/kbdb_base 未核實(rag_chat workflow yaml 在 arcrun-rag,非本 repo
- takedown:不在 portal-data.ts,在 arcrun-rag 端(dashboard/安裝器),超出本 repo 範圍
- [x] **t130 新實例缺 triplet template → 總圖永遠空(2026-07-29,任務層小改)**
真因:`PORTAL_TEMPLATE_SEEDS` 只有 `portal_user``portal_library``rag_ingest_card.post_triplet`
`POST /records {template:'triplet'}` → 新實例回 400「template not found: triplet」→ 三元組全滅。
修:`portal-seeds.ts``triplet` seedslots 來源:2026-07-19 kbdb_list_templates 核實 15 槽 + library)。
呼叫路徑驗證(`ensurePortalTemplates` 全四掛點):
`POST /init/seed`(acr init 觸發)← 主線,新裝必經
`POST /portal/admin/bootstrap`(首次 admin 設定)
`POST /portal/daemon/libraries`daemon 登記資料夾→庫)
`POST /portal/admin/libraries`admin 手動新增庫)
既有壞實例補救:任一掛點重跑即補建(冪等),e.g. 重跑 acr init 或 bootstrap。
執行範圍:`cypher-executor/src/lib/portal-seeds.ts`(新增 triplet seed);
`cypher-executor/tests/portal-auth.test.ts`mockTemplatesExist 補 triplet + t130 3 條新測試);
`cypher-executor/tests/portal-admin.test.ts`mockTemplatesExist 補 triplet)。
測試:純資料驗證(seed 含 triplet + 必要 slots)+冪等驗(已存→existing)+自動補建(404→POST→created)。
- [x] **t129 AI 問答出處重複一整頁——後端按 page_name 去重(2026-07-29,任務層小改)**
診斷:一張卡拆成 3-5 block,每 block 各回一筆 source(同 page_name)→ 前端列一整頁重複。
選後端修(/portal/data/chat)理由:出處去重是資料清潔,跟前端渲染無關;改後端不需動 HTML。