Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { ScalarTypes } from '@roostorg/types';
2
3import getBottle, { type Dependencies } from '../../iocContainer/index.js';
4import { getBottleContainerWithIOMocks } from '../../test/setupMockedServer.js';
5import { SignalType, type SignalsService } from '../signalsService/index.js';
6import makeGetTransientRunSignalWithCache from './signalExecutionService.js';
7
8describe('Signal Execution Service', () => {
9 let container: Dependencies;
10 let signalsService: SignalsService;
11 let getPolicyActionPenalties: Dependencies['getPolicyActionPenaltiesEventuallyConsistent'];
12 const mockLocationsLoader = async () => [];
13 const mockTextBankStringsLoader = jest.fn(async ({ bankId }) =>
14 bankId === '1' ? ['a', 'b', 'c'] : bankId === '2' ? ['d', 'e', 'f'] : [],
15 );
16 const mockGetImageBank = jest.fn(async ({ bankId }) =>
17 bankId === 'test-bank' ? { id: 1, name: 'test-bank', hma_name: 'org_test-bank', description: null, enabled_ratio: 1.0, org_id: 'test-org', created_at: new Date(), updated_at: new Date() } : null,
18 );
19
20 // eslint-disable-next-line better-mutation/no-mutation
21 mockLocationsLoader.close = jest.fn();
22 // eslint-disable-next-line better-mutation/no-mutation
23 (mockTextBankStringsLoader as any).close = jest.fn();
24 // eslint-disable-next-line better-mutation/no-mutation, @typescript-eslint/no-explicit-any
25 (mockGetImageBank as any).close = jest.fn();
26
27 describe('getTransientRunSignalWithCache', () => {
28 beforeAll(async () => {
29 // No mutation rule here is a false positive, since this is more initial
30 // setup (we're never gonna reassign again later) that simply has to use
31 // `let` vars to defer the work until the test suite is actually running
32 // (so we don't bother w/ it if this test suite is skipped, e.g., in which
33 // case cleanup wouldn't happen).
34 /* eslint-disable better-mutation/no-mutation */
35 container = await getBottleContainerWithIOMocks();
36 signalsService = container.SignalsService;
37 getPolicyActionPenalties =
38 container.getPolicyActionPenaltiesEventuallyConsistent;
39 /* eslint-enable better-mutation/no-mutation */
40 });
41
42 afterAll(async () => {
43 await container.closeSharedResourcesForShutdown();
44 }, 20_000);
45
46 beforeEach(() => {
47 // This is only safe while we're not running tests concurrently.
48 // Consider using the `makeTestWithFixture` helper instead to make
49 // a local copy of this state for each test.
50 jest.clearAllMocks();
51 });
52
53 test('should batch textBank loads w/i a single tick', async () => {
54 const runSignal = makeGetTransientRunSignalWithCache(
55 mockLocationsLoader,
56 mockTextBankStringsLoader,
57 getPolicyActionPenalties,
58 mockGetImageBank,
59 signalsService,
60 container.Tracer,
61 )();
62
63 const signalInputs = [
64 {
65 signal: { type: SignalType.TEXT_MATCHING_CONTAINS_REGEX },
66 value: { type: ScalarTypes.STRING, value: 'a' },
67 matchingValues: {
68 textBankIds: ['1'],
69 },
70 threshold: null,
71 comparator: null,
72 userId: 'dummy',
73 orgId: 'dummmy',
74 },
75 {
76 signal: { type: SignalType.TEXT_MATCHING_CONTAINS_TEXT },
77 value: { type: ScalarTypes.STRING, value: 'a' },
78 matchingValues: {
79 textBankIds: ['2'],
80 },
81 threshold: null,
82 comparator: null,
83 userId: 'dummy',
84 orgId: 'dummmy',
85 },
86 ] as const;
87
88 const results = await Promise.all(signalInputs.map(runSignal));
89 expect(mockTextBankStringsLoader).toHaveBeenCalledTimes(2);
90 expect(mockTextBankStringsLoader.mock.calls).toMatchInlineSnapshot(`
91 [
92 [
93 {
94 "bankId": "1",
95 "orgId": "dummmy",
96 },
97 ],
98 [
99 {
100 "bankId": "2",
101 "orgId": "dummmy",
102 },
103 ],
104 ]
105 `);
106
107 expect(results).toEqual([
108 {
109 score: true,
110 outputType: { scalarType: ScalarTypes.BOOLEAN },
111 matchedValue: 'a',
112 },
113 {
114 score: false,
115 outputType: { scalarType: ScalarTypes.BOOLEAN },
116 matchedValue: undefined,
117 },
118 ]);
119 });
120
121 test('should not re-run the same signal with the same input', async () => {
122 // NB: this test actually does run the signals, but that's fine; these
123 // signals don't hit the network. (The point of the mock is just so we can
124 // spy on how many times runSignal was called.)
125 const signalsServiceSpy = (await getBottle()).container.SignalsService;
126 // eslint-disable-next-line better-mutation/no-mutation
127 signalsServiceSpy.runSignal = jest.fn(
128 signalsServiceSpy.runSignal.bind(signalsServiceSpy),
129 ) as any;
130
131 const runSignal = makeGetTransientRunSignalWithCache(
132 mockLocationsLoader,
133 mockTextBankStringsLoader,
134 getPolicyActionPenalties,
135 mockGetImageBank,
136 signalsServiceSpy,
137 container.Tracer,
138 )();
139
140 const signalInputs = [
141 {
142 signal: { type: SignalType.TEXT_MATCHING_CONTAINS_REGEX },
143 value: { type: ScalarTypes.STRING, value: 'a' },
144 matchingValues: {
145 textBankIds: ['1'],
146 },
147 threshold: null,
148 comparator: null,
149 userId: 'dummy',
150 orgId: 'dummmy',
151 },
152 {
153 signal: { type: SignalType.TEXT_MATCHING_CONTAINS_REGEX },
154 value: { type: ScalarTypes.STRING, value: 'a' },
155 matchingValues: {
156 textBankIds: ['1'],
157 },
158 threshold: null,
159 comparator: null,
160 userId: 'dummy',
161 orgId: 'dummmy',
162 },
163 ] as const;
164
165 const results = await Promise.all(signalInputs.map(runSignal));
166 expect(signalsServiceSpy.runSignal).toHaveBeenCalledTimes(1);
167 expect(results).toEqual([
168 {
169 matchedValue: 'a',
170 score: true,
171 outputType: { scalarType: ScalarTypes.BOOLEAN },
172 },
173 {
174 matchedValue: 'a',
175 score: true,
176 outputType: { scalarType: ScalarTypes.BOOLEAN },
177 },
178 ]);
179 });
180 });
181});