const assert = require('node:assert/strict') const crypto = require('node:crypto') const fs = require('node:fs') const path = require('node:path') const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') const root = path.resolve(__dirname, '../..') const origin = process.env.VERIFY_FRONTEND_ORIGIN || 'https://agent.pducv.cn' const apiOrigin = process.env.VERIFY_API_ORIGIN || 'http://127.0.0.1:18080' const kbID = '16c3e59c-4bb6-41cf-ae0a-6d0941b3d032' const report = { checked_at: new Date().toISOString(), origin, api_origin: apiOrigin, checks: [], failures: [] } let auth, browser function check(name, value) { report.checks.push({ name, passed: !!value }) assert.ok(value, name) } async function api(route, requestOrigin = apiOrigin) { const response = await fetch(`${requestOrigin}/api/v1${route}`, { headers: { Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059' }, signal: AbortSignal.timeout(30000), }) assert.equal(response.status, 200, `GET ${route}: HTTP ${response.status}`) const result = await response.json() assert.notEqual(result.success, false, `GET ${route} failed`) return result.data } async function main() { const credentials = JSON.parse(fs.readFileSync(path.join(root, 'acceptance/super-admin-login.json'), 'utf8')) const login = await fetch(`${apiOrigin}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), signal: AbortSignal.timeout(30000), }) assert.equal(login.status, 200, 'authentication failed') const loginResult = await login.json() auth = loginResult.data || loginResult const kbs = await api('/knowledge-bases') report.knowledge_bases = [] for (const kb of kbs) { const config = await api(`/initialization/config/${kb.id}`) const initialized = !!(kb.summary_model_id && kb.embedding_model_id && config.llm?.modelName && config.embedding?.modelName) report.knowledge_bases.push({ id: kb.id, name: kb.name, initialized, graph_enabled: !!kb.extract_config?.enabled, llm_model: config.llm?.modelName, embedding_model: config.embedding?.modelName }) check(`models_resolve:${kb.name}`, initialized) } const overview = await api(`/knowledge-bases/${kbID}/evidence-graph`) report.graph = { counts: overview.counts, sources: overview.sources, mapped_sources: overview.mapped_sources } check('product_has_extracted_entities_and_relations', overview.counts.knowledgeentity > 2 && overview.counts.knowledgerelation > 0) const nodes = [...overview.nodes] let cursor = overview.next_cursor for (let pages = 0; cursor && pages < 50; pages++) { const next = await api(`/knowledge-bases/${kbID}/evidence-graph?cursor=${cursor}`) nodes.push(...next.nodes) assert.notEqual(next.next_cursor, cursor, 'graph cursor did not advance') cursor = next.next_cursor } assert.equal(cursor, 0, 'graph exceeded verification pagination bound') check('all_entity_ids_are_exact_in_browser', nodes.every(node => Number.isSafeInteger(node.id))) const nativeNodes = nodes.filter(node => node.payload.origin === 'native_extraction_v1') check('native_extraction_is_visible', nativeNodes.length > 0) check('example_entities_are_absent', !nativeNodes.some(node => ['基础概念', '实践练习'].includes(node.payload.canonical_name))) const chunks = new Map(), documents = new Map(), relations = new Map(), evidenceIDs = new Set() let selected for (const node of nativeNodes) { const detail = await api(`/knowledge-bases/${kbID}/evidence-graph?entity=${node.id}`) assert.equal(detail.nodes[0]?.id, node.id, 'entity detail must resolve exact ID') const nativeEvidence = detail.evidence.filter(item => item.payload.origin === 'native_extraction_v1') assert.ok(nativeEvidence.length, `native entity ${node.id} has no evidence`) if (!selected && detail.relations.length) selected = { node, evidence: nativeEvidence[0] } for (const relation of detail.relations) relations.set(relation.id, relation) for (const evidence of nativeEvidence) { assert.equal(evidence.mapping_state, 'native_extraction', 'evidence no longer maps to its original chunk') assert.ok(evidence.native_chunk_id && evidence.native_knowledge_id, 'evidence source link is missing') if (!chunks.has(evidence.native_chunk_id)) chunks.set(evidence.native_chunk_id, await api(`/chunks/by-id/${evidence.native_chunk_id}`)) if (!documents.has(evidence.native_knowledge_id)) documents.set(evidence.native_knowledge_id, await api(`/knowledge/${evidence.native_knowledge_id}`)) const chunk = chunks.get(evidence.native_chunk_id), document = documents.get(evidence.native_knowledge_id) assert.equal(chunk.knowledge_base_id, kbID, 'chunk belongs to another knowledge base') assert.equal(chunk.knowledge_id, evidence.native_knowledge_id, 'chunk does not belong to the source document') assert.equal(document.knowledge_base_id, kbID, 'source document belongs to another knowledge base') assert.equal(crypto.createHash('sha256').update(chunk.content).digest('hex'), evidence.payload.source_content_hash, 'evidence content hash differs from actual chunk') assert.ok(evidence.payload.excerpt && chunk.content.includes(evidence.payload.excerpt), 'evidence excerpt is not from the original chunk') evidenceIDs.add(evidence.id) } } check('native_evidence_resolves_to_unchanged_source', evidenceIDs.size > 0) check('example_relation_is_absent', [...relations.values()].every(relation => !(relation.payload.from_name === '基础概念' && relation.payload.to_name === '实践练习'))) const nativeRelations = [...relations.values()].filter(relation => relation.payload.origin === 'native_extraction_v1') for (const relation of nativeRelations) { assert.ok(typeof relation.payload.evidence === 'string' && relation.payload.evidence.trim(), `native relation ${relation.id} has no source quotation`) assert.ok([...chunks.values()].some(chunk => chunk.content.includes(relation.payload.evidence)), `native relation ${relation.id} quotation is absent from verified source chunks`) } check('native_relations_have_verbatim_source_quotations', nativeRelations.length > 0) report.verified_native = { entities: nativeNodes.length, entity_evidence: evidenceIDs.size, relations: nativeRelations.length, quoted_relations: nativeRelations.length, chunks: chunks.size, documents: documents.size } report.sample_relations = [...relations.values()].slice(0, 5).map(relation => ({ from: relation.payload.from_name, relation: relation.payload.relation_type, to: relation.payload.to_name, })) const publicOverview = await api(`/knowledge-bases/${kbID}/evidence-graph`, origin) check('public_graph_api_exposes_completed_extractions', publicOverview.counts.knowledgeentity >= overview.counts.knowledgeentity) const denied = await fetch(`${origin}/api/v1/knowledge-bases/${kbID}/evidence-graph`, { signal: AbortSignal.timeout(30000) }) check('public_graph_api_requires_auth', denied.status === 401) browser = await chromium.launch({ headless: true, channel: 'chrome' }) const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) await context.addInitScript(value => { const local = { weknora_user: JSON.stringify({ ...value.user, tenant_id: String(value.user.tenant_id) }), weknora_tenant: JSON.stringify({ ...value.active_tenant, id: '10059' }), weknora_token: value.token, weknora_refresh_token: value.refresh_token, weknora_memberships: JSON.stringify(value.memberships), weknora_selected_tenant_id: '10059' } for (const [key, item] of Object.entries(local)) localStorage.setItem(key, item) localStorage.setItem('weknora:new-user-guide-done:v1', '1') }, auth) const page = await context.newPage(), errors = [] page.on('pageerror', error => errors.push(error.message)) page.on('response', response => { const pathname = new URL(response.url()).pathname if (response.status() >= 400 && /\/(?:api|assets)\//.test(pathname)) errors.push(`${response.status()} ${pathname}`) }) await page.goto(`${origin}/platform/knowledge-bases/${kbID}?tab=evidence`) await page.locator('.entity-row').first().waitFor({ timeout: 30000 }) assert.ok(selected, 'missing native entity with relation for browser verification') await page.getByRole('textbox', { name: '搜索实体' }).fill(selected.node.payload.canonical_name) await page.getByRole('button', { name: '搜索', exact: true }).click() await page.locator('.entity-row').filter({ hasText: `#${selected.node.id}` }).click() await page.locator('.evidence-row').first().waitFor({ timeout: 30000 }) check('native_entity_relations_visible', await page.locator('.relation-list li').count() > 0) check('source_evidence_marked_verified', await page.getByText('已核验原生分块', { exact: true }).count() > 0) await page.screenshot({ path: path.join(root, 'acceptance/business-graph-live-desktop.png'), animations: 'disabled' }) await page.getByRole('button', { name: '查看原生文档', exact: true }).first().click() await page.locator('.t-drawer--open').first().waitFor({ timeout: 30000 }) const originalDocument = documents.get(selected.evidence.native_knowledge_id) const drawer = page.locator('.t-drawer--open').first() const rawTitle = originalDocument.file_name || originalDocument.title || originalDocument.source const extensionIndex = rawTitle.lastIndexOf('.') const displayTitle = originalDocument.type === 'file' && extensionIndex > 0 ? rawTitle.slice(0, extensionIndex) : rawTitle await page.waitForFunction(title => [...document.querySelectorAll('.t-drawer--open .doc-drawer-header-title')] .some(element => element.textContent.trim() === title), displayTitle, { timeout: 30000 }) check('source_document_opens_in_drawer', (await drawer.locator('.doc-drawer-header-title').innerText()).trim() === displayTitle) await page.screenshot({ path: path.join(root, 'acceptance/business-graph-live-source.png'), animations: 'disabled' }) await page.keyboard.press('Escape') await page.setViewportSize({ width: 390, height: 844 }) await page.screenshot({ path: path.join(root, 'acceptance/business-graph-live-mobile.png'), animations: 'disabled' }) check('mobile_graph_has_no_overflow', await page.locator('.evidence-graph').evaluate(element => element.scrollWidth <= element.clientWidth + 2)) check('mobile_page_has_no_overflow', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)) await page.goto(`${origin}/platform/knowledge-bases`) await page.waitForLoadState('networkidle', { timeout: 30000 }) check('knowledge_base_list_has_no_initialization_warning', await page.locator('.warning-banner').count() === 0 && await page.locator('.uninitialized').count() === 0) report.browser_errors = errors check('browser_has_no_api_or_runtime_errors', errors.length === 0) } main().catch(error => { report.failures.push(error.message); process.exitCode = 1 }).finally(async () => { if (browser) await browser.close() report.passed = report.failures.length === 0 fs.writeFileSync(path.join(root, 'acceptance/business-graph-live.json'), JSON.stringify(report, null, 2) + '\n') console.log(JSON.stringify(report)) })