Files
lislgosms/api/src/send-chain/channel-sensitive-routing.spec.ts
T

90 lines
5.1 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.
import { ChannelWordSnapshot, loadChannelWords } from './channel-sensitive-routing';
import { PrismaService } from '../prisma/prisma.service';
const channel = {
status: 'active',
carrier: 'all',
carriers: ['mobile', 'unicom', 'telecom'],
sendRegion: '全国',
connectionStates: [{ status: 'connected', currentConnections: 1, desiredConnections: 1 }],
};
const items = ['a', 'b'].map((channelId, index) => ({ channelId, carrier: 'mobile', priority: index + 1, channel }));
const options = { carrier: 'mobile', excludedChannelIds: new Set<string>(), approvedChannelIds: new Set(['a', 'b']) };
const rule = { id: 'word-a', channelId: 'a', word: '贷款', version: 1 };
describe('channel sensitive routing snapshot', () => {
it('checks rewritten content separately for each candidate', () => {
const snapshot = new ChannelWordSnapshot([{ ...rule, word: '拒收请回复R' }]);
expect(
snapshot.select('m', '正文拒收请回复R', items, options, (id) => (id === 'a' ? '正文' : '正文拒收请回复R'))
.selected?.channelId,
).toBe('a');
expect(snapshot.select('n', '正文', items, options, () => '正文拒收请回复R').selected?.channelId).toBe('b');
});
it('removes only matching eligible channels before original priority selection', () => {
const snapshot = new ChannelWordSnapshot([rule]);
expect(snapshot.select('m', '【签名】贷款业务', items, options).selected?.channelId).toBe('b');
expect(snapshot.select('n', '正常业务', items, options).selected?.channelId).toBe('a');
expect(snapshot.decisions[0].snapshot).toEqual(
expect.objectContaining({ excludedChannelIds: ['a'], stage: 'route' }),
);
});
it('keeps original exclusions and distinguishes no original route from all hit', () => {
const snapshot = new ChannelWordSnapshot([rule]);
expect(snapshot.select('m', '贷款', items, { ...options, excludedChannelIds: new Set(['b']) }).rejected).toBe(true);
expect(snapshot.select('m', '贷款', items, { ...options, approvedChannelIds: new Set() }).rejected).toBe(false);
expect(
snapshot.select(
'm',
'贷款',
items.map((item) => ({ ...item, channel: { ...channel, status: 'inactive' } })),
options,
).rejected,
).toBe(false);
});
it('allows national fallback when matching province channel is excluded', () => {
const provincial = [{ ...items[0], province: '上海', channel: { ...channel, sendRegion: '上海' } }, items[1]];
expect(
new ChannelWordSnapshot([rule]).select('m', '贷款', provincial, { ...options, province: '上海' }).selected
?.channelId,
).toBe('b');
});
it('matches original complete content, case sensitively without removing separators', () => {
const snapshot = new ChannelWordSnapshot([{ ...rule, word: 'Ab贷款' }]);
expect(snapshot.hits('【签名】Ab贷款')[0].count).toBe(1);
for (const value of ['ab贷款', 'b贷款', 'Ab贷-款']) expect(snapshot.hits(value)).toHaveLength(0);
});
it('caps sample words but never truncates excluded channels or hit counts', () => {
const rules = Array.from({ length: 21 }, (_, index) => ({ ...rule, id: String(index) }));
const snapshot = new ChannelWordSnapshot([...rules, { ...rule, channelId: 'b' }]);
expect(snapshot.hits('贷款')[0]).toEqual(expect.objectContaining({ count: 21, samples: expect.any(Array) }));
expect(snapshot.hits('贷款')[0].samples).toHaveLength(20);
expect(snapshot.select('m', '贷款', items, options).rejected).toBe(true);
});
it('reads rules once per batch and persists all decisions in one idempotent write', async () => {
const prisma = {
channelSensitiveWord: { findMany: jest.fn().mockResolvedValue([rule]) },
smsChannelSensitiveDecision: { createMany: jest.fn() },
};
const snapshot = await loadChannelWords(prisma as unknown as PrismaService, ['a', 'b', 'a']);
for (let index = 0; index < 100; index++) snapshot.select(String(index), '贷款', items, options);
expect(snapshot.hits('贷款')).toBe(snapshot.hits('贷款'));
await snapshot.persist(prisma as unknown as PrismaService);
expect(prisma.channelSensitiveWord.findMany).toHaveBeenCalledTimes(1);
expect(prisma.smsChannelSensitiveDecision.createMany).toHaveBeenCalledWith({
data: expect.any(Array),
skipDuplicates: true,
});
expect(snapshot.decisions).toHaveLength(100);
expect(new Set(snapshot.decisions.map((value) => value.routeAttemptId)).size).toBe(100);
});
it('fails closed for technical read and persistence errors, not as a word hit', async () => {
const prisma = {
channelSensitiveWord: { findMany: jest.fn().mockRejectedValue(Error('db unavailable')) },
smsChannelSensitiveDecision: { createMany: jest.fn().mockRejectedValue(Error('db unavailable')) },
};
await expect(loadChannelWords(prisma as unknown as PrismaService, ['a'])).rejects.toMatchObject({ status: 503 });
const snapshot = new ChannelWordSnapshot([rule]);
snapshot.select('m', '贷款', items, options);
await expect(snapshot.persist(prisma as unknown as PrismaService)).rejects.toMatchObject({ status: 503 });
});
});