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 directory = '/Volumes/Extreme SSD/yawan-storage/weknora/acceptance' 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' 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 = [], blockedWrites = [], writes = [] 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) const fixture = (id, kind, name, data) => ({ id, kind, name, data, tenant_id: 10059, owner_id: auth.user.id, owner_name: 'UI verification', status: kind === 'training-maps' ? 'confirmed' : 'draft', version: 1, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }) const map = fixture('duration-map', 'training-maps', '时长验证导图', { days: [{ day: 1, title: '基础与实践' }], agent_id: 'duration-agent' }) const flow = fixture('duration-flow', 'flows', '时长验证画布', { nodes: [{ id: 'node', label: '内容作者', agent_id: 'duration-agent' }], edges: [] }) let task = null // All business writes below are in-memory fixtures; no task or AI job reaches the API. await context.route('**/api/v1/**', async route => { const request = route.request(), path = new URL(request.url()).pathname, method = request.method() const ok = body => route.fulfill({ status: 200, json: { success: true, ...body } }) if (path === '/api/v1/access/me') return ok({ data: { administrator: false, restricted: true, permissions: ['content.read', 'content.write', 'flows.read'] } }) const match = path.match(/^\/api\/v1\/workbench\/resources\/(content-tasks|training-maps|flows)(?:\/([^/]+))?(?:\/(jobs|actions\/.*))?$/) if (match) { const [, kind, id, action] = match if (method === 'GET' && action === 'jobs') return ok({ data: [], total: 0 }) if (method === 'GET') { const items = kind === 'training-maps' ? [map] : kind === 'flows' ? [flow] : task ? [task] : [] return ok(id ? { data: items.find(item => item.id === id) } : { data: items, total: items.length }) } if (kind === 'content-tasks' && !action && ['POST', 'PUT'].includes(method)) { const body = request.postDataJSON() writes.push({ method, data: body.data }) task = { ...fixture('duration-task', kind, body.name, body.data), version: (task?.version || 0) + 1 } return ok({ data: task }) } } if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) { blockedWrites.push({ path, method }) return route.fulfill({ status: 409, json: { error: 'Verification prevents live writes' } }) } return route.continue() }) const page = await context.newPage() page.on('pageerror', error => errors.push(error.message)) await page.goto(`${origin}/platform/workbench/content?tab=tasks`) await page.getByRole('button', { name: '创建内容任务', exact: true }).click() const drawer = page.locator('.t-drawer--open') const duration = drawer.locator('[aria-label="直播时长(分钟)"] input') await duration.waitFor() check('new_task_defaults_to_60_minutes', await duration.inputValue() === '60') await drawer.locator('.t-form__item').filter({ has: page.locator('.t-form__label').filter({ hasText: /^名称$/ }) }).locator('input').fill('直播时长验证任务') await duration.fill('135') await duration.press('Tab') await drawer.locator('.t-form__item').filter({ hasText: '已确认的训练营导图' }).locator('.t-select').click() await page.getByText('时长验证导图', { exact: true }).last().click() await drawer.locator('.t-form__item').filter({ hasText: '画布模板' }).locator('.t-select').click() await page.getByText('时长验证画布', { exact: true }).last().click() await page.screenshot({ path: `${directory}/content-duration-desktop.png`, animations: 'disabled' }) await drawer.getByRole('button', { name: '保存草稿', exact: true }).click() await page.getByText('直播时长:135 分钟', { exact: true }).waitFor() check('draft_payload_and_detail_keep_duration', writes[0]?.data.duration_minutes === 135) await page.getByRole('button', { name: '编辑配置', exact: true }).click() await duration.waitFor() check('editing_reopens_saved_duration', await duration.inputValue() === '135') await page.setViewportSize({ width: 390, height: 844 }) await duration.scrollIntoViewIfNeeded() await page.screenshot({ path: `${directory}/content-duration-mobile.png`, animations: 'disabled' }) check('duration_control_fits_mobile', await duration.evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= 0 && r.right <= innerWidth })) check('drawer_fits_mobile', await drawer.locator('.t-drawer__content-wrapper').evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 })) await duration.fill('225') await duration.press('Tab') await drawer.getByRole('button', { name: '保存配置', exact: true }).click() await page.getByText('直播时长:225 分钟', { exact: true }).waitFor() check('edited_duration_saved', writes.at(-1)?.data.duration_minutes === 225) check('no_live_business_or_ai_writes', blockedWrites.length === 0 && writes.length === 2) check('no_runtime_errors', errors.length === 0) fs.writeFileSync(`${directory}/content-duration-ui.json`, JSON.stringify({ origin, checks, errors, businessWrites: 'mocked in browser only' }, null, 2)) console.log(JSON.stringify({ checks, errors }, null, 2)) } catch (error) { console.error(error.stack) process.exitCode = 1 } finally { await browser.close() } } main().catch(error => { console.error(error.stack); process.exitCode = 1 })