import { SendGatewaySubmitService } from './send-gateway-submit.service'; describe('shared dispatch night gate', () => { function setup() { const message = { id: 'm1', status: 'queued', tenantId: 't1', applicationId: 'a1', batchTaskId: 'b1', content: '任意内容', }; const prisma = { smsMessageRecord: { findUnique: jest.fn().mockResolvedValue(message), findMany: jest.fn().mockResolvedValue([message, { ...message, id: 'm2' }]), update: jest.fn(), }, }; const risk = { guardNightSending: jest.fn().mockResolvedValue(new Set(['m1'])) }; const facade = { refreshTaskProgress: jest.fn(), selectChannelForMessage: jest.fn(), submitMessageToGateway: jest.fn(), }; const sut = new SendGatewaySubmitService( prisma as never, {} as never, risk as never, {} as never, {} as never, facade as never, {} as never, ); return { sut, prisma, risk, facade }; } it('holds single-message jobs before any channel or Submit operation', async () => { const { sut, facade } = setup(); expect(await sut.processSendJob({ messageRecordId: 'm1' })).toMatchObject({ status: 'pending_review', submitted: false, }); expect(facade.selectChannelForMessage).not.toHaveBeenCalled(); expect(facade.submitMessageToGateway).not.toHaveBeenCalled(); }); it('partitions the fast worker batch by persistent gate decisions', async () => { const { sut } = setup(); const route = jest.spyOn(sut as never, 'planRoutesBatch').mockResolvedValue({ planned: [], failed: [] } as never); const result = await sut['processSendJobBatch']([{ messageRecordId: 'm1' }, { messageRecordId: 'm2' }]); expect(result.get('m1')).toMatchObject({ status: 'pending_review' }); expect(route).toHaveBeenCalledWith([expect.objectContaining({ id: 'm2' })]); }); it('propagates database failures without submitting or misclassifying them as routing failures', async () => { const { sut, risk, facade, prisma } = setup(); risk.guardNightSending.mockRejectedValue(new Error('database unavailable')); await expect(sut.processSendJob({ messageRecordId: 'm1' })).rejects.toThrow('database unavailable'); expect(prisma.smsMessageRecord.update).not.toHaveBeenCalled(); expect(facade.submitMessageToGateway).not.toHaveBeenCalled(); }); });