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 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))
|
||||
})
|
||||
Reference in New Issue
Block a user