Commit e1ffafbc by luoqi

test(plan): runAllForHost 批量化等价性测试 + 修复 stale-close spec 过时 mock

新增 plan-engine-batch.spec.ts:忠实内存 mock 端到端验证批量路径 5 种结局(create/unchanged/superseded/suppressed/stale-close)+ 混合并发 + createMany 日志,与逐患者语义等价。修 plan-engine-stale-close.spec.ts 过时 mock(status:{in}匹配、加 persona.findMany/createMany)与返回形态断言(toMatchObject);assigned 0命中关闭对齐 2026-06 决策。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 5b5379af
......@@ -28,18 +28,25 @@ function makePrismaMock(opts: {
const create = jest.fn().mockResolvedValue({ id: 'new-plan' });
const findFirst = jest.fn().mockResolvedValue(opts.latestPlan ?? null);
const findMany = jest.fn().mockImplementation(async ({ where }) => {
// runAllForHost 关闭闸:status='active'
if (where?.status === 'active') return opts.activePlans ?? [];
// fetchSnoozedSignalKeys:status IN ('completed','abandoned')(冷静期)
// runAllForHost 关闭闸:status IN ('active','assigned')
const statusIn = where?.status?.in;
if (Array.isArray(statusIn) && statusIn.includes('active')) return opts.activePlans ?? [];
// snoozed(completed/abandoned)/ prefetchForBatch latest(无 status)→ 空
return [];
});
const prisma = {
followupPlan: { findFirst, findMany, update, updateMany, create },
persona: { findFirst: jest.fn().mockResolvedValue(null) },
// 批量路径 prefetchForBatch 用 persona.findMany;单刷用 findFirst
persona: {
findFirst: jest.fn().mockResolvedValue(null),
findMany: jest.fn().mockResolvedValue([]),
},
planGenerationLog: {
create: jest.fn().mockResolvedValue({ id: 'log-1' }),
update: jest.fn().mockResolvedValue({}),
// 批量路径日志一次性 createMany
createMany: jest.fn().mockResolvedValue({ count: 0 }),
},
$transaction: jest
.fn()
......@@ -84,7 +91,7 @@ describe('缺口1 — recomputeForPatient 0 命中关闭遗留 active plan', ()
const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });
expect(res).toEqual({ plansCreated: 0, plansClosed: 1 });
expect(res).toMatchObject({ plansCreated: 0, plansClosed: 1 });
expect(spies.update).toHaveBeenCalledTimes(1);
expect(spies.update).toHaveBeenCalledWith(
expect.objectContaining({
......@@ -96,14 +103,16 @@ describe('缺口1 — recomputeForPatient 0 命中关闭遗留 active plan', ()
expect(prisma.planGenerationLog.create).not.toHaveBeenCalled();
});
test('0 命中 + assigned plan → 不动(客服跟进中,plansClosed=0)', async () => {
// 注:closeStaleActivePlan 现关闭 active **及 assigned**(2026-06 决策:缺口全消失即关,
// 含 assigned,否则客服拿"已解决"理由白打)。service.ts:70 行旧注释"assigned 不动"已过时。
test('0 命中 + assigned plan → 关闭(2026-06 决策:缺口消失即关,含 assigned)', async () => {
const { prisma, spies } = makePrismaMock({ latestPlan: { id: 'plan-a', status: 'assigned' } });
const engine = makeEngine(prisma, makeScenario([]));
const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });
expect(res).toEqual({ plansCreated: 0, plansClosed: 0 });
expect(spies.update).not.toHaveBeenCalled();
expect(res).toMatchObject({ plansCreated: 0, plansClosed: 1 });
expect(spies.update).toHaveBeenCalledTimes(1);
});
test('0 命中 + 无任何 plan → 无操作(plansClosed=0)', async () => {
......@@ -112,7 +121,7 @@ describe('缺口1 — recomputeForPatient 0 命中关闭遗留 active plan', ()
const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });
expect(res).toEqual({ plansCreated: 0, plansClosed: 0 });
expect(res).toMatchObject({ plansCreated: 0, plansClosed: 0 });
expect(spies.update).not.toHaveBeenCalled();
});
......@@ -122,7 +131,7 @@ describe('缺口1 — recomputeForPatient 0 命中关闭遗留 active plan', ()
const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });
expect(res).toEqual({ plansCreated: 0, plansClosed: 0 });
expect(res).toMatchObject({ plansCreated: 0, plansClosed: 0 });
expect(spies.update).not.toHaveBeenCalled();
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment