Files
xinmi/scripts/verify_memory_ui.cjs
T

145 lines
12 KiB
JavaScript
Raw Normal View History

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 login = 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(login.status, 200)
const auth = await login.json()
const browser = await chromium.launch({ headless: true, channel: 'chrome' })
const checks = [], errors = [], writes = [], blockedWrites = []
const check = (name, result) => { checks.push({ name, passed: !!result }); assert.ok(result, name) }
try {
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } })
await context.addInitScript(a => {
for (const [key, value] of Object.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', 'weknora:contextual-guide-agent-list:v1': '1',
})) localStorage.setItem(key, value)
}, auth)
let failList = true
let recoverySettings = { write_mode: 'auto', extraction_pending: false, extraction_exhausted: false, extraction_failures: 0, extraction_failure_limit: 8 }
let extractionRetries = 0
const now = new Date().toISOString()
const candidate = { id: 'memory-check', agent_id: '', status: 'pending', content: '请用清晰简短的表达。'.repeat(35), created_at: now }
const legacy = { id: 'legacy-check', owner_id: auth.user.id, agent_id: '', status: 'accepted', content: '历史采纳记录,尚未写入当前记忆库。', legacy_history_id: 'history-check', can_reconfirm: true, created_at: now }
const unavailable = { ...legacy, id: 'unavailable-check', content: '原 Agent 已删除的历史记录。', can_reconfirm: false, reconfirmation_unavailable_reason: '原 Agent 已删除,无法重新确认。' }
const linked = { ...legacy, id: 'linked-check', content: '已重新确认的历史记录。', can_reconfirm: false, native_memory_id: 'linked-native', reconfirmed_content: '采用简洁表达。', reconfirmed_at: now }
const item = { id: 'memory-item', kind: 'preference', content: '已成功重新加载的记忆', status: 'active', origin: 'manual', importance: 3, created_at: now }
// Only authentication reaches the live server as a write; all review writes stay in these fixtures.
await context.route('**/api/v1/**', async route => {
const request = route.request(), url = new URL(request.url()), path = 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: ['agents.read', 'agents.memory'] } })
if (path === '/api/v1/memory/settings') return ok({ data: { workspace_enabled: true, user_enabled: true, effective: true, item_count: 1, max_items: 200, ...recoverySettings } })
if (path === '/api/v1/memory/extraction/retry' && method === 'POST') {
extractionRetries++
recoverySettings = { ...recoverySettings, extraction_exhausted: false, extraction_failures: 0 }
return ok({})
}
if (path === '/api/v1/memory/items') {
if (failList && url.searchParams.get('limit') === '20') return route.fulfill({ status: 503, json: { error: { message: '记忆服务暂不可用,请刷新重试' } } })
return ok({ data: url.searchParams.get('status') === 'active' ? [item] : [], total: url.searchParams.get('status') === 'active' ? 1 : 0 })
}
if (['/api/v1/memory/topics', '/api/v1/memory/documents'].includes(path)) return ok({ data: [], total: 0 })
if (path === '/api/v1/workbench/operations/records/memories') {
const rows = [candidate, legacy, unavailable, linked].filter(row => row.status === url.searchParams.get('status'))
return ok({ data: rows, total: rows.length })
}
if (path === '/api/v1/workbench/operations/memories/memory-check' && method === 'POST') {
const body = request.postDataJSON()
writes.push(body)
Object.assign(candidate, body)
return ok({ data: candidate })
}
if (path === '/api/v1/workbench/operations/memories/legacy-check' && method === 'POST') {
const body = request.postDataJSON()
writes.push(body)
Object.assign(legacy, { native_memory_id: 'new-native', reconfirmed_content: body.content, reconfirmed_at: now, can_reconfirm: false })
return ok({ data: legacy })
}
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/agents?tab=memory`)
const memory = page.locator('.memory-settings')
await memory.locator('.t-alert--error').waitFor()
check('failure_is_not_an_empty_memory_state', await memory.locator('.empty').count() === 0)
await page.screenshot({ path: `${directory}/memory-error-desktop.png`, animations: 'disabled' })
failList = false
await memory.getByRole('button', { name: '刷新', exact: true }).click()
await memory.getByText('已成功重新加载的记忆', { exact: true }).waitFor()
check('refresh_recovers_memory_list', await memory.locator('.t-alert--error').count() === 0)
await page.locator('.pending-memories').getByRole('button', { name: '审核', exact: true }).click()
const dialog = page.locator('.t-dialog:visible')
const text = dialog.locator('textarea')
await text.waitFor()
check('review_textarea_has_visible_label', await text.evaluate(element => Array.from(element.labels || []).some(label => label.textContent.includes('记忆内容'))))
check('long_candidate_not_truncated', await text.inputValue() === candidate.content)
check('long_candidate_cannot_be_accepted', await dialog.getByRole('button', { name: '确认并存入记忆' }).isDisabled())
check('long_candidate_can_be_rejected', await dialog.getByRole('button', { name: '拒绝', exact: true }).isEnabled())
await page.setViewportSize({ width: 390, height: 844 })
await page.screenshot({ path: `${directory}/memory-review-mobile.png`, animations: 'disabled' })
check('review_fits_mobile', await dialog.evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 && r.top >= 0 && r.bottom <= innerHeight + 1 }))
await text.fill('采用简短清晰的表达,先给结论,再补充例子。')
await dialog.getByRole('button', { name: '确认并存入记忆' }).click()
await dialog.waitFor({ state: 'hidden' })
check('edited_candidate_is_accepted', writes.length === 1 && writes[0].status === 'accepted' && writes[0].content.length < 300)
await page.setViewportSize({ width: 1440, height: 1000 })
await page.locator('.pending-toolbar .t-select').click()
await page.getByText('已采纳', { exact: true }).last().click()
const historicalRow = page.locator('.pending-row').filter({ hasText: '历史采纳记录,尚未写入当前记忆库。' })
await historicalRow.getByRole('button', { name: '重新确认', exact: true }).waitFor()
check('only_eligible_legacy_history_has_reconfirmation', await page.locator('.pending-memories').getByRole('button', { name: '重新确认', exact: true }).count() === 1)
check('unavailable_history_and_reason_remain_visible', await page.getByText('原 Agent 已删除,无法重新确认。', { exact: true }).isVisible())
await historicalRow.getByRole('button', { name: '重新确认', exact: true }).click()
await dialog.getByText('重新确认历史记忆', { exact: true }).waitFor()
check('legacy_cannot_be_rejected_or_accepted_without_consent', await dialog.getByRole('button', { name: '拒绝', exact: true }).count() === 0 && await dialog.getByRole('button', { name: '重新确认并存入记忆', exact: true }).isDisabled())
await text.fill('先给结论,再用简短案例说明。')
await page.screenshot({ path: `${directory}/memory-legacy-confirmation-desktop.png`, animations: 'disabled' })
await page.setViewportSize({ width: 390, height: 844 })
await page.screenshot({ path: `${directory}/memory-legacy-confirmation-mobile.png`, animations: 'disabled' })
check('legacy_confirmation_fits_mobile', await dialog.evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 && r.top >= 0 && r.bottom <= innerHeight + 1 && element.scrollWidth <= element.clientWidth + 1 }))
await dialog.locator('.review-consent').click()
await dialog.getByRole('button', { name: '重新确认并存入记忆', exact: true }).click()
await dialog.waitFor({ state: 'hidden' })
check('legacy_confirmation_sends_explicit_consent', writes.length === 2 && writes[1].reconfirm_legacy === true && writes[1].confirm_personal_scope === true && writes[1].content === '先给结论,再用简短案例说明。')
await historicalRow.getByText('已重新确认', { exact: true }).waitFor()
check('reconfirmed_history_keeps_original_and_final_wording', legacy.content === '历史采纳记录,尚未写入当前记忆库。' && (await historicalRow.innerText()).includes('先给结论,再用简短案例说明。') && await historicalRow.getByRole('button', { name: '重新确认', exact: true }).count() === 0)
recoverySettings = { ...recoverySettings, extraction_pending: true, extraction_exhausted: true, extraction_failures: 8 }
await memory.getByRole('button', { name: '刷新', exact: true }).click()
const recovery = memory.locator('.extraction-recovery')
await recovery.getByRole('button', { name: '重新尝试', exact: true }).waitFor()
check('exhausted_state_explains_retained_work', (await recovery.innerText()).includes('自动重试已停止') && (await recovery.innerText()).includes('待处理聊天记录已保留'))
await recovery.scrollIntoViewIfNeeded()
await page.screenshot({ path: `${directory}/memory-recovery-mobile.png`, animations: 'disabled' })
check('recovery_controls_fit_mobile', await recovery.evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 && element.scrollWidth <= element.clientWidth + 1 }))
await page.setViewportSize({ width: 1440, height: 1000 })
await page.screenshot({ path: `${directory}/memory-recovery-desktop.png`, animations: 'disabled' })
await recovery.getByRole('button', { name: '重新尝试', exact: true }).click()
await recovery.getByText('仍有聊天记录等待提取,后台将继续处理。', { exact: true }).waitFor()
check('explicit_retry_refreshes_status_and_hides_button', extractionRetries === 1 && await recovery.getByRole('button', { name: '重新尝试', exact: true }).count() === 0)
check('no_live_business_writes', blockedWrites.length === 0)
check('no_runtime_errors', errors.length === 0)
fs.writeFileSync(`${directory}/memory-ui.json`, JSON.stringify({ origin, checks, errors, businessWrites: 'mocked in browser only' }, null, 2))
console.log(JSON.stringify({ checks, errors }, null, 2))
} finally {
await browser.close()
}
}
main().catch(error => { console.error(error.stack); process.exitCode = 1 })