WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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))
|
||||
})
|
||||
Reference in New Issue
Block a user