| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- /**
- * 四模块链路 E2E 集成测试(P1-3)
- * 场景:上传报告 → 解析入库 → 按规则生成方案 → 规划师审核 → 激活拆任务 → 孩子任务出现 → 重复激活幂等 → 全局统计
- *
- * 运行前提:
- * - 后端已启动(本地 localhost:9082 或线上 https://ai.etotem.com.cn 同域代理)
- * - 环境变量:
- * API_BASE_URL 后端地址(默认 http://localhost:9082)
- * ADMIN_PHONE 管理员/规划师手机号
- * ADMIN_PASSWORD 登录密码
- * PDF_PATH 测试报告 PDF 绝对路径(默认跳过上传步骤,仅跑方案→任务段)
- * FAMILY_ID 演示家庭ID(激活后查孩子任务用)
- * CHILD_ID 孩子ID(激活后查孩子任务用)
- * PLAN_ID 已有 generated 方案ID(跳过生成/审核直接激活用)
- *
- * 运行:npx jest tests/integration/plan-chain.spec.js --forceExit
- * 输出:每一步 接口 / 请求 / 响应 code / 耗时(ms)
- */
- describe('【主链路】报告解析→方案生成→任务分解→跟踪执行 E2E', () => {
- const BASE = process.env.API_BASE_URL || 'http://localhost:9082';
- const PHONE = process.env.ADMIN_PHONE || '';
- const PASSWORD = process.env.ADMIN_PASSWORD || '';
- const PDF_PATH = process.env.PDF_PATH || '';
- const FAMILY_ID = process.env.FAMILY_ID || '';
- const CHILD_ID = process.env.CHILD_ID || '';
- const PLAN_ID = process.env.PLAN_ID || '';
- const state = { token: '', reportId: null, planId: null, taskCountBefore: 0, taskCountAfter: 0 };
- const steps = [];
- let order = 0;
- async function post(path, body, { auth = true, multipart = false } = {}) {
- const headers = {};
- if (!multipart) headers['Content-Type'] = 'application/json';
- if (auth && state.token) headers['Authorization'] = 'Bearer ' + state.token;
- const t0 = Date.now();
- let res;
- try {
- res = await fetch(BASE + path, {
- method: 'POST',
- headers,
- body: multipart ? body : (body === undefined ? undefined : JSON.stringify(body)),
- signal: AbortSignal.timeout(30000)
- });
- } catch (e) {
- const ms = Date.now() - t0;
- steps.push({ no: ++order, api: path, body: safeBody(body), code: 'NET', ms, note: e.message });
- console.error(`[${ms}ms] ${path} 网络错误: ${e.message}`);
- return null;
- }
- const ms = Date.now() - t0;
- let json = null;
- try { json = await res.json(); } catch (e) { /* 非JSON响应 */ }
- const code = json ? json.code : ('HTTP' + res.status);
- steps.push({ no: ++order, api: path, body: safeBody(body), code, ms,
- note: json && json.message ? String(json.message).substring(0, 60) : '' });
- console.log(`[${ms}ms] POST ${path} -> code=${code}${json && json.message ? ' msg=' + json.message : ''}`);
- return json;
- }
- function safeBody(body) {
- if (!body) return {};
- if (body instanceof FormData) return '{file}';
- try { return JSON.stringify(body).substring(0, 200); } catch (e) { return String(body).substring(0, 200); }
- }
- test('[0] 前置环境检查', () => {
- expect(PHONE && PASSWORD).toBeTruthy();
- console.log('BASE_URL =', BASE);
- console.log('PDF_PATH =', PDF_PATH || '(未提供,跳过上传段)');
- console.log('FAMILY_ID =', FAMILY_ID || '(未提供,跳过孩子任务断言)');
- console.log('CHILD_ID =', CHILD_ID || '(未提供,跳过孩子任务断言)');
- });
- test('[1] 登录管理后台获取 token', async () => {
- const json = await post('/api/admin-auth/login-by-password', { phone: PHONE, password: PASSWORD }, { auth: false });
- expect(json).not.toBeNull();
- expect(json.code).toBe(200);
- state.token = json.data && (json.data.token || json.data.accessToken);
- expect(state.token).toBeTruthy();
- console.log('已获取 token:', String(state.token).substring(0, 20) + '...');
- });
- test('[2] 上传报告 PDF → 解析入库(P0-4/P1-1)', async () => {
- if (!PDF_PATH) { console.log('跳过(未提供 PDF_PATH)'); return; }
- const fs = require('fs');
- const buf = fs.readFileSync(PDF_PATH);
- const fd = new FormData();
- fd.append('file', new Blob([buf], { type: 'application/pdf' }), PDF_PATH.split(/[\\/]/).pop());
- const json = await post('/api/health/report/upload', fd, { multipart: true });
- expect(json).not.toBeNull();
- if (json.code !== 200) {
- console.warn('上传未返回 200(可能 PDF 非募极生物肠道菌群报告或后端未装解析依赖):', json.message);
- return;
- }
- state.reportId = json.data && (json.data.id || json.data.reportId);
- console.log('报告已入库 reportId =', state.reportId);
- });
- test('[3] 报告列表可见(入库落库验证)', async () => {
- if (!state.reportId) { console.log('跳过(无报告ID)'); return; }
- const json = await post('/api/health/report/list', {});
- expect(json).not.toBeNull();
- console.log('报告列表 total =', json.code === 200 && json.data ? (Array.isArray(json.data) ? json.data.length : json.data.total || '-') : '-');
- });
- test('[4] 按规则生成方案(DAN 报告确认触发 AssessmentPlanGenerator,P0-3)', async () => {
- const resultId = process.env.DAN_RESULT_ID;
- if (!resultId) { console.log('跳过(未提供 DAN_RESULT_ID;规则引擎在 /api/assessment/report/{resultId}/confirm-upload 触发)'); return; }
- const json = await post('/api/assessment/report/' + resultId + '/confirm-upload', {});
- expect(json).not.toBeNull();
- console.log('DAN 结果确认返回 code =', json.code);
- });
- test('[5] 规划师审核方案生成的方案(P0-2 联调)', async () => {
- let planId = PLAN_ID;
- if (!planId) {
- // 取当前家庭最新 generated 方案(须有 familyId;此处由后端 JWT familyId 决定)
- const list = await post('/api/guide/plans/family-list', { status: 'generated' });
- if (list && list.code === 200 && Array.isArray(list.data) && list.data.length > 0) {
- planId = list.data[0].id;
- }
- }
- if (!planId) { console.log('跳过(无可用 generated 方案;需先完成 DAN 报告确认或提供 PLAN_ID)'); return; }
- state.planId = planId;
- const json = await post('/api/guide/plans/review', { planId: Number(planId), approved: true });
- expect(json).not.toBeNull();
- if (json.code === 200) console.log('方案已批准 planId =', planId);
- else console.warn('方案审核结果:', json.message);
- });
- test('[6] 激活方案 → 任务分解(P0-5)', async () => {
- if (!state.planId) { console.log('跳过(无方案ID)'); return; }
- if (FAMILY_ID && CHILD_ID) {
- const before = await post('/api/guide/families/' + FAMILY_ID + '/children/' + CHILD_ID + '/tasks', {});
- state.taskCountBefore = before && before.code === 200 && Array.isArray(before.data)
- ? before.data.length : 0;
- console.log('激活前孩子任务数 =', state.taskCountBefore);
- }
- const json = await post('/api/guide/plans/activate', { planId: Number(state.planId) });
- expect(json).not.toBeNull();
- if (json.code === 200) console.log('方案已激活(引导任务 + 模板包任务已生成)');
- else console.warn('激活失败:', json.message);
- });
- test('[7] 孩子任务列表出现方案任务(跟踪执行)', async () => {
- if (!state.planId || !FAMILY_ID || !CHILD_ID) { console.log('跳过(缺 FAMILY_ID/CHILD_ID)'); return; }
- const json = await post('/api/guide/families/' + FAMILY_ID + '/children/' + CHILD_ID + '/tasks', {});
- expect(json && json.code).toBe(200);
- const tasks = json && Array.isArray(json.data) ? json.data : [];
- const planTasks = tasks.filter(t => t.sourceType === 'plan' || t.taskType === 'plan');
- console.log('激活后孩子任务数 =', tasks.length, ',其中方案任务 =', planTasks.length);
- expect(planTasks.length).toBeGreaterThan(0);
- });
- test('[8] 重复激活幂等(不产生重复任务)', async () => {
- if (!state.planId) { console.log('跳过(无方案ID)'); return; }
- const json = await post('/api/guide/plans/activate', { planId: Number(state.planId) });
- expect(json).not.toBeNull();
- // 幂等表现:返回业务错误(400 状态提示),且不重复生成任务
- console.log('重复激活响应 code =', json.code, 'message =', json.message);
- if (FAMILY_ID && CHILD_ID) {
- const after = await post('/api/guide/families/' + FAMILY_ID + '/children/' + CHILD_ID + '/tasks', {});
- state.taskCountAfter = after && after.code === 200 && Array.isArray(after.data) ? after.data.length : -1;
- console.log('重复激活后孩子任务数 =', state.taskCountAfter, '(应等于激活后数量,不新增)');
- }
- });
- test('[9] 全局任务统计可查', async () => {
- const json = await post('/api/admin/tasks/stats', {});
- expect(json).not.toBeNull();
- console.log('tasks/stats code =', json.code);
- });
- afterAll(() => {
- console.log('\n===== E2E 步序记录(告警: FAIL 表示 code!=200)=====');
- console.table(steps);
- const fails = steps.filter(s => String(s.code) !== '200');
- if (fails.length) console.warn('存在非 200 步骤:', fails.map(f => '#' + f.no + ' ' + f.api).join(', '));
- });
- });
|