Files
HealthCarePregnant/pcm-platform/backend/src/integration/scenarios.spec.ts
T
2026-06-18 09:48:05 +08:00

262 lines
12 KiB
TypeScript
Raw 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.
/**
* 端到端场景演练(T-9.2 / PRD §3.2 核心场景 S1–S7、§5.1 闭环、§7 上线判据)。
*
* 在服务层串联各模块,覆盖 V1 可上线场景的实质闭环;
* 标注为 V2/V3 的能力(趋势预测、游戏化)以 it.todo 记录,不在 V1 断言。
*
* 复用既有内存仓储与服务,构造共享依赖的系统实例(makeSystem)。
*/
import { PatientService } from '../modules/patient/patient.service';
import { InMemoryPatientRepository } from '../modules/patient/patient.repository';
import { AnalysisService } from '../modules/analysis/analysis.service';
import { InMemoryAlertRepository } from '../modules/analysis/alert.repository';
import { CaseflowService } from '../modules/caseflow/caseflow.service';
import { InMemoryCaseflowRepository } from '../modules/caseflow/caseflow.repository';
import { ObservationService } from '../modules/observation/observation.service';
import { InMemoryObservationRepository } from '../modules/observation/observation.repository';
import { KnowledgeService } from '../modules/knowledge/knowledge.service';
import { InMemoryKnowledgeRepository } from '../modules/knowledge/knowledge.repository';
import { AiService } from '../modules/ai/ai.service';
import { NotificationService } from '../modules/notification/notification.service';
import { RedflagService } from '../modules/redflag/redflag.service';
import { ReminderService } from '../modules/reminder/reminder.service';
import { FollowupService } from '../modules/followup/followup.service';
import { InMemoryFollowupRepository } from '../modules/followup/followup.repository';
function makeSystem() {
const notification = new NotificationService();
const followupService = new FollowupService(new InMemoryFollowupRepository());
const patientService = new PatientService(new InMemoryPatientRepository());
const analysisService = new AnalysisService(new InMemoryAlertRepository());
const caseflowService = new CaseflowService(new InMemoryCaseflowRepository());
const observationService = new ObservationService(
new InMemoryObservationRepository(),
patientService,
analysisService,
caseflowService,
followupService,
);
const knowledgeService = new KnowledgeService(new InMemoryKnowledgeRepository());
const aiService = new AiService(knowledgeService, analysisService, caseflowService);
const redflagService = new RedflagService(notification, caseflowService);
const reminderService = new ReminderService(patientService, notification);
return {
notification,
patientService,
analysisService,
caseflowService,
observationService,
knowledgeService,
aiService,
redflagService,
reminderService,
};
}
describe('PRD §3.2 核心场景端到端演练(T-9.2', () => {
describe('S1 居家测血糖/血压后自动分析(REQ-1/3)', () => {
it('血糖录入 → 质控通过 → 规则分析 → 可追溯预警', async () => {
const sys = makeSystem();
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
const rec = await sys.observationService.record(patient.id, {
indicator: 'fasting_glucose',
value: 5.6,
});
expect(rec.observation.qcStatus).toBe('accepted');
expect(rec.alert).not.toBeNull();
expect(rec.alert?.level).toBe('medium');
// 可解释 / 可追溯(NFR-3):命中规则 + 关联观测值
expect(rec.alert?.ruleIds.length).toBeGreaterThan(0);
expect(rec.alert?.observationId).toBe(rec.observation.id);
});
it('血压录入 → 升高生成预警', async () => {
const sys = makeSystem();
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
const rec = await sys.observationService.record(patient.id, {
indicator: 'systolic_bp',
value: 150,
});
expect(rec.observation.qcStatus).toBe('accepted');
expect(rec.alert?.level).toBe('medium');
});
it('不可信数值 → 质控拦截,不驱动高风险结论(C-5)', async () => {
const sys = makeSystem();
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
const rec = await sys.observationService.record(patient.id, {
indicator: 'fasting_glucose',
value: 50, // 超出生理合理范围
});
expect(rec.observation.qcStatus).toBe('rejected');
expect(rec.alert).toBeNull();
});
});
describe('S2 GDM 风险孕妇的日常管理闭环(REQ-6/9/10', () => {
it('建档→预警→自动开案→指派→流转→AI建议(需人工确认)→照护计划→提醒', async () => {
const sys = makeSystem();
// 既往 GDM → 基线 medium
const patient = await sys.patientService.create({
name: '小雅',
age: 31,
lmp: '2026-01-01',
historyGdm: true,
});
expect(patient.initialRiskLevel).toBe('medium');
// 录入偏高血糖 → 预警 → 预警驱动开案
const rec = await sys.observationService.record(patient.id, {
indicator: 'fasting_glucose',
value: 6.2,
});
expect(rec.alert).not.toBeNull();
const c = await sys.caseflowService.getCaseByPatient(patient.id);
expect(c.status).toBe('open');
expect(c.riskLevel).toBe('medium');
// 指派管理师 + 推进阶段
await sys.caseflowService.assignManager(c.id, 'cm-1');
const advanced = await sys.caseflowService.advanceStage(c.id, 'assessment', '评估');
expect(advanced.stage).toBe('assessment');
expect(advanced.caseManagerId).toBe('cm-1');
// AI 决策建议:中风险 → 必须人工确认(REQ-10.3)
const reco = await sys.aiService.recommendForPatient(patient.id);
expect(reco.requiresHumanConfirmation).toBe(true);
expect(reco.actions.length).toBeGreaterThan(0);
// 照护计划
const plan = await sys.caseflowService.createCarePlan(c.id, {
goals: ['空腹血糖 < 5.1 mmol/L'],
interventions: [{ kind: 'lifestyle', description: '饮食控制 + 餐后散步' }],
followUpFrequency: 'weekly',
});
expect(plan.goals.length).toBe(1);
// 提醒下发
const reminder = await sys.reminderService.dispatch({ patientId: patient.id, type: 'measurement' });
expect(reminder.effectiveType).toBe('measurement');
expect(sys.notification.findByRecipient(patient.id).length).toBeGreaterThan(0);
});
});
describe('S3 子痫前期红旗症状旁路(REQ-5', () => {
it('危急组合 → 即时就医提示 + 通知管理师/医生 + 升级高风险', async () => {
const sys = makeSystem();
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
const result = await sys.redflagService.evaluate(patient.id, {
systolicBp: 165,
diastolicBp: 112,
symptoms: ['severe_headache'],
});
expect(result.triggered).toBe(true);
expect(result.hits.map((h) => h.ruleId)).toContain('RF-PREECLAMPSIA');
expect(result.patientAdvice).toContain('就医');
// 通知:孕妇 + 管理师 + 医生,均为紧急
const msgs = sys.notification.findByRecipient(patient.id);
const audiences = msgs.filter((m) => m.urgent).map((m) => m.audience);
expect(audiences).toEqual(expect.arrayContaining(['patient', 'case_manager', 'physician']));
// 个案升级为高风险
const c = await sys.caseflowService.getCaseByPatient(patient.id);
expect(c.riskLevel).toBe('high');
});
});
describe('S4 深夜疑问"我能吃这个吗"REQ-7', () => {
it('命中知识 → 带溯源作答;无依据 → 不超纲并建议就医', async () => {
const sys = makeSystem();
await sys.knowledgeService.create({
category: 'guideline',
title: '孕期饮食与血糖',
content: '控制精制碳水、少量多餐;水果适量并计入总量。',
keywords: ['饮食', '血糖', '水果', '能吃'],
source: '某权威指南',
authority: 'authoritative',
});
const grounded = await sys.aiService.ask('孕期能吃水果吗');
expect(grounded.grounded).toBe(true);
expect(grounded.citations.length).toBeGreaterThan(0);
expect(grounded.citations[0].source).toBe('某权威指南');
const ungrounded = await sys.aiService.ask('明天会下雨吗');
expect(ungrounded.grounded).toBe(false);
expect(ungrounded.citations).toHaveLength(0);
});
});
describe('S5 趋势预警/预测(REQ-3.5/4', () => {
it('纵向观测按孕周累积,构成趋势分析的数据基础', async () => {
const sys = makeSystem();
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
await sys.observationService.record(patient.id, { indicator: 'fasting_glucose', value: 4.8 });
await sys.observationService.record(patient.id, { indicator: 'fasting_glucose', value: 5.0 });
const series = await sys.observationService.list(patient.id);
expect(series.length).toBe(2);
// 每条带孕周上下文,支撑后续时序趋势
expect(series.every((o) => typeof o.gestationalWeeks === 'number')).toBe(true);
});
// 趋势异常检测(REQ-3.5,V2)与预测模型(REQ-4,V3,需回顾性+前瞻性验证)超出 V1 范围
it.todo('S5 时序趋势异常检测(REQ-3.5)为 V2');
it.todo('S5 趋势预测模型(REQ-4)为 V3,需经验证');
});
describe('S6 提升坚持度、缓解焦虑(REQ-9', () => {
it('高风险孕妇运动提醒自动替换为休息(REQ-9.2 安全约束)', async () => {
const sys = makeSystem();
// 多胎 + 既往GDM → score 4 → 基线高风险
const patient = await sys.patientService.create({
name: '小雅',
age: 31,
lmp: '2026-01-01',
multipleGestation: true,
historyGdm: true,
});
expect(patient.initialRiskLevel).toBe('high');
const reminder = await sys.reminderService.dispatch({ patientId: patient.id, type: 'exercise' });
expect(reminder.effectiveType).toBe('rest');
expect(reminder.adjustedForRisk).toBe(true);
});
it('非高风险孕妇运动提醒保持运动', async () => {
const sys = makeSystem();
const patient = await sys.patientService.create({ name: '小雅', age: 28, lmp: '2026-01-01' });
const reminder = await sys.reminderService.dispatch({ patientId: patient.id, type: 'exercise' });
expect(reminder.effectiveType).toBe('exercise');
expect(reminder.adjustedForRisk).toBe(false);
});
// 游戏化(积分/徽章/小游戏,REQ-9.3/9.4)为 V2
it.todo('S6 游戏化激励(REQ-9.3/9.4)为 V2');
});
describe('S7 管理师 PC 端批量管理(REQ-13.3', () => {
it('多名孕妇入列,提供风险分层作为工作台排序依据', async () => {
const sys = makeSystem();
await sys.patientService.create({ name: '低风险', age: 28, lmp: '2026-01-01' });
await sys.patientService.create({ name: '中风险', age: 31, lmp: '2026-01-01', historyGdm: true });
await sys.patientService.create({
name: '高风险',
age: 36,
lmp: '2026-01-01',
multipleGestation: true,
historyPih: true,
});
const list = await sys.patientService.list();
expect(list.length).toBe(3);
// 每名孕妇均带初始风险分层(admin-web 工作列表据此按 高→中→低 排序)
const levels = list.map((p) => p.initialRiskLevel).sort();
expect(levels).toEqual(['high', 'low', 'medium']);
});
});
});