diff --git a/cypher-executor/src/lib/portal-seeds.ts b/cypher-executor/src/lib/portal-seeds.ts index 003c922..3e151e0 100644 --- a/cypher-executor/src/lib/portal-seeds.ts +++ b/cypher-executor/src/lib/portal-seeds.ts @@ -37,4 +37,21 @@ export const PORTAL_TEMPLATE_SEEDS: PortalTemplateSeed[] = [ slots: ['name', 'display_name', 'description', 'status', 'graph_source'], created_by: 'system', }, + { + // t130:rag_ingest_card.post_triplet 寫 POST /records {template:'triplet'}。 + // 新實例若無此 template 回 400「template not found: triplet」→ 三元組全滅。 + // slots 來源:kbdb_list_templates 核實(2026-07-19,library-map.test.ts PROD_TRIPLET_SLOTS) + // + library(library-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', + }, ]; diff --git a/cypher-executor/tests/portal-admin.test.ts b/cypher-executor/tests/portal-admin.test.ts index 52771f7..4476146 100644 --- a/cypher-executor/tests/portal-admin.test.ts +++ b/cypher-executor/tests/portal-admin.test.ts @@ -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' }) diff --git a/cypher-executor/tests/portal-auth.test.ts b/cypher-executor/tests/portal-auth.test.ts index d4f26cb..ace19e1 100644 --- a/cypher-executor/tests/portal-auth.test.ts +++ b/cypher-executor/tests/portal-auth.test.ts @@ -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 seed(PORTAL_TEMPLATE_SEEDS 補 triplet,ensurePortalTemplates 冪等)', () => { + 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'); + }); +}); diff --git a/system-dev/docs/3-specs/portal-auth/tasks.md b/system-dev/docs/3-specs/portal-auth/tasks.md index 2407c69..dd043b2 100644 --- a/system-dev/docs/3-specs/portal-auth/tasks.md +++ b/system-dev/docs/3-specs/portal-auth/tasks.md @@ -254,6 +254,21 @@ - rag_chat:question ✅;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` seed(slots 來源: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。