const assert = require('node:assert/strict') const fs = require('node:fs') const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') const origin = process.env.VERIFY_FRONTEND_ORIGIN || 'http://127.0.0.1:15173' const apiOrigin = process.env.VERIFY_API_ORIGIN || 'http://127.0.0.1:18080' const reportName = process.env.VERIFY_REPORT_NAME || 'agent-creation-ui' assert.match(reportName, /^[a-zA-Z0-9_-]+$/) const startedAt = new Date().toISOString() // Draft AI output is a fixture; other reads use the running local API. No resources are saved. async function main() { const directory = '/Volumes/Extreme SSD/yawan-storage/weknora/acceptance' const credentials = JSON.parse(fs.readFileSync(`${directory}/super-admin-login.json`, 'utf8')) const response = await fetch(`${apiOrigin}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: credentials.email, password: credentials.password }), }) assert.equal(response.status, 200) const auth = await response.json() const browser = await chromium.launch({ headless: true, channel: 'chrome' }) const checks = [], errors = [], failures = [], requests = [], failedRequests = [], slowRequests = [] const pendingRequests = new Map() let finalPath = '' const check = (name, passed) => { checks.push({ name, passed: !!passed }); assert.ok(passed, name) } try { const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) await context.addInitScript(a => { const entries = { 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:new-user-guide-done:v1': '1', } for (const [key, value] of Object.entries(entries)) localStorage.setItem(key, value) }, auth) let releaseDraft, draftRequest await context.route('**/api/v1/agents/draft', route => new Promise(resolve => { draftRequest = route.request().postDataJSON() releaseDraft = async () => { await route.fulfill({ status: 200, json: { success: true, data: { name: '课程框架规划助手', description: '按用户目标整理课程结构,确保前后衔接。', avatar: '', config: { role: 'training_map', agent_type: 'custom', agent_mode: 'quick-answer', model_id: draftRequest.model_id, system_prompt: '规划课程框架并说明每一天的前置条件。', kb_selection_mode: 'none', knowledge_bases: [], mcp_selection_mode: 'none', skills_selection_mode: 'none', selected_skills: [], instruction_skill_ids: [] }, } } }) resolve() } })) const page = await context.newPage() page.on('request', request => { requests.push({ path: new URL(request.url()).pathname, method: request.method() }) pendingRequests.set(request, Date.now()) }) 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) if (failure.error !== 'net::ERR_ABORTED') errors.push(failure) }) page.on('pageerror', error => errors.push({ stage: checks.at(-1)?.name, error: error.stack || error.message })) await page.goto(`${origin}/platform/agents`) const ai = page.getByRole('button', { name: 'AI 创建', exact: true }) await ai.waitFor() check('manual_and_ai_creation_visible', await page.getByRole('button', { name: '手动创建', exact: true }).isVisible()) await page.locator('.agent-installations').first().waitFor() check('installation_status_visible_on_agent_cards', await page.locator('.agent-installations').count() > 0) await page.waitForLoadState('networkidle', { timeout: 15000 }) check('hidden_agent_editor_not_requested', !requests.some(item => /\/AgentEditorModal-[^/]+\.js$/.test(item.path))) check('hidden_draft_dialog_not_requested', !requests.some(item => /\/AgentDraftDialog-[^/]+\.js$/.test(item.path))) check('retired_resource_versions_menu_absent', await page.locator('.sidebar').getByText('资源版本', { exact: true }).count() === 0) await page.screenshot({ path: `${directory}/agent-creation-list-desktop.png`, animations: 'disabled' }) await ai.click() const dialog = page.locator('.t-dialog:visible') await dialog.getByPlaceholder('例如:面向课程学员,负责将每日课程框架整理为有前后衔接的讲稿,重点解释概念和案例。').fill('请创建负责课程规划与每日框架衔接的导图助手。') await dialog.getByRole('button', { name: '生成配置', exact: true }).click() await dialog.locator('progress').waitFor() check('ai_generation_progress_visible', await dialog.locator('progress').isVisible()) check('ai_double_submit_prevented', await dialog.getByRole('button', { name: /生成配置/ }).isDisabled()) await page.screenshot({ path: `${directory}/agent-draft-progress-desktop.png`, animations: 'disabled' }) await page.setViewportSize({ width: 390, height: 844 }) check('draft_dialog_fits_mobile', await dialog.evaluate(element => { const rect = element.getBoundingClientRect(); return rect.left >= 0 && rect.right <= innerWidth + 2 })) await page.screenshot({ path: `${directory}/agent-draft-progress-mobile.png`, animations: 'disabled' }) await releaseDraft() const name = page.locator('[data-guide="agent-create-name"] input') await name.waitFor() await page.waitForFunction(() => document.querySelector('[data-guide="agent-create-name"] input')?.value === '课程框架规划助手') check('ai_draft_loaded_into_editable_editor', await name.inputValue() === '课程框架规划助手') check('draft_request_preserves_requirement', draftRequest.requirement === '请创建负责课程规划与每日框架衔接的导图助手。') await page.screenshot({ path: `${directory}/agent-draft-editor-mobile.png`, animations: 'disabled' }) // Restore normal browser caching after the one mocked AI request. await context.unroute('**/api/v1/agents/draft') await page.goto(`${origin}/platform/workbench/resource-versions`) await ai.waitFor() check('retired_route_redirects_to_agents', new URL(page.url()).pathname === '/platform/agents') await page.getByRole('button', { name: '手动创建', exact: true }).click() await name.waitFor() check('manual_creation_does_not_reuse_ai_draft', await name.inputValue() !== '课程框架规划助手') check('no_business_resources_saved', !requests.some(item => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method) && item.path.startsWith('/api/') && !item.path.startsWith('/api/v1/auth/') && item.path !== '/api/v1/agents/draft')) check('no_vue_runtime_errors', errors.length === 0) } catch (error) { failures.push(error.message) const pages = browser.contexts().flatMap(context => context.pages()) await pages[0]?.screenshot({ path: `${directory}/agent-creation-ui-failure.png` }) } finally { finalPath = browser.contexts().flatMap(context => context.pages()).map(page => new URL(page.url()).pathname).join(',') const diagnostics = { path: finalPath, slow_requests: slowRequests, failed_requests: failedRequests, pending_requests: [...pendingRequests].map(([request, started]) => ({ path: new URL(request.url()).pathname, duration_ms: Date.now() - started })) } await browser.close() const report = { started_at: startedAt, checked_at: new Date().toISOString(), origin, checks, errors, failures, diagnostics, ai_output_mocked: true, passed: failures.length === 0 && errors.length === 0 && checks.every(item => item.passed) } fs.writeFileSync(`${directory}/${reportName}.json`, JSON.stringify(report, null, 2)) console.log(JSON.stringify(report)) if (!report.passed) process.exitCode = 1 } } main().catch(error => { console.error(error.message); process.exitCode = 1 })