Backend: - Add SSE streaming client (unifuncsSseClient) replacing async polling - Add paragraph-based reasoning parser with mergeConsecutiveThinking - Add requirement expansion service (DeepSeek-V3 PICOS+MeSH) - Add Word export service with Pandoc, inline hyperlinks, reference link expansion - Add deep research V2 worker with 2s log flush and Chinese source prompt - Add 5 curated data sources config (PubMed/ClinicalTrials/Cochrane/CNKI/MedJournals) - Add 4 API endpoints (generate-requirement/tasks/task-status/export-word) - Update Prisma schema with 6 new V2.0 fields on AslResearchTask - Add DB migration for V2.0 fields - Simplify ASL_DEEP_RESEARCH_EXPANSION prompt (remove strategy section) Frontend: - Add waterfall-flow DeepResearchPage (phase 0-4 progressive reveal) - Add LandingView, SetupPanel, StrategyConfirm, AgentTerminal, ResultsView - Add react-markdown + remark-gfm for report rendering - Add custom link component showing visible URLs after references - Add useDeepResearchTask polling hook - Add deep research TypeScript types Tests: - Add E2E test, smoke test, and Chinese data source test scripts Docs: - Update ASL module status (v2.0 - core features complete) - Update system status (v6.1 - ASL V2.0 milestone) - Update Unifuncs DeepSearch API guide (v2.0 - SSE mode + Chinese source results) - Update module auth specification (test script guidelines) - Update V2.0 development plan Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
/**
|
||
* Deep Research V2.0 — 冒烟测试(Smoke Test)
|
||
*
|
||
* 仅测试 API 接口连通性和基本参数校验,
|
||
* 不调用 LLM / Unifuncs,无外部依赖,几秒完成。
|
||
*
|
||
* 运行方式:
|
||
* npx tsx src/modules/asl/__tests__/deep-research-v2-smoke.ts
|
||
*
|
||
* 前置条件:
|
||
* - 后端服务运行在 localhost:3001
|
||
* - PostgreSQL 运行中
|
||
*/
|
||
|
||
const BASE_URL = process.env.TEST_BASE_URL || 'http://localhost:3001';
|
||
const API_PREFIX = `${BASE_URL}/api/v1`;
|
||
|
||
const TEST_PHONE = process.env.TEST_PHONE || '13800000001';
|
||
const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456';
|
||
|
||
let authToken = '';
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
async function api(path: string, options: RequestInit = {}): Promise<{ status: number; data: any }> {
|
||
const res = await fetch(`${API_PREFIX}${path}`, {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
||
...(options.headers || {}),
|
||
},
|
||
});
|
||
|
||
const contentType = res.headers.get('content-type') || '';
|
||
const data = contentType.includes('json') ? await res.json() : await res.text();
|
||
return { status: res.status, data };
|
||
}
|
||
|
||
function check(ok: boolean, label: string) {
|
||
if (ok) {
|
||
console.log(` ✅ ${label}`);
|
||
passed++;
|
||
} else {
|
||
console.log(` ❌ ${label}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
async function run() {
|
||
console.log('🔥 Deep Research V2.0 冒烟测试\n');
|
||
|
||
// ─── 1. 登录 ───
|
||
console.log('[1] 登录');
|
||
try {
|
||
const res = await api('/auth/login/password', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ phone: TEST_PHONE, password: TEST_PASSWORD }),
|
||
});
|
||
const ok = res.status === 200 && res.data.success;
|
||
check(ok, `POST /auth/login/password → ${res.status}`);
|
||
if (ok) {
|
||
authToken = res.data.data.tokens?.accessToken || res.data.data.accessToken || res.data.data.token;
|
||
} else {
|
||
console.log(' ⚠️ 登录失败,后续需要认证的测试将跳过');
|
||
console.log(` 返回: ${JSON.stringify(res.data).slice(0, 200)}`);
|
||
}
|
||
} catch (e: any) {
|
||
check(false, `连接后端失败: ${e.message}`);
|
||
console.log('\n💡 请先启动后端: cd backend && npm run dev\n');
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── 2. GET /data-sources ───
|
||
console.log('\n[2] GET /asl/research/data-sources');
|
||
{
|
||
const res = await api('/asl/research/data-sources');
|
||
check(res.status === 200, `HTTP ${res.status} === 200`);
|
||
check(res.data.success === true, 'success=true');
|
||
check(Array.isArray(res.data.data) && res.data.data.length >= 3, `返回 ${res.data.data?.length} 个数据源`);
|
||
|
||
const hasPubmed = res.data.data?.some((s: any) => s.id === 'pubmed');
|
||
check(hasPubmed, 'PubMed 存在于数据源列表');
|
||
|
||
const defaultChecked = res.data.data?.filter((s: any) => s.defaultChecked);
|
||
check(defaultChecked?.length >= 1, `默认选中 ${defaultChecked?.length} 个`);
|
||
}
|
||
|
||
// ─── 3. POST /generate-requirement (参数校验) ───
|
||
console.log('\n[3] POST /asl/research/generate-requirement — 参数校验');
|
||
{
|
||
const res = await api('/asl/research/generate-requirement', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ originalQuery: '' }),
|
||
});
|
||
check(res.status === 400, `空 query → HTTP ${res.status} === 400`);
|
||
}
|
||
|
||
{
|
||
const res = await api('/asl/research/generate-requirement', {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
});
|
||
check(res.status === 400, `缺少 query → HTTP ${res.status} === 400`);
|
||
}
|
||
|
||
// ─── 4. PUT /tasks/:taskId/execute (参数校验) ───
|
||
console.log('\n[4] PUT /asl/research/tasks/:taskId/execute — 参数校验');
|
||
{
|
||
const res = await api('/asl/research/tasks/nonexistent-id/execute', {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ confirmedRequirement: 'test' }),
|
||
});
|
||
check(res.status === 404, `不存在的 taskId → HTTP ${res.status} === 404`);
|
||
}
|
||
|
||
{
|
||
const res = await api('/asl/research/tasks/some-id/execute', {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ confirmedRequirement: '' }),
|
||
});
|
||
check(res.status === 400, `空 confirmedRequirement → HTTP ${res.status} === 400`);
|
||
}
|
||
|
||
// ─── 5. GET /tasks/:taskId (不存在) ───
|
||
console.log('\n[5] GET /asl/research/tasks/:taskId — 不存在');
|
||
{
|
||
const res = await api('/asl/research/tasks/nonexistent-id');
|
||
check(res.status === 404, `不存在 → HTTP ${res.status} === 404`);
|
||
}
|
||
|
||
// ─── 6. GET /tasks/:taskId/export-word (不存在) ───
|
||
console.log('\n[6] GET /asl/research/tasks/:taskId/export-word — 不存在');
|
||
{
|
||
const res = await api('/asl/research/tasks/nonexistent-id/export-word');
|
||
check(res.status === 500 || res.status === 404, `不存在 → HTTP ${res.status}`);
|
||
}
|
||
|
||
// ─── 7. 未认证访问 ───
|
||
console.log('\n[7] 未认证访问(无 Token)');
|
||
{
|
||
const savedToken = authToken;
|
||
authToken = '';
|
||
|
||
const res = await api('/asl/research/data-sources');
|
||
check(res.status === 401, `无 Token → HTTP ${res.status} === 401`);
|
||
|
||
authToken = savedToken;
|
||
}
|
||
|
||
// ─── 结果汇总 ───
|
||
console.log(`\n${'═'.repeat(50)}`);
|
||
console.log(` 🏁 冒烟测试完成: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`);
|
||
console.log(`${'═'.repeat(50)}\n`);
|
||
|
||
if (failed > 0) {
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
run();
|