Files
jerryandClaude Fable 5 0875bb036c P0/M1: 四部分重构——frontend→ui、backend→sdk 改名 + 引用修复 + 骨架
- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
  历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
  单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
  tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
  生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:56:47 +08:00

139 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// deploy/m5_acceptance.js —— M5 PM 最终验收脚本(在远端运行)
// 与 M2/M3 验收互补:本轮跑「生产形态静态托管」+「加固后的 Vite」双链路,
// 并在三档视口执行 PRD 验收脚本的核心交互(PM 人工走查的自动化部分)。
const { chromium } = require('playwright');
const results = [];
function check(name, ok, extra = '') {
results.push({ name, ok });
console.log(`${ok ? ' ✅' : ' ❌'} ${name}${extra ? ' —— ' + extra : ''}`);
}
async function waitFor(fn, ms, iv = 700, label = '') {
const t0 = Date.now();
while (Date.now() - t0 < ms) {
try { const v = await fn(); if (v) return v; } catch {}
await new Promise((r) => setTimeout(r, iv));
}
throw new Error(`等待超时(${ms}ms): ${label}`);
}
(async () => {
const browser = await chromium.launch();
// ============ 0. 生产形态静态托管(BFF 8090 直接服务 dist)============
{
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
// BFF 静态托管需 ui/dist 存在且 BFF 重启——先探测是否已挂载
const prod = await page.goto('http://127.0.0.1:8090/', { waitUntil: 'domcontentloaded' }).then(() => true).catch(() => false);
if (prod) {
await waitFor(() => page.locator('.app-shell').count(), 15000, 500, '生产形态 AppShell');
check('生产形态:BFF 静态托管渲染 AppShell', true);
const health = await page.evaluate(async () => (await fetch('/api/health')).status);
check('生产形态:同源 /api/health 可达', health === 200);
} else {
// dist 未同步或 BFF 未带静态挂载重启——记失败但继续
check('生产形态:BFF 静态托管', false, '8090 根路径不可达(dist 未部署?)');
}
check('生产形态无 JS 错误', errors.length === 0, errors.slice(0, 2).join('|'));
await ctx.close();
}
// ============ 1. Desktop 验收脚本(PRD 核心交互)============
{
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.goto('http://127.0.0.1:5180', { waitUntil: 'domcontentloaded' });
await waitFor(() => page.locator('.app-shell').count(), 15000, 500, 'AppShell');
// 会话列表来自真实 SQLite
const sessCount = await page.locator('.session-item').count();
check('桌面:会话列表从真实 SQLite 加载', sessCount >= 1, `${sessCount} 条`);
// 打开一个有 done 状态的会话验证历史恢复(含 markdown 渲染)
const doneItem = page.locator('.session-item').filter({ has: page.locator('.status-dot.done') }).first();
if (await doneItem.count()) {
await doneItem.click();
await waitFor(() => page.locator('.msg.assistant').count(), 15000, 500, '历史消息');
const hasMd = await page.locator('.msg.assistant .md-text strong, .msg.assistant .md-text li, .msg.assistant .md-text h1, .msg.assistant .md-text h2').count();
const hasPlain = await page.locator('.msg.assistant .md-text').count();
check('桌面:历史答案渲染(markdown 块存在)', hasPlain > 0, `md-text ${hasPlain} / 加粗列表 ${hasMd}`);
// 相位卡恢复为 done
const steps = await page.locator('.pipeline-card .p-step.done').count();
check('桌面:历史相位卡恢复 done', steps >= 4, `${steps}/5`);
}
// 引用 chip 高亮链(FR-CHAT-015 补全的验收)
const chip = page.locator('.msg.assistant .inline-sources .src-chip').first();
if (await chip.count()) {
await chip.click();
await waitFor(() => page.locator('.bench-tab.on').filter({ hasText: '文献来源' }).count(), 5000, 400, '来源 tab 切换');
await waitFor(() => page.locator('.source-row.hl-active').count(), 5000, 400, '来源高亮');
check('桌面:引用 chip → 来源 tab 高亮定位', true);
} else {
check('桌面:引用 chip → 来源 tab 高亮定位', false, '当前会话无引用 chip(可接受,需人工复核一次问答)');
}
check('桌面验收无 JS 错误', errors.length === 0, errors.slice(0, 3).join('|'));
await ctx.close();
}
// ============ 2. 完整真实问答(桌面,加固后链路)============
{
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.goto('http://127.0.0.1:5180', { waitUntil: 'domcontentloaded' });
await waitFor(() => page.locator('.app-shell').count(), 15000, 500, 'AppShell');
await page.click('.new-task-btn');
await waitFor(() => page.locator('.composer textarea').count(), 15000, 500, '作曲器');
await page.fill('.composer textarea', '拟南芥HD-ZIP转录因子的生物学功能?');
await page.click('.send-btn');
// 等流结束(真实 QA 最长 92s,终态等待上限 360s 已加固)
await waitFor(async () => {
const ok = await page.evaluate(() => {
const rootEl = document.querySelector('#app');
let n = rootEl; let app = null;
while (n && !app) { if (n.__vue_app__) app = n.__vue_app__; n = n.__vueParentComponent?.parent || null; }
if (!app) return false;
const chat = app.config.globalProperties.$pinia._s.get('chat');
return chat && !chat.busy && chat.completedOnce;
});
return ok;
}, 900000, 1500, '真实问答流结束'); // 2026-08-28:长文(5000字) + 高负载下单次问答可达 10 分钟
await waitFor(async () => {
const n = await page.locator('.msg.assistant .md-text').count();
return n > 0;
}, 30000, 800, '最终答案出现');
const answerLen = (await page.locator('.msg.assistant .md-text').last().textContent()).length;
check('真实问答:五相位终态 + 答案渲染', answerLen > 20, `答案 ${answerLen} 字`);
const allDone = (await page.locator('.pipeline-card .p-step.done').count()) >= 4;
check('真实问答:相位卡全部 done', allDone);
const srcChip = await page.locator('.msg.assistant .inline-sources .src-chip').count();
check('真实问答:内联引用 chip 出现', srcChip > 0, `${srcChip} 个`);
check('真实问答无 JS 错误', errors.length === 0, errors.slice(0, 3).join('|'));
await ctx.close();
}
// ============ 3. 三档视口布局终检 ============
for (const [name, vp] of [['mobile', { width: 390, height: 844 }], ['tablet', { width: 768, height: 1024 }], ['desktop', { width: 1440, height: 900 }]]) {
const ctx = await browser.newContext({ viewport: vp });
const page = await ctx.newPage();
await page.goto('http://127.0.0.1:5180', { waitUntil: 'domcontentloaded' });
await waitFor(() => page.locator('.app-shell').count(), 15000, 500, `${name} AppShell`);
const tier = await page.evaluate(() => document.querySelector('.app-shell').className);
check(`${name}:档位 class 正确`, tier.includes(name === 'desktop' ? 'app-shell' : name === 'tablet' ? 'drawers-mode' : 'mobile'), tier.slice(0, 60));
await ctx.close();
}
await browser.close();
const passed = results.filter((r) => r.ok).length;
console.log(`\nM5 验收结果:${passed}/${results.length} 通过`);
process.exit(passed === results.length ? 0 : 1);
})();