Files

138 lines
8.7 KiB
JavaScript
Raw Permalink Normal View History

const startedAt = new Date().toISOString()
const assert = require('node:assert/strict')
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 outputName = process.env.VERIFY_REPORT_NAME || 'public-release-smoke'
assert.match(outputName, /^[a-zA-Z0-9_-]+$/)
const checks = [], failures = [], expectedDenials = [], pageErrors = []
const timings = {}, pendingRequests = new Map(), slowRequests = [], failedRequests = []
const mapRequests = []
const runtimeConfigRequests = []
let measuringMapEntry = false
let browser, page
function check(name, value) {
checks.push({ name, passed: !!value })
assert.ok(value, name)
}
async function main() {
const release = JSON.parse(fs.readFileSync(path.join(root, 'public-release.json'), 'utf8'))
const res = await fetch(`${origin}/release.json`, { signal: AbortSignal.timeout(30000) })
check('immutable_release_matches_origin', res.ok && (await res.json()).release === release.release)
const html = await fetch(`${origin}/login`, { signal: AbortSignal.timeout(30000) })
check('html_revalidates_after_updates', html.ok && /no-cache/.test(html.headers.get('cache-control')))
const body = await html.text()
check('main_runtime_config_is_inline', body.includes('<script data-runtime-config>') && !body.includes('<script src="/config.js"></script>'))
const embed = await fetch(`${origin}/embed/release-verification`, { signal: AbortSignal.timeout(30000) })
const embedBody = await embed.text()
check('embed_runtime_config_is_inline', embed.ok && embedBody.includes('<script data-runtime-config>') && !embedBody.includes('<script src="/config.js"></script>'))
const assets = [...body.matchAll(/(?:src|href)="(\/assets\/[^"]+\.(?:js|css))"/g)].map(match => match[1])
check('production_build_has_assets', assets.length > 0)
for (const asset of assets) {
const response = await fetch(origin + asset, { signal: AbortSignal.timeout(30000) })
check(`static_asset_available:${asset}`, response.ok && /immutable/.test(response.headers.get('cache-control')))
await response.arrayBuffer()
}
const protectedResponse = await fetch(`${origin}/api/v1/access/me`, { signal: AbortSignal.timeout(30000) })
check('api_requires_auth_and_does_not_cache', protectedResponse.status === 401 && /no-store/.test(protectedResponse.headers.get('cache-control')))
const credentials = JSON.parse(fs.readFileSync(path.join(root, 'acceptance/super-admin-login.json'), 'utf8'))
browser = await chromium.launch({ headless: true, channel: 'chrome' })
page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
await page.addInitScript(() => localStorage.setItem('weknora:new-user-guide-done:v1', '1'))
page.on('pageerror', error => pageErrors.push(error.stack || error.message))
page.on('request', request => {
pendingRequests.set(request, Date.now())
const url = new URL(request.url())
if (url.pathname === '/config.js') runtimeConfigRequests.push(url.pathname)
if (measuringMapEntry && request.method() === 'GET' && url.pathname === '/api/v1/workbench/resources/training-maps') mapRequests.push(url.pathname + url.search)
})
page.on('requestfinished', request => {
const started = pendingRequests.get(request)
pendingRequests.delete(request)
const duration = started === undefined ? 0 : Date.now() - started
if (duration >= 3000) slowRequests.push({ path: new URL(request.url()).pathname, duration_ms: duration })
})
page.on('requestfailed', request => {
pendingRequests.delete(request)
const failure = { path: new URL(request.url()).pathname, error: request.failure()?.errorText || '' }
failedRequests.push(failure)
// Full-page navigation intentionally cancels the preceding page's polling.
if (failure.error !== 'net::ERR_ABORTED') pageErrors.push(`request failed: ${failure.path} ${failure.error}`)
})
page.on('response', response => {
const pathname = new URL(response.url()).pathname
// Standard deployments explicitly refuse the login page's Lite bootstrap probe.
if (pathname === '/api/v1/auth/auto-setup' && response.status() === 403) {
expectedDenials.push({ path: pathname, status: 403, reason: 'Lite-only bootstrap denied in standard edition' })
return
}
if (response.status() >= 400 && /\/(?:assets|api)\//.test(pathname)) {
pageErrors.push(`${response.status()} ${pathname}`)
}
})
await page.goto(`${origin}/login`)
check('login_has_no_upstream_official_links', await page.locator('a[href*="github.com"], a[href*="weknora.com"]').count() === 0)
await page.locator('input[autocomplete="username"]').fill(credentials.username)
await page.locator('input[type="password"]').first().fill(credentials.password)
const loginStarted = Date.now()
await page.locator('button[type="submit"]').first().click()
await page.waitForURL('**/platform/**', { timeout: 30000 })
timings.login_to_route_ms = Date.now() - loginStarted
await page.waitForLoadState('networkidle', { timeout: 30000 })
check('real_public_form_login', true)
const mapsStarted = Date.now()
measuringMapEntry = true
await page.goto(`${origin}/admin#/admin/training-maps`)
await page.waitForURL('**/platform/workbench/content?tab=maps', { timeout: 30000 })
await page.waitForLoadState('networkidle', { timeout: 30000 })
timings.training_maps_ready_ms = Date.now() - mapsStarted
measuringMapEntry = false
check('old_training_map_bookmark_redirects', true)
check('training_maps_entry_does_not_fetch_duplicate_pages', mapRequests.length > 0 && new Set(mapRequests).size === mapRequests.length)
check('training_maps_entry_uses_lightweight_lists', mapRequests.every(url => new URL(url, origin).searchParams.get('view') === 'summary'))
await page.locator('.workbench-detail h2').waitFor({ state: 'visible' })
const firstSelectedName = await page.locator('.workbench-resource.is-selected strong').innerText()
check('selected_map_loads_complete_detail', await page.locator('.workbench-detail h2').innerText() === firstSelectedName)
if (await page.locator('.workbench-resource').count() > 1) {
const next = page.locator('.workbench-resource').nth(1)
const nextName = await next.locator('strong').innerText()
await next.click()
await page.locator('.workbench-detail h2').filter({ hasText: nextName }).waitFor({ state: 'visible' })
check('selecting_another_map_loads_matching_detail', await page.locator('.workbench-detail h2').innerText() === nextName)
await page.getByRole('button', { name: '刷新', exact: true }).click()
await page.locator('.workbench-detail h2').filter({ hasText: nextName }).waitFor({ state: 'visible' })
check('refresh_keeps_selected_map_detail', await page.locator('.workbench-detail h2').innerText() === nextName)
}
await page.screenshot({ path: path.join(root, `acceptance/${outputName}-desktop.png`), animations: 'disabled' })
if (process.env.VERIFY_PC_ONLY !== '1') {
await page.setViewportSize({ width: 390, height: 844 })
await page.screenshot({ path: path.join(root, `acceptance/${outputName}-mobile.png`), animations: 'disabled' })
check('mobile_page_does_not_overflow', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2))
}
await page.goto(`${origin}/admin#/admin/digital-humans`)
await page.waitForURL('**/platform/digital-humans', { timeout: 30000 })
await page.waitForLoadState('networkidle', { timeout: 30000 })
check('old_digital_human_bookmark_redirects', true)
check('no_unexpected_api_or_lazy_module_errors', pageErrors.length === 0)
check('no_blocking_runtime_config_request', runtimeConfigRequests.length === 0)
}
main().catch(error => { failures.push(error.message); process.exitCode = 1 }).finally(async () => {
const diagnostics = {
path: page ? new URL(page.url()).pathname : null,
slow_requests: slowRequests,
failed_requests: failedRequests,
training_map_requests: mapRequests,
runtime_config_requests: runtimeConfigRequests,
pending_requests: [...pendingRequests].map(([request, started]) => ({ path: new URL(request.url()).pathname, duration_ms: Date.now() - started })),
}
failures.push(...pageErrors)
if (failures.length) process.exitCode = 1
if (browser) await browser.close()
const report = { started_at: startedAt, checked_at: new Date().toISOString(), origin, pc_only: process.env.VERIFY_PC_ONLY === '1', checks, timings, diagnostics, expected_denials: expectedDenials, failures, passed: !failures.length }
fs.writeFileSync(path.join(root, `acceptance/${outputName}.json`), JSON.stringify(report, null, 2))
console.log(JSON.stringify(report))
})