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,137 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const http = require('node:http')
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const dependencies = '/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules'
|
||||
const { chromium } = require(path.join(dependencies, 'playwright'))
|
||||
const { Document, Packer, Paragraph } = require(path.join(dependencies, 'docx'))
|
||||
const { PDFDocument, StandardFonts } = require(path.join(dependencies, 'pdf-lib'))
|
||||
|
||||
const directory = path.resolve(__dirname, '../../acceptance')
|
||||
const base = 'http://127.0.0.1:18080/api/v1'
|
||||
const database = 'yawan_native_acceptance_20260915'
|
||||
const sql = statement => execFileSync('docker', ['--context', 'colima-yawan', 'exec', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', database, '-v', 'ON_ERROR_STOP=1', '-Atc', statement], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
|
||||
const literal = value => "'" + String(value).replaceAll("'", "''") + "'"
|
||||
const checks = [], errors = [], modelCalls = []
|
||||
let auth, model, human, channel, browser, page, stage = 'login'
|
||||
const check = (name, passed) => { checks.push({ name, passed: !!passed }); assert.ok(passed, name) }
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
let text = ''; for await (const part of request) text += part
|
||||
const body = JSON.parse(text), system = body.messages[0].content, input = JSON.parse(body.messages.at(-1).content)
|
||||
const schema = JSON.parse(system.slice(system.indexOf('{'), system.indexOf('}') + 1))
|
||||
modelCalls.push(input)
|
||||
const evidence = input.sources.map(source => source.text || source.Text).join('; ')
|
||||
const content = JSON.stringify(Object.fromEntries(Object.keys(schema).map(key => [key, `Evidence: ${evidence}`])))
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' })
|
||||
response.end(JSON.stringify({ id: 'digital-parity', model: 'digital-parity', choices: [{ index: 0, message: { role: 'assistant', content }, finish_reason: 'stop' }], usage: { prompt_tokens: 30, completion_tokens: 30, total_tokens: 60 } }))
|
||||
} catch { response.writeHead(500); response.end('{}') }
|
||||
})
|
||||
|
||||
async function request(route, method = 'GET', body, expected = 200) {
|
||||
const response = await fetch(base + route, { method, headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059' } : {}) }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(45000) })
|
||||
assert.equal(response.status, expected, `${stage}: ${method} ${route} HTTP ${response.status}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8'))
|
||||
auth = await request('/auth/login', 'POST', { email: credentials.email, password: credentials.password })
|
||||
await new Promise(resolve => server.listen(18083, '0.0.0.0', resolve))
|
||||
try {
|
||||
stage = 'create_fixtures'
|
||||
model = (await request('/models', 'POST', { name: `digital-parity-${Date.now()}`, type: 'KnowledgeQA', source: 'remote', parameters: { base_url: 'http://host.docker.internal:18083/v1', api_key: 'local-test-only', provider: 'generic' } }, 201)).data
|
||||
human = (await request('/digital-humans', 'POST', { name: `数字人迁移验收-${Date.now()}`, description: '临时测试,完成后删除', model_id: model.id }, 201)).data
|
||||
const primary = (await request(`/agents/${human.primary_agent_id}`)).data
|
||||
check('automatic_same_name_agent_and_binding', primary.name === human.name && human.bindings.length === 1 && human.bindings[0].agent_id === primary.id)
|
||||
|
||||
stage = 'document_upload'
|
||||
const docx = await Packer.toBuffer(new Document({ sections: [{ children: [new Paragraph('DOCX_PARITY_EVIDENCE: Explain a concept, demonstrate an example, and summarize the lesson.')] }] }))
|
||||
const pdf = await PDFDocument.create(), font = await pdf.embedFont(StandardFonts.Helvetica), pdfPage = pdf.addPage([600, 300])
|
||||
pdfPage.drawText('PDF_PARITY_EVIDENCE: Use a clear example and conclude with a recap.', { x: 25, y: 220, size: 12, font })
|
||||
const form = new FormData()
|
||||
form.set('revision', String(human.revision)); form.set('model_id', model.id)
|
||||
form.append('files', new Blob([docx], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }), 'parity-evidence.docx')
|
||||
form.append('files', new Blob([await pdf.save()], { type: 'application/pdf' }), 'parity-evidence.pdf')
|
||||
const upload = await fetch(`${base}/digital-humans/${human.id}/distill`, { method: 'POST', headers: { Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059' }, body: form, signal: AbortSignal.timeout(30000) })
|
||||
check('docx_pdf_upload_accepted', upload.status === 202)
|
||||
const job = (await upload.json()).data, start = Date.now(), progress = []
|
||||
let current
|
||||
do {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
current = (await request(`/digital-humans/${human.id}/jobs/${job.id}`)).data
|
||||
if (progress.at(-1)?.stage !== current.stage) progress.push({ status: current.status, stage: current.stage, progress: current.progress })
|
||||
} while (['running', 'queued'].includes(current.status) && Date.now() - start < 150000)
|
||||
fs.writeFileSync(path.join(directory, 'digital-parity-document-progress.json'), JSON.stringify({ duration_ms: Date.now() - start, progress, error: current.error }, null, 2))
|
||||
check('docx_pdf_real_reader_completed', current.status === 'completed')
|
||||
human = (await request(`/digital-humans/${human.id}`)).data
|
||||
const evidence = modelCalls.flatMap(call => call.sources || []).map(source => source.text || source.Text).join('\n')
|
||||
fs.writeFileSync(path.join(directory, 'digital-parity-document-evidence.json'), JSON.stringify(modelCalls.flatMap(call => call.sources || []), null, 2))
|
||||
check('both_document_markers_reach_model', evidence.includes('Explain a concept, demonstrate an example, and summarize the lesson.') && evidence.includes('Use a clear example and conclude with a recap'))
|
||||
|
||||
stage = 'wechat_browser'
|
||||
channel = { id: require('node:crypto').randomUUID(), name: '验收专用已停用连接' }
|
||||
sql(`INSERT INTO im_channels (id, tenant_id, agent_id, platform, name, enabled, mode, output_mode, session_mode, credentials, created_at, updated_at) VALUES (${literal(channel.id)},10059,${literal(human.primary_agent_id)},'wechat',${literal(channel.name)},false,'longpoll','full','user','{}',now(),now())`)
|
||||
browser = await chromium.launch({ headless: true, channel: 'chrome' })
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } })
|
||||
await context.addInitScript(a => {
|
||||
const values = { weknora_user: JSON.stringify({ ...a.user, tenant_id: String(a.user.tenant_id) }), weknora_tenant: JSON.stringify({ ...a.active_tenant, id: '10059' }), weknora_token: a.token, weknora_refresh_token: a.refresh_token, weknora_memberships: JSON.stringify(a.memberships) }
|
||||
for (const [key, value] of Object.entries(values)) localStorage.setItem(key, value)
|
||||
localStorage.setItem('weknora:new-user-guide-done:v1', '1')
|
||||
}, auth)
|
||||
page = await context.newPage(); page.setDefaultTimeout(15000)
|
||||
page.on('pageerror', error => errors.push(error.message))
|
||||
await page.goto('http://127.0.0.1:15173/platform/digital-humans')
|
||||
const row = page.locator('.human-row').filter({ hasText: human.name })
|
||||
await row.getByRole('button', { name: '微信连接', exact: true }).click()
|
||||
const panel = page.locator('.wechat-panel')
|
||||
await panel.getByText(channel.name, { exact: true }).waitFor()
|
||||
await panel.getByRole('button', { name: '删除微信连接', exact: true }).waitFor()
|
||||
check('wechat_delete_control_visible', await panel.getByRole('button', { name: '删除微信连接', exact: true }).isVisible())
|
||||
await page.screenshot({ path: path.join(directory, 'digital-wechat-desktop.png'), animations: 'disabled' })
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.screenshot({ path: path.join(directory, 'digital-wechat-mobile.png'), animations: 'disabled' })
|
||||
check('wechat_mobile_no_horizontal_overflow', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2))
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
const qrResponse = page.waitForResponse(response => response.url().endsWith(`/digital-humans/${human.id}/wechat/qrcode`) && response.request().method() === 'POST', { timeout: 50000 })
|
||||
await panel.getByRole('button', { name: '连接微信', exact: true }).click()
|
||||
const response = await qrResponse
|
||||
check('official_wechat_qrcode_response', response.status() === 200)
|
||||
const dialog = page.locator('.t-dialog:visible')
|
||||
await dialog.locator('canvas, .t-qrcode svg').first().waitFor()
|
||||
check('wechat_qrcode_renders', await dialog.locator('canvas, .t-qrcode svg').first().evaluate(element => {
|
||||
if (element.tagName.toLowerCase() === 'svg') return element.getBoundingClientRect().width > 100 && element.querySelectorAll('path,rect').length > 0
|
||||
const data = element.getContext('2d').getImageData(0, 0, element.width, element.height).data
|
||||
let dark = 0, light = 0
|
||||
for (let i = 0; i < data.length; i += 4) { if (data[i] < 80 && data[i + 3] > 0) dark++; if (data[i] > 200 && data[i + 3] > 0) light++ }
|
||||
return dark > 100 && light > 100
|
||||
}))
|
||||
await page.screenshot({ path: path.join(directory, 'digital-wechat-qr-desktop.png'), animations: 'disabled' })
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.screenshot({ path: path.join(directory, 'digital-wechat-qr-mobile.png'), animations: 'disabled' })
|
||||
check('wechat_qrcode_mobile_fits', await dialog.evaluate(element => { const box = element.getBoundingClientRect(); return box.left >= 0 && box.right <= innerWidth + 2 && box.top >= 0 && box.bottom <= innerHeight + 2 }))
|
||||
await dialog.locator('.t-dialog__close').click()
|
||||
await panel.getByRole('button', { name: '删除微信连接', exact: true }).click()
|
||||
await page.getByRole('button', { name: '删除连接', exact: true }).click()
|
||||
await panel.getByText(channel.name, { exact: true }).waitFor({ state: 'detached' })
|
||||
check('wechat_delete_persisted', (await request(`/digital-humans/${human.id}/wechat/channels`)).data.length === 0)
|
||||
channel = undefined
|
||||
check('no_browser_errors', errors.length === 0)
|
||||
} finally {
|
||||
if (page && checks.some(item => !item.passed)) await page.screenshot({ path: path.join(directory, 'digital-parity-failure.png'), animations: 'disabled' }).catch(() => {})
|
||||
if (browser) await browser.close()
|
||||
if (human) {
|
||||
await request(`/digital-humans/${human.id}`, 'DELETE').catch(() => {})
|
||||
const humanID = literal(human.id), agentID = literal(human.primary_agent_id)
|
||||
sql(`BEGIN; DELETE FROM im_channels WHERE agent_id=${agentID}; DELETE FROM digital_human_jobs WHERE digital_human_id=${humanID}; DELETE FROM digital_human_bindings WHERE digital_human_id=${humanID}; DELETE FROM digital_human_versions WHERE digital_human_id=${humanID}; DELETE FROM digital_humans WHERE id=${humanID}; DELETE FROM custom_agents WHERE id=${agentID}; COMMIT;`)
|
||||
check('temporary_human_agent_removed', sql(`SELECT count(*) FROM digital_humans WHERE id=${humanID}`) === '0' && sql(`SELECT count(*) FROM custom_agents WHERE id=${agentID}`) === '0')
|
||||
}
|
||||
if (model) sql(`DELETE FROM models WHERE id=${literal(model.id)}`)
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
const result = { time: new Date().toISOString(), stage, checks, browser_errors: errors, model_calls: modelCalls.length, model: 'local deterministic stub', limits: ['No real WeChat scan or message', 'Text PDF tested; scanned PDF OCR not tested'] }
|
||||
fs.writeFileSync(path.join(directory, 'digital-parity-live-20260916.json'), JSON.stringify(result, null, 2))
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
}
|
||||
}
|
||||
main().catch(error => { console.error(`${stage}: ${error.message}`); process.exitCode = 1 })
|
||||
Reference in New Issue
Block a user