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 { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const directory = path.resolve(__dirname, '../../acceptance'); const base = 'http://127.0.0.1:18080/api/v1'; const sql = statement => execFileSync('docker', ['--context', 'colima-yawan', 'exec', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', 'yawan_native_acceptance_20260915', '-v', 'ON_ERROR_STOP=1', '-Atc', statement], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); const literal = value => "'" + String(value).replaceAll("'", "''") + "'"; const checks = [], calls = [], browserErrors = []; const check = (name, passed) => { checks.push({ name, passed: !!passed }); assert.ok(passed, name); }; let auth, model, human, browser; const server = http.createServer(async (request, response) => { try { let body = ''; for await (const part of request) body += part; const input = JSON.parse(body), payload = JSON.parse(input.messages.at(-1).content); calls.push(payload); assert.equal(input.stream, true); response.writeHead(200, { 'Content-Type': 'text/event-stream' }); const chunk = delta => ({ id: 'trial-fixture', model: 'trial-fixture', choices: [{ index: 0, delta, finish_reason: null }] }); response.write(`data: ${JSON.stringify(chunk({ role: 'assistant', content: '先观察问题,再用清晰的例子展开。' }))}\n\n`); await new Promise(resolve => setTimeout(resolve, 900)); response.write(`data: ${JSON.stringify(chunk({ content: '最后总结观点,邀请听众回应。' }))}\n\n`); response.write(`data: ${JSON.stringify({ id: 'trial-fixture', model: 'trial-fixture', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 30, completion_tokens: 20, total_tokens: 50 } })}\n\n`); response.end('data: [DONE]\n\n'); } catch (error) { response.writeHead(500); response.end(JSON.stringify({ error: 'fixture failure' })); } }); async function request(route, method = 'GET', body, expected = 200) { const result = 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(30000) }); assert.equal(result.status, expected, `${method} ${route} HTTP ${result.status}`); return result.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(0, '0.0.0.0', resolve)); try { model = (await request('/models', 'POST', { name: `trial-fixture-${Date.now()}`, type: 'KnowledgeQA', source: 'remote', parameters: { base_url: `http://host.docker.internal:${server.address().port}/v1`, api_key: 'local-fixture-only', provider: 'generic' } }, 201)).data; human = (await request('/digital-humans', 'POST', { name: `版本试写验收-${Date.now()}`, model_id: model.id, persona: { tone: 'ACTIVE_VERSION_ONE' } }, 201)).data; human = (await request(`/digital-humans/${human.id}/review`, 'POST', { version: 1, approve: true, note: 'fixture' })).data; human = (await request(`/digital-humans/${human.id}`, 'PUT', { name: human.name, model_id: model.id, revision: human.revision, persona: { tone: 'SELECTED_DRAFT_VERSION_TWO' } })).data; const before = JSON.stringify(human); const trial = await fetch(`${base}/digital-humans/${human.id}/trial`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059' }, body: JSON.stringify({ version: 2, theme: '教学表达', advice: '先举例,再总结' }), signal: AbortSignal.timeout(30000) }); check('stream_endpoint_ready', trial.status === 200 && trial.headers.get('content-type').includes('text/event-stream')); const events = (await trial.text()).split('\n\n').filter(event => event.startsWith('event:trial') || event.startsWith('event: trial')).map(event => JSON.parse(event.split('\n').find(line => line.startsWith('data:')).slice(5))); check('progress_and_result_complete', events.some(event => event.type === 'progress') && events.some(event => event.type === 'delta') && events.at(-1).type === 'complete' && events.at(-1).progress === 100); check('selected_draft_reaches_configured_model', calls.length === 1 && calls[0].selected_version === 2 && calls[0].expression_profile.tone === 'SELECTED_DRAFT_VERSION_TWO' && calls[0].writing_advice === '先举例,再总结'); const after = (await request(`/digital-humans/${human.id}`)).data; check('trial_does_not_publish_or_edit_profile', JSON.stringify(after) === before && after.active_version === 1 && after.bindings[0].applied_version === 1); await request(`/digital-humans/${human.id}/trial`, 'POST', { version: 999, theme: 'missing' }, 404); check('missing_version_does_not_call_model', calls.length === 1); 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), weknora_selected_tenant_id: '10059', 'weknora:new-user-guide-done:v1': '1' }; for (const [key, value] of Object.entries(values)) localStorage.setItem(key, value); }, auth); const page = await context.newPage(); page.setDefaultTimeout(20000); page.on('pageerror', error => browserErrors.push(error.message)); await page.goto('http://127.0.0.1:15173/platform/digital-humans'); await page.locator('.human-row').filter({ hasText: human.name }).getByRole('button', { name: '管理', exact: true }).click(); await page.getByText('版本审核', { exact: true }).click(); const panel = page.locator('.trial-panel'); await panel.getByPlaceholder('填写主题和目标受众').fill('教学试写验收'); await panel.getByRole('button', { name: '开始试写', exact: true }).click(); await panel.locator('.trial-progress').waitFor(); await page.screenshot({ path: path.join(directory, 'digital-trial-progress-desktop.png'), animations: 'disabled' }); await panel.getByText('试写完成', { exact: true }).waitFor(); check('browser_selected_version_result', (await panel.locator('.trial-result').innerText()).includes('v2 试写结果') && (await panel.locator('.trial-result').innerText()).includes('最后总结观点')); await page.screenshot({ path: path.join(directory, 'digital-trial-desktop.png'), animations: 'disabled' }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: path.join(directory, 'digital-trial-mobile.png'), animations: 'disabled' }); check('mobile_no_horizontal_overflow', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)); const usage = JSON.parse(sql(`SELECT COALESCE(json_agg(u),'[]') FROM workbench_usage u WHERE tenant_id=10059 AND model_id=${literal(model.id)}`)); check('trial_metered_to_caller', usage.length === 2 && usage.every(row => row.owner_id === auth.user.id && row.status === 'succeeded' && row.points > 0 && row.action.includes('试写'))); check('no_browser_errors', browserErrors.length === 0); } finally { if (browser) await browser.close(); if (human) { await request(`/digital-humans/${human.id}`, 'DELETE').catch(() => {}); sql(`BEGIN; DELETE FROM digital_human_jobs WHERE digital_human_id=${literal(human.id)}; DELETE FROM digital_human_bindings WHERE digital_human_id=${literal(human.id)}; DELETE FROM digital_human_versions WHERE digital_human_id=${literal(human.id)}; DELETE FROM digital_humans WHERE id=${literal(human.id)}; DELETE FROM custom_agents WHERE id=${literal(human.primary_agent_id)}; COMMIT;`); } if (model) { // Refund only this model fixture's metered calls; concurrent balances are preserved. sql(`BEGIN; UPDATE workbench_balances SET balance=balance+(SELECT COALESCE(sum(points),0) FROM workbench_usage WHERE tenant_id=10059 AND owner_id=${literal(auth.user.id)} AND model_id=${literal(model.id)}) WHERE tenant_id=10059 AND user_id=${literal(auth.user.id)}; DELETE FROM workbench_transactions WHERE tenant_id=10059 AND user_id=${literal(auth.user.id)} AND metadata->>'call_id' IN (SELECT call_id FROM workbench_usage WHERE model_id=${literal(model.id)}); DELETE FROM workbench_balance_holds WHERE id IN (SELECT call_id FROM workbench_usage WHERE model_id=${literal(model.id)}); DELETE FROM workbench_usage WHERE model_id=${literal(model.id)}; DELETE FROM models WHERE id=${literal(model.id)}; COMMIT;`); } await new Promise(resolve => server.close(resolve)); const report = { time: new Date().toISOString(), checks, browser_errors: browserErrors, model_calls: calls.length, model: 'local deterministic stream fixture; no paid generation' }; fs.writeFileSync(path.join(directory, 'digital-trial-live-20260917.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify(report, null, 2)); } } main().catch(error => { console.error(error.message); process.exitCode = 1; });