Files
xinmi/scripts/verify_instruction_skills_ui.cjs
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

127 lines
11 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 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 })