fix: harden dependencies and downstream delivery

This commit is contained in:
hectorzhao
2026-07-15 12:05:24 +08:00
parent 9bbfb72be6
commit a758672436
17 changed files with 418 additions and 57 deletions
@@ -1574,6 +1574,50 @@ describe('SendChainService', () => {
});
});
it('immediately terminates an unrecoverable downstream delivery', async () => {
const { service, prisma } = createService();
prisma.cmppDownstreamDelivery.findUnique.mockResolvedValue({
id: 'delivery-1',
tenantId: 'tenant-1',
applicationId: 'app-1',
messageId: 'MSG-1',
deliveryType: 'receipt',
retryCount: 0,
retryEnabled: true,
});
await service.markDownstreamDeliveryFailed(
'delivery-1',
'历史回执缺少原 Submit Sequence_Id,无法重建 Msg_Id',
'unrecoverable',
);
expect(prisma.cmppDownstreamDelivery.update).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
status: 'failed',
retryCount: 1,
nextRetryAt: null,
}),
}));
});
it('does not let the pending timeout scan overwrite a delivery that is already awaiting acknowledgement', async () => {
const { service, prisma } = createService();
const awaitingAck = {
id: 'delivery-1', status: 'awaiting_ack', tenantId: 'tenant-1', applicationId: 'app-1',
messageId: 'MSG-1', deliveryType: 'receipt', retryCount: 0,
};
prisma.cmppDownstreamDelivery.findUnique.mockResolvedValue(awaitingAck);
await expect(service.markDownstreamDeliveryFailed(
'delivery-1',
'下游投递排队超过 72 小时,系统自动终止重试',
'queue_timeout',
)).resolves.toEqual(awaitingAck);
expect(prisma.cmppDownstreamDelivery.update).not.toHaveBeenCalled();
});
it('only marks downstream delivery delivered after a successful CMPP_DELIVER_RESP', async () => {
const { service, prisma } = createService();
@@ -1769,6 +1813,35 @@ describe('SendChainService', () => {
expect(service['postGatewayControl']).not.toHaveBeenCalled();
});
it('terminates a manual requeue when gateway reports it is unrecoverable', async () => {
const { service, prisma } = createService();
prisma.cmppDownstreamDelivery.findUnique.mockResolvedValueOnce({
id: 'delivery-1', tenantId: 'tenant-1', applicationId: 'app-1', messageId: 'MSG-1',
deliveryType: 'receipt', status: 'failed', retryCount: 3, manualRetryCount: 0,
payload: { account: '100001', messageId: 'MSG-1', receiptStatus: 'delivered' },
application: { cmppAccount: '100001' },
}).mockResolvedValueOnce({
id: 'delivery-1', tenantId: 'tenant-1', applicationId: 'app-1', messageId: 'MSG-1',
deliveryType: 'receipt', status: 'pending', retryCount: 0, retryEnabled: true,
});
service['postGatewayControl'] = jest.fn().mockResolvedValue({
sent: false,
retryable: false,
reasonCode: 'MISSING_SUBMIT_SEQUENCE_ID',
errorMessage: '历史回执缺少原 Submit Sequence_Id,无法重建 Msg_Id,系统已终止重投',
});
await service.requeueDownstreamDelivery('delivery-1');
expect(prisma.cmppDownstreamDelivery.update).toHaveBeenLastCalledWith(expect.objectContaining({
data: expect.objectContaining({
status: 'failed',
nextRetryAt: null,
lastError: expect.stringContaining('MISSING_SUBMIT_SEQUENCE_ID'),
}),
}));
});
it('supports batch requeue of downstream deliveries', async () => {
const { service } = createService();
service.requeueDownstreamDelivery = jest.fn()
@@ -1819,16 +1892,41 @@ describe('SendChainService', () => {
expect(prisma.smsBatchTask.update).toHaveBeenCalled();
});
it('terminates downstream deliveries that remain pending for 72 hours after the latest manual retry', async () => {
const { service, prisma } = createService();
prisma.cmppDownstreamDelivery.findMany.mockResolvedValue([{ id: 'delivery-expired' }]);
service.markDownstreamDeliveryFailed = jest.fn().mockResolvedValue({ id: 'delivery-expired', status: 'failed' });
await expect(service.markExpiredDownstreamDeliveries(72)).resolves.toEqual({ failed: 1 });
expect(prisma.cmppDownstreamDelivery.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: {
status: 'pending',
OR: [
{ lastRetriedAt: null, createdAt: { lte: expect.any(Date) } },
{ lastRetriedAt: { lte: expect.any(Date) } },
],
},
}));
expect(service.markDownstreamDeliveryFailed).toHaveBeenCalledWith(
'delivery-expired',
'下游投递排队超过 72 小时,系统自动终止重试',
'queue_timeout',
);
});
it('starts the automatic receipt-timeout scan after application startup', async () => {
jest.useFakeTimers();
const previousEnabled = process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED;
const { service } = createService();
const scan = jest.spyOn(service, 'markUnknownTimeout').mockResolvedValue({ timeout: 0 });
const downstreamScan = jest.spyOn(service, 'markExpiredDownstreamDeliveries').mockResolvedValue({ failed: 0 });
try {
process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED = 'true';
service.onModuleInit();
await jest.advanceTimersByTimeAsync(60_000);
expect(scan).toHaveBeenCalledWith({});
expect(downstreamScan).toHaveBeenCalledWith();
await service.onModuleDestroy();
} finally {
if (previousEnabled === undefined) delete process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED;