90 lines
5.1 KiB
TypeScript
90 lines
5.1 KiB
TypeScript
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贷款', 'Ab贷款', '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 });
|
||
});
|
||
});
|