121 lines
8.3 KiB
JavaScript
121 lines
8.3 KiB
JavaScript
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 })
|