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 directory = '/Volumes/Extreme SSD/yawan-storage/weknora/acceptance' // All business writes and Agent/Markdown skill rows are fixtures. Other reads use the local API. async function main() { 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 = [], writes = [] const check = (name, value) => { checks.push({ name, passed: !!value }); assert.ok(value, name) } let agents const skills = [ { id: 'ui-writing', name: '课程写作规则与案例衔接方法', enabled: true, visibility: 'global' }, { id: 'ui-disabled', name: '已停用的旧表达规则', enabled: false, visibility: 'private' }, ].map(skill => ({ ...skill, created_by: auth.user.id, instruction_only: true, updated_at: new Date().toISOString(), description: '仅用于界面验收的临时浏览器数据。' })) 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) await context.route('**/api/**', async route => { const request = route.request(), pathname = new URL(request.url()).pathname, method = request.method() if (pathname === '/api/v1/agents' && method === 'GET') { if (!agents) { const result = await route.fetch(), body = await result.json() const base = body.data?.find(agent => agent.config?.model_id) assert.ok(base, 'A model-backed Agent is needed for fixture defaults') agents = [ { ...base, id: 'ui-mounted-agent', name: '技能验收助手', created_by: auth.user.id, is_builtin: false, digital_human: null, config: { ...base.config, agent_mode: 'smart-reasoning', digital_human_id: '', instruction_skill_ids: ['ui-writing', 'ui-disabled', 'ui-unavailable'], sandbox_config_id: '', skills_selection_mode: 'selected', selected_skills: ['saved-unavailable-script'] } }, { ...base, id: 'ui-empty-agent', name: '未挂载助手', created_by: auth.user.id, is_builtin: false, digital_human: null, config: { ...base.config, agent_mode: 'quick-answer', digital_human_id: '', instruction_skill_ids: [], sandbox_config_id: '', skills_selection_mode: 'none', selected_skills: [] } }, ] } return route.fulfill({ json: { success: true, data: agents, disabled_own_agent_ids: [] } }) } if (pathname === '/api/v1/skills/instructions' && method === 'GET') return route.fulfill({ json: { success: true, data: skills } }) if (pathname === '/api/v1/skills/instructions/mount' && method === 'POST') { const body = request.postDataJSON(); writes.push({ path: pathname, body }) const data = body.agent_ids.map(id => { const agent = agents.find(agent => agent.id === id) agent.config.instruction_skill_ids = body.mode === 'remove' ? agent.config.instruction_skill_ids.filter(id => !body.skill_ids.includes(id)) : [...new Set([...agent.config.instruction_skill_ids, ...body.skill_ids])] return { agent_id: id, instruction_skill_ids: agent.config.instruction_skill_ids } }) return route.fulfill({ json: { success: true, data } }) } if (pathname === '/api/v1/agents/ui-mounted-agent' && method === 'PUT') { const body = request.postDataJSON(); writes.push({ path: pathname, body }) agents[0] = { ...agents[0], ...body } return route.fulfill({ json: { success: true, data: agents[0] } }) } if (pathname.startsWith('/api/v1/agents/ui-')) { if (pathname.endsWith('/im/channels') || pathname.endsWith('/embed/channels')) return route.fulfill({ json: { success: true, data: [] } }) if (method === 'GET') return route.fulfill({ json: { success: true, data: agents.find(agent => pathname.endsWith(agent.id)) || [] } }) } if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { writes.push({ path: pathname, blocked: true }) return route.fulfill({ status: 409, json: { success: false, message: 'UI verification blocks business writes' } }) } return route.continue() }) const page = await context.newPage() page.on('pageerror', error => errors.push(error.message)) await page.goto(`${origin}/platform/agents`) const card = page.locator('.agent-card').filter({ hasText: '技能验收助手' }).first() await card.waitFor() check('card_shows_instruction_names_and_unavailable_binding', (await card.innerText()).includes('课程写作规则') && (await card.innerText()).includes('不可用')) await card.click() await page.locator('[data-guide="agent-create-name"] input').waitFor() await page.locator('.settings-sidebar').getByText('技能', { exact: true }).click() const section = page.locator('.section:visible').filter({ has: page.getByRole('heading', { name: '技能', exact: true }) }) await section.getByText('Markdown 技能', { exact: true }).waitFor() check('markdown_picker_enabled_without_sandbox', await section.locator('.t-select').first().getAttribute('aria-disabled') !== 'true') check('script_sandbox_collapsed_by_default', !(await section.locator('.sandbox-config-select').isVisible())) await page.screenshot({ path: `${directory}/instruction-skills-editor-desktop.png`, animations: 'disabled' }) await page.setViewportSize({ width: 390, height: 844 }) await page.screenshot({ path: `${directory}/instruction-skills-editor-mobile.png`, animations: 'disabled' }) check('editor_fits_mobile', await page.locator('.settings-modal').evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 })) check('mounted_chips_fit_mobile_without_clipping', await section.locator('.agent-installations').evaluate(element => element.scrollWidth <= element.clientWidth + 1 && [...element.querySelectorAll('.installed-skill')].every(chip => chip.getBoundingClientRect().right <= innerWidth))) await page.setViewportSize({ width: 1440, height: 1000 }) for (const label of ['课程写作规则与案例衔接方法', '已停用的旧表达规则', '技能 ui-unava']) await section.getByRole('button', { name: `解除挂载 ${label}`, exact: true }).click() await page.getByRole('button', { name: '保存并关闭', exact: true }).click() await page.waitForFunction(() => !document.querySelector('[data-guide="agent-create-name"] input')) const saved = writes.find(write => write.path === '/api/v1/agents/ui-mounted-agent') check('empty_instruction_selection_sent_on_save', Array.isArray(saved?.body.config.instruction_skill_ids) && saved.body.config.instruction_skill_ids.length === 0) check('script_selection_preserved_on_markdown_save', saved?.body.config.selected_skills.join(',') === 'saved-unavailable-script' && saved.body.config.skills_selection_mode === 'selected' && saved.body.config.sandbox_config_id === '') await page.locator('.agent-card').filter({ hasText: '未挂载助手' }).first().click() await page.locator('[data-guide="agent-create-name"] input').waitFor() await page.locator('.settings-sidebar').getByText('技能', { exact: true }).click() check('quick_answer_has_markdown_skills_without_script_controls', await section.getByText('Markdown 技能', { exact: true }).isVisible() && await section.locator('.script-skills-section').count() === 0) agents[0].config.instruction_skill_ids = ['ui-writing'] await page.goto(`${origin}/platform/agents?tab=skills`) const row = page.locator('.instruction-row').filter({ hasText: '课程写作规则与案例衔接方法' }) await row.getByRole('button', { name: '解除挂载', exact: true }).waitFor() await row.getByRole('button', { name: '从 技能验收助手 解除挂载 课程写作规则与案例衔接方法', exact: true }).click() const dialog = page.locator('.skills-mount-dialog:visible') check('skill_chip_preselects_only_mounted_agent', await dialog.locator('.mount-agent').count() === 1 && await dialog.locator('.mount-agent.is-selected').count() === 1) await page.screenshot({ path: `${directory}/instruction-skills-remove-desktop.png`, animations: 'disabled' }) await page.setViewportSize({ width: 390, height: 844 }) await page.screenshot({ path: `${directory}/instruction-skills-remove-mobile.png`, animations: 'disabled' }) check('unmount_dialog_fits_mobile', await dialog.evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 && r.top >= -1 && r.bottom <= innerHeight + 1 })) await dialog.getByRole('button', { name: '确认解除', exact: true }).click() await dialog.waitFor({ state: 'hidden' }) check('skill_row_updates_after_unmount', await row.locator('.mounted-agent-chip').count() === 0) check('only_fixture_writes_attempted', writes.every(write => !write.blocked)) check('no_browser_runtime_errors', errors.length === 0) } catch (error) { failures.push(error.message) await browser.contexts()[0]?.pages()[0]?.screenshot({ path: `${directory}/instruction-skills-ui-failure.png` }) } finally { await browser.close() const report = { checked_at: new Date().toISOString(), origin, api_origin: apiOrigin, checks, errors, failures, fixtures_and_writes_mocked: true, passed: !failures.length && !errors.length } fs.writeFileSync(`${directory}/instruction-skills-ui.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 })