WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
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
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
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const http = require('node:http')
|
||||
const { isDeepStrictEqual } = require('node:util')
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
||||
|
||||
const directory = path.resolve(__dirname, '../../acceptance')
|
||||
const base = 'http://127.0.0.1:18080/api/v1'
|
||||
const db = 'yawan_native_acceptance_20260915'
|
||||
const sql = statement => execFileSync('docker', ['--context', 'colima-yawan', 'exec', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', db, '-v', 'ON_ERROR_STOP=1', '-Atc', statement], {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).trim()
|
||||
const literal = value => "'" + String(value).replaceAll("'", "''") + "'"
|
||||
const checks = [], fixtureIDs = [], errors = []
|
||||
let auth, author, model, visionModel, browser, page, stage = 'login', imageCalls = 0
|
||||
const check = (name, pass) => { checks.push({name, passed: !!pass}); assert.ok(pass, name) }
|
||||
const resource = r => `/workbench/resources/${r.kind}/${r.id}`
|
||||
const days = [
|
||||
{day: 1, title: '建立判断依据', prerequisite: '', hook: '如何应用判断依据', nodes: [{id: 'n1', label: '概念框架', children: [{id: 'n1a', label: '基本判断'}, {id: 'n1b', label: '应用边界'}]}]},
|
||||
{day: 2, title: '实践与复盘', prerequisite: '掌握第一天的基本判断', hook: '如何独立迁移', nodes: [{id: 'n2', label: '实践框架', children: [{id: 'n2a', label: '案例练习'}, {id: 'n2b', label: '复盘问题'}]}]},
|
||||
]
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
let text = ''; for await (const part of req) text += part
|
||||
const body = JSON.parse(text), system = typeof body.messages[0].content === 'string' ? body.messages[0].content : '', prompt = body.messages.at(-1).content
|
||||
let content
|
||||
if (system.includes('Markdown 节点框架')) content = JSON.stringify({framework: '- 判断依据\n - 识别条件\n - 练习验证'})
|
||||
else if (system.includes('课程架构师')) {
|
||||
const output = structuredClone(days)
|
||||
if (String(prompt).includes('本次批注只修改节点 n1a')) output[0].nodes[0].children[0].label = '更新后的基本判断'
|
||||
content = JSON.stringify({days: String(prompt).includes('只修改第 1 天') ? [output[0]] : output})
|
||||
} else if (system.includes('内容编排师')) content = JSON.stringify({segments: [
|
||||
{id: 's1', title: '建立判断', agent_id: author.id},
|
||||
{id: 's2', title: '可选案例', agent_id: author.id},
|
||||
{id: 's3', title: '迁移练习', agent_id: author.id},
|
||||
]})
|
||||
else if (system.includes('跨稿质检员')) content = JSON.stringify({passed: true, summary: '前后判断依据一致,承接已采用段落。', issues: []})
|
||||
else if (system.includes('成稿编辑')) content = '先建立判断依据,再完成迁移练习。'
|
||||
else if (Array.isArray(prompt)) { imageCalls++; content = '第一部分:建立判断。第二部分:迁移练习。' }
|
||||
else content = '先建立判断依据,再通过案例验证并进入迁移练习。'
|
||||
await new Promise(resolve => setTimeout(resolve, 900))
|
||||
res.writeHead(200, {'Content-Type': 'application/json'})
|
||||
res.end(JSON.stringify({id: 'parity', model: 'parity-model', choices: [{index: 0, message: {role: 'assistant', content}, finish_reason: 'stop'}], usage: {prompt_tokens: 30, completion_tokens: 30, total_tokens: 60}}))
|
||||
} catch { res.writeHead(500); res.end('{}') }
|
||||
})
|
||||
async function request(route, method = 'GET', body, expected = 200) {
|
||||
const response = await fetch(base + route, {method, headers: {'Content-Type': 'application/json', ...(auth ? {Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059'} : {})}, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30000)})
|
||||
assert.equal(response.status, expected, `${stage}: ${method} ${route} HTTP ${response.status}`)
|
||||
return response.json()
|
||||
}
|
||||
async function create(kind, name, data) {
|
||||
const item = (await request(`/workbench/resources/${kind}`, 'POST', {name, data}, 201)).data
|
||||
fixtureIDs.push(item.id); return item
|
||||
}
|
||||
async function action(item, name, args = {}) {
|
||||
stage = `action_${name}`
|
||||
const job = (await request(`${resource(item)}/actions/${name}`, 'POST', {version: item.version, ...args}, 202)).data
|
||||
const start = Date.now()
|
||||
let current
|
||||
do {
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
current = (await request(`/workbench/jobs/${job.id}`)).data
|
||||
} while (['queued', 'running'].includes(current.status) && Date.now() - start < 45000)
|
||||
check(`${name}_job_succeeded`, current.status === 'succeeded')
|
||||
return (await request(resource(item))).data
|
||||
}
|
||||
async function visit(route) {
|
||||
await page.goto('http://127.0.0.1:15173' + route)
|
||||
await page.locator('.workbench-resource').first().waitFor({timeout: 30000})
|
||||
}
|
||||
async function main() {
|
||||
const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8'))
|
||||
auth = await request('/auth/login', 'POST', {email: credentials.email, password: credentials.password})
|
||||
const snapshot = () => sql("SELECT md5(coalesce(jsonb_agg(to_jsonb(r) ORDER BY id),'[]')::text) FROM workbench_resources r")
|
||||
const originals = snapshot()
|
||||
await new Promise(resolve => server.listen(18081, '0.0.0.0', resolve))
|
||||
try {
|
||||
stage = 'create_fixtures'
|
||||
model = (await request('/models', 'POST', {name: `parity-${Date.now()}`, type: 'KnowledgeQA', source: 'remote', parameters: {base_url: 'http://host.docker.internal:18081/v1', api_key: 'acceptance-fixture', provider: 'generic'}}, 201)).data
|
||||
visionModel = (await request('/models', 'POST', {name: `parity-vision-${Date.now()}`, type: 'VLLM', source: 'remote', parameters: {base_url: 'http://host.docker.internal:18081/v1', api_key: 'acceptance-fixture', provider: 'generic'}}, 201)).data
|
||||
author = (await request('/agents', 'POST', {name: '迁移回归测试作者', config: {model_id: model.id, vlm_model_id: visionModel.id, agent_mode: 'quick-answer', kb_selection_mode: 'none'}}, 201)).data
|
||||
const flow = await create('flows', '迁移回归测试画布', {nodes: [{id: 'writer', agent_id: author.id, label: '内容作者'}], edges: []})
|
||||
let map = await create('training-maps', '迁移回归测试导图', {topic: '学习方法', days_count: 0, days_auto: true, agent_id: author.id})
|
||||
check('days_optional_on_create', map.data.days_count === 0)
|
||||
map = await action(map, 'generate')
|
||||
check('ai_recommended_days_saved', map.data.days_count === 2 && map.data.days.length === 2)
|
||||
const otherDay = JSON.stringify(map.data.days[1])
|
||||
map = await action(map, 'revise', {day: 1, node_id: 'n1a', quote: '基本判断', advice: '补充判断条件'})
|
||||
check('node_revision_is_scoped', map.data.days[0].nodes[0].children[0].label === '更新后的基本判断' && JSON.stringify(map.data.days[1]) === otherDay && map.data.days[0].nodes[0].children[1].label === '应用边界')
|
||||
map = await action(map, 'confirm')
|
||||
map = await action(map, 'generate-day', {day: 2})
|
||||
map = await action(map, 'confirm-day', {day: 2})
|
||||
let task = await create('content-tasks', '迁移回归测试任务', {map_id: map.id, day: 2, flow_id: flow.id})
|
||||
check('task_inherits_canvas_agent', task.data.agent_id === author.id)
|
||||
task = await action(task, 'decompose')
|
||||
task = await action(task, 'generate', {segment_id: 's1'})
|
||||
task = await action(task, 'accept', {segment_id: 's1'})
|
||||
task = await action(task, 'skip', {segment_id: 's2'})
|
||||
task = await action(task, 'generate', {segment_id: 's3'})
|
||||
task = await action(task, 'accept', {segment_id: 's3'})
|
||||
task = await action(task, 'review')
|
||||
task = await action(task, 'compose')
|
||||
check('complete_pipeline_output_visible', task.status === 'completed' && !!task.data.review && !!task.data.composed)
|
||||
const textTask = await create('content-tasks', '手动导入回归测试', {source_text: '基础概念、练习、复盘', flow_id: flow.id})
|
||||
await action(textTask, 'decompose')
|
||||
let imageTask = await create('content-tasks', '导图图片回归测试', {source_image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jv1sAAAAASUVORK5CYII=', flow_id: flow.id})
|
||||
imageTask = await action(imageTask, 'import-image')
|
||||
check('image_recognition_then_decomposition', imageCalls === 1 && imageTask.data.source_text.includes('第一部分') && imageTask.data.segments.length === 3)
|
||||
imageTask = await action(imageTask, 'import-image')
|
||||
check('image_retry_refreshes_previous_ocr_text', imageCalls === 2)
|
||||
|
||||
stage = 'legacy_fixture'
|
||||
const legacy = await create('training-maps', '迁移配置回归测试', {topic: '历史主题', days_count: 2, agent_id: author.id})
|
||||
sql(`UPDATE workbench_resources SET status='needs_configuration', data=${literal(JSON.stringify({...legacy.data, agent_id: 'missing-agent', days, legacy_import: {read_only: true, needs_configuration: true}, legacy_history: {original: 'preserved'}}))}::jsonb WHERE id=${literal(legacy.id)}`)
|
||||
let imported = (await request(resource(legacy))).data
|
||||
imported = (await request(resource(imported), 'PUT', {name: '历史导图重命名测试', data: imported.data, version: imported.version})).data
|
||||
check('imported_rename_without_execution_config', imported.name === '历史导图重命名测试' && !imported.data.legacy_activation)
|
||||
const beforeDays = JSON.stringify(imported.data.days)
|
||||
imported = (await request(resource(imported), 'PUT', {name: imported.name, version: imported.version, configure: true, data: {...imported.data, agent_id: author.id}})).data
|
||||
const preservedDays = rows => rows.map(({confirmed, ...day}) => ({framework: '', ...day}))
|
||||
check('imported_configured_without_losing_nodes', imported.data.legacy_activation?.configured && isDeepStrictEqual(preservedDays(imported.data.days), preservedDays(JSON.parse(beforeDays))) && imported.data.legacy_history.original === 'preserved')
|
||||
|
||||
stage = 'browser'
|
||||
browser = await chromium.launch({headless: true, channel: 'chrome'})
|
||||
const context = await browser.newContext({viewport: {width: 1440, height: 1000}})
|
||||
await context.addInitScript(a => {
|
||||
const values = {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)}
|
||||
for (const [key, value] of Object.entries(values)) localStorage.setItem(key, value)
|
||||
localStorage.setItem('weknora:new-user-guide-done:v1', '1')
|
||||
}, auth)
|
||||
page = await context.newPage(); page.setDefaultTimeout(15000)
|
||||
page.on('pageerror', () => errors.push('page_error'))
|
||||
await visit(`/platform/workbench/content?tab=maps&resource=${map.id}`)
|
||||
await page.getByRole('button', {name: '全屏查看', exact: true}).click()
|
||||
await page.locator('.map-overview__canvas').waitFor()
|
||||
check('full_map_all_days_visible', await page.locator('.map-overview__day').count() === 2)
|
||||
await page.screenshot({path: path.join(directory, 'parity-map-desktop.png'), animations: 'disabled'})
|
||||
await page.locator('.map-overview').getByRole('button', {name: '更新后的基本判断', exact: true}).click({button: 'right'})
|
||||
const correction = page.locator('.t-dialog:visible').filter({hasText: '提交给 AI 修改'})
|
||||
check('node_comment_quotes_selected_node', await correction.locator('blockquote').innerText() === '更新后的基本判断')
|
||||
await correction.locator('textarea').fill('保持先修关系,完善表述')
|
||||
await correction.getByRole('button', {name: '提交给 AI 修改', exact: true}).click()
|
||||
await page.locator('.t-dialog:visible .generation-status .t-progress').first().waitFor()
|
||||
check('node_revision_progress_visible', true)
|
||||
await page.locator('.t-dialog:visible .generation-status').getByText('已完成', {exact: true}).first().waitFor({timeout: 30000})
|
||||
await page.setViewportSize({width: 390, height: 844})
|
||||
await page.waitForFunction(() => { const r = document.querySelector('.t-dialog')?.getBoundingClientRect(); return r && r.left >= 0 && r.right <= innerWidth + 2 }, undefined, {polling: 100})
|
||||
await page.screenshot({path: path.join(directory, 'parity-map-mobile.png'), animations: 'disabled'})
|
||||
check('mobile_page_no_overflow', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2))
|
||||
await page.locator('.t-dialog:visible .t-dialog__close').click()
|
||||
await page.setViewportSize({width: 1440, height: 1000})
|
||||
await page.getByRole('button', {name: '新建训练营导图', exact: true}).click()
|
||||
const drawer = page.locator('.t-drawer--open')
|
||||
await drawer.getByText('AI 推荐', {exact: true}).waitFor()
|
||||
check('days_default_to_ai_recommendation', await drawer.locator('.t-input-number').count() === 0)
|
||||
await drawer.locator('.knowledge-scope > .t-select__wrap').click()
|
||||
await page.locator('.t-select-option').filter({hasText: '销售逻辑'}).click()
|
||||
await drawer.getByText('知识库与分类', {exact: true}).click()
|
||||
await drawer.locator('.knowledge-scope__row .t-tree-select').click()
|
||||
await page.locator('.knowledge-scope-popup .t-tree__item').first().waitFor()
|
||||
check('knowledge_categories_render_tree', await page.locator('.knowledge-scope-popup .t-tree__item').count() > 1)
|
||||
await page.screenshot({path: path.join(directory, 'parity-map-categories.png'), animations: 'disabled'})
|
||||
stage = 'close_map_drawer'
|
||||
await drawer.getByRole('button', {name: '取消', exact: true}).click()
|
||||
stage = 'task_creation_browser'
|
||||
await visit(`/platform/workbench/content?tab=tasks&resource=${task.id}`)
|
||||
await page.getByRole('button', {name: '创建内容任务', exact: true}).click()
|
||||
check('task_form_has_no_redundant_agent_or_kb', await drawer.locator('.knowledge-scope').count() === 0 && !(await drawer.innerText()).includes('内容编排 Agent'))
|
||||
await drawer.getByText('导入文字或图片', {exact: true}).click()
|
||||
check('manual_image_upload_available', await drawer.locator('input[type=file]').getAttribute('accept') === 'image/png,image/jpeg,image/webp')
|
||||
await drawer.getByRole('button', {name: '取消', exact: true}).click()
|
||||
await visit(`/platform/workbench/content?tab=maps&resource=${imported.id}`)
|
||||
await page.getByRole('button', {name: '编辑配置', exact: true}).click()
|
||||
check('imported_edit_dialog_works', await drawer.locator('textarea').first().inputValue() === '历史主题')
|
||||
await drawer.getByRole('button', {name: '取消', exact: true}).click()
|
||||
check('browser_no_javascript_errors', errors.length === 0)
|
||||
stage = 'delete'
|
||||
await request(`${resource(imported)}?detach=true`, 'DELETE')
|
||||
await request(resource(imported), 'GET', undefined, 404)
|
||||
check('imported_delete_persists', true)
|
||||
} finally {
|
||||
if (page && !page.isClosed()) await page.screenshot({path: path.join(directory, 'parity-debug.png'), animations: 'disabled'})
|
||||
await browser?.close()
|
||||
if (fixtureIDs.length) {
|
||||
const ids = fixtureIDs.map(literal).join(',')
|
||||
sql(`BEGIN; DELETE FROM workbench_jobs WHERE resource_id IN (${ids}); DELETE FROM workbench_events WHERE resource_id IN (${ids}); DELETE FROM workbench_revisions WHERE resource_id IN (${ids}); DELETE FROM workbench_resources WHERE id IN (${ids}); COMMIT;`)
|
||||
}
|
||||
if (author) await request(`/agents/${author.id}`, 'DELETE')
|
||||
if (visionModel) await request(`/models/${visionModel.id}`, 'DELETE')
|
||||
if (model) await request(`/models/${model.id}`, 'DELETE')
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
check('existing_resources_unchanged', snapshot() === originals)
|
||||
fs.writeFileSync(path.join(directory, 'migration-parity-checks.json'), JSON.stringify({stage, checks}, null, 2))
|
||||
}
|
||||
console.log(JSON.stringify({checks, passed: checks.every(c => c.passed)}))
|
||||
}
|
||||
main().catch(error => { console.error(`${stage}: ${error instanceof assert.AssertionError ? error.message : String(error.message).split('\n')[0]}`); server.close(); process.exitCode = 1 })
|
||||
Reference in New Issue
Block a user