@eeacms/volto-eea-chatbot 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,25 @@
1
- import middleware from './middleware';
1
+ import fetch from 'node-fetch';
2
2
  import { isPathAllowed } from '../middleware';
3
3
 
4
- jest.mock('./generative');
4
+ // Mock node-fetch
5
+ jest.mock('node-fetch');
5
6
 
6
7
  describe('halloumi middleware', () => {
7
- let req, res, next;
8
+ let req, res, next, middleware;
8
9
 
9
10
  beforeEach(() => {
11
+ fetch.mockReset();
12
+ // Clear the module cache so middleware is re-imported with current env vars
13
+ jest.resetModules();
14
+ // Re-mock after reset
15
+ jest.mock('node-fetch', () => jest.fn());
16
+
10
17
  req = {
11
18
  url: '/_ha/generate',
12
19
  method: 'POST',
13
20
  body: {
14
21
  sources: ['source1', 'source2'],
15
22
  answer: 'test answer',
16
- maxContextSegments: 3,
17
23
  },
18
24
  headers: {},
19
25
  ip: '127.0.0.1',
@@ -30,23 +36,90 @@ describe('halloumi middleware', () => {
30
36
  jest.restoreAllMocks();
31
37
  });
32
38
 
33
- it('returns error when LLMGW_TOKEN is missing', async () => {
34
- const origToken = process.env.LLMGW_TOKEN;
35
- const origUrl = process.env.LLMGW_URL;
36
- delete process.env.LLMGW_TOKEN;
37
- delete process.env.LLMGW_URL;
39
+ it('proxies request to rag-fact-checker and returns response', async () => {
40
+ process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
41
+ middleware = require('./middleware').default;
42
+ const mockedFetch = require('node-fetch');
43
+
44
+ const mockResponse = {
45
+ claims: [
46
+ {
47
+ startOffset: 0,
48
+ endOffset: 20,
49
+ segmentIds: ['0'],
50
+ score: 0.95,
51
+ rationale: 'Supported by source.',
52
+ },
53
+ ],
54
+ segments: { 0: { startOffset: 0, endOffset: 30 } },
55
+ };
56
+
57
+ mockedFetch.mockResolvedValue({
58
+ ok: true,
59
+ status: 200,
60
+ json: () => Promise.resolve(mockResponse),
61
+ });
62
+
63
+ await middleware(req, res, next);
64
+
65
+ expect(mockedFetch).toHaveBeenCalledWith(
66
+ 'http://localhost:8000/halloumi/generate',
67
+ expect.objectContaining({
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({
71
+ answer: 'test answer',
72
+ sources: ['source1', 'source2'],
73
+ max_context_segments: 0,
74
+ }),
75
+ }),
76
+ );
77
+ expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/json');
78
+ expect(res.send).toHaveBeenCalledWith(mockResponse);
79
+ });
80
+
81
+ it('returns 502 when rag-fact-checker is unreachable', async () => {
82
+ process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
83
+ middleware = require('./middleware').default;
84
+ const mockedFetch = require('node-fetch');
85
+
86
+ mockedFetch.mockRejectedValue(new Error('ECONNREFUSED'));
38
87
 
39
88
  await middleware(req, res, next);
40
89
 
41
- expect(res.send).toHaveBeenCalledWith({
42
- error: 'Invalid configuration: missing LLMGW_TOKEN or LLMGW_URL',
90
+ expect(res.status).toHaveBeenCalledWith(502);
91
+ expect(res.send).toHaveBeenCalledWith(
92
+ expect.objectContaining({
93
+ error: expect.stringContaining('Fact-checker unavailable'),
94
+ }),
95
+ );
96
+ });
97
+
98
+ it('returns error when rag-fact-checker responds with non-ok status', async () => {
99
+ process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
100
+ middleware = require('./middleware').default;
101
+ const mockedFetch = require('node-fetch');
102
+
103
+ mockedFetch.mockResolvedValue({
104
+ ok: false,
105
+ status: 500,
106
+ json: () => Promise.resolve({ detail: 'Internal server error' }),
43
107
  });
44
108
 
45
- process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
46
- process.env.LLMGW_URL = origUrl;
109
+ await middleware(req, res, next);
110
+
111
+ expect(res.status).toHaveBeenCalledWith(500);
112
+ expect(res.send).toHaveBeenCalledWith(
113
+ expect.objectContaining({
114
+ error: expect.stringContaining('Internal server error'),
115
+ }),
116
+ );
47
117
  });
48
118
 
49
119
  it('rejects disallowed paths with 404', async () => {
120
+ process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
121
+ middleware = require('./middleware').default;
122
+
50
123
  req.url = '/_ha/admin/config';
51
124
  req.method = 'POST';
52
125
 
@@ -57,6 +130,9 @@ describe('halloumi middleware', () => {
57
130
  });
58
131
 
59
132
  it('rejects allowed path with wrong HTTP method', async () => {
133
+ process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
134
+ middleware = require('./middleware').default;
135
+
60
136
  req.url = '/_ha/generate';
61
137
  req.method = 'GET';
62
138
 
@@ -65,100 +141,29 @@ describe('halloumi middleware', () => {
65
141
  expect(res.statusCode).toBe(404);
66
142
  expect(res.send).toHaveBeenCalledWith({ error: 'Not Found' });
67
143
  });
68
- });
69
-
70
- // These tests need a fresh module loaded with env vars set so that
71
- // LLMGW_TOKEN and LLMGW_URL are defined at module load time,
72
- // allowing the code to reach getVerifyClaimResponse.
73
- describe('halloumi middleware - dynamic import', () => {
74
- const buildReq = () => ({
75
- url: '/_ha/generate',
76
- method: 'POST',
77
- body: {
78
- sources: ['source1', 'source2'],
79
- answer: 'test answer',
80
- maxContextSegments: 3,
81
- },
82
- headers: {},
83
- ip: '127.0.0.1',
84
- });
85
144
 
86
- const buildRes = () => ({
87
- send: jest.fn(),
88
- set: jest.fn(),
89
- status: jest.fn().mockReturnThis(),
90
- });
91
-
92
- it('sends response on successful getVerifyClaimResponse', async () => {
93
- const origToken = process.env.LLMGW_TOKEN;
94
- const origUrl = process.env.LLMGW_URL;
95
- process.env.LLMGW_TOKEN = 'test-token'; //betterleaks:allow
96
- process.env.LLMGW_URL = 'http://test-url';
97
-
98
- jest.resetModules();
99
-
100
- const mockGetVerifyClaimResponse = jest
101
- .fn()
102
- .mockResolvedValue({ claims: [], segments: {} });
103
-
104
- jest.setMock('./generative', {
105
- getVerifyClaimResponse: mockGetVerifyClaimResponse,
106
- });
107
-
108
- const middlewareMod = require('./middleware').default;
109
-
110
- const req = buildReq();
111
- const res = buildRes();
112
-
113
- await middlewareMod(req, res, jest.fn());
114
-
115
- expect(mockGetVerifyClaimResponse).toHaveBeenCalled();
116
- expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/json');
117
- expect(res.send).toHaveBeenCalledWith({ claims: [], segments: {} });
118
-
119
- process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
120
- process.env.LLMGW_URL = origUrl;
121
- });
145
+ it('uses default URL when RAG_FACT_CHECKER_URL is not set', async () => {
146
+ delete process.env.RAG_FACT_CHECKER_URL;
147
+ middleware = require('./middleware').default;
148
+ const mockedFetch = require('node-fetch');
122
149
 
123
- it('handles errors from getVerifyClaimResponse', async () => {
124
- const origToken = process.env.LLMGW_TOKEN;
125
- const origUrl = process.env.LLMGW_URL;
126
- process.env.LLMGW_TOKEN = 'test-token'; //betterleaks:allow
127
- process.env.LLMGW_URL = 'http://test-url';
128
-
129
- jest.resetModules();
130
-
131
- const mockGetVerifyClaimResponse = jest
132
- .fn()
133
- .mockRejectedValue(new Error('LLM error'));
134
-
135
- jest.setMock('./generative', {
136
- getVerifyClaimResponse: mockGetVerifyClaimResponse,
150
+ mockedFetch.mockResolvedValue({
151
+ ok: true,
152
+ status: 200,
153
+ json: () => Promise.resolve({ claims: [], segments: {} }),
137
154
  });
138
155
 
139
- const middlewareMod = require('./middleware').default;
140
-
141
- const req = buildReq();
142
- const res = buildRes();
143
-
144
- await middlewareMod(req, res, jest.fn());
156
+ await middleware(req, res, next);
145
157
 
146
- expect(res.status).toHaveBeenCalledWith(500);
147
- expect(res.send).toHaveBeenCalledWith(
148
- expect.objectContaining({ error: expect.stringContaining('LLM error') }),
158
+ expect(mockedFetch).toHaveBeenCalledWith(
159
+ 'http://localhost:8000/halloumi/generate',
160
+ expect.any(Object),
149
161
  );
150
-
151
- process.env.LLMGW_TOKEN = origToken; //betterleaks:allow
152
- process.env.LLMGW_URL = origUrl;
153
162
  });
154
163
  });
155
164
 
156
165
  describe('halloumi path allowlist', () => {
157
- // Mirror of ALLOWED_HALLOUMI_PATHS from middleware.js
158
- const ALLOWED_HALLOUMI_PATHS = [
159
- { path: '/generate', methods: ['POST'] },
160
- { path: '/classify', methods: ['POST'] },
161
- ];
166
+ const ALLOWED_HALLOUMI_PATHS = [{ path: '/generate', methods: ['POST'] }];
162
167
 
163
168
  it('allows /generate with POST', () => {
164
169
  expect(isPathAllowed('/generate', 'POST', ALLOWED_HALLOUMI_PATHS)).toBe(
@@ -166,12 +171,6 @@ describe('halloumi path allowlist', () => {
166
171
  );
167
172
  });
168
173
 
169
- it('allows /classify with POST', () => {
170
- expect(isPathAllowed('/classify', 'POST', ALLOWED_HALLOUMI_PATHS)).toBe(
171
- true,
172
- );
173
- });
174
-
175
174
  it('rejects /generate with wrong method', () => {
176
175
  expect(isPathAllowed('/generate', 'GET', ALLOWED_HALLOUMI_PATHS)).toBe(
177
176
  false,
@@ -1,149 +0,0 @@
1
- import debug from 'debug';
2
- import fetch from 'node-fetch';
3
-
4
- const log = debug('halloumi');
5
-
6
- const LLMGW_URL = process.env.LLMGW_URL;
7
- const LLMGW_API_KEY = process.env.LLMGW_TOKEN; //betterleaks:allow
8
- const MIN_CONTEXT_SENTENCES_FOR_FILTERING = 75;
9
-
10
- const filterModel = {
11
- name: 'Inhouse-LLM/gpt-oss-120b',
12
- apiUrl: `${LLMGW_URL}/chat/completions`,
13
- apiKey: LLMGW_API_KEY, //betterleaks:allow
14
- };
15
-
16
- export async function callLLM(apiUrl, apiKey, requestBody, { ip } = {}) {
17
- const headers = {
18
- 'Content-Type': 'application/json',
19
- accept: 'application/json',
20
- };
21
- if (apiKey) {
22
- headers['Authorization'] = `Bearer ${apiKey}`;
23
- }
24
- if (ip) {
25
- headers['X-Forwarded-For'] = ip;
26
- }
27
-
28
- const response = await fetch(apiUrl, {
29
- method: 'POST',
30
- headers,
31
- body: JSON.stringify(requestBody),
32
- });
33
-
34
- return response.json();
35
- }
36
-
37
- function buildClaimFilterPrompt(sentences) {
38
- const numberedSentences = sentences
39
- .map((s, i) => `${i + 1}. "${s.trim()}"`)
40
- .join('\n');
41
-
42
- return `Given the following numbered sentences, identify which ones are NOT verifiable factual claims.
43
-
44
- Exclude sentences that are:
45
- - Greetings, preambles, or transitional phrases
46
- - Opinions, vague statements, or subjective assessments
47
- - Introductory sentences that set up a list or table
48
- - Sentences that merely restate or paraphrase the question
49
- - Conversational closers (e.g., "let me know if you need more")
50
- - Broad summaries that don't assert a specific fact
51
- - Disclaimers or meta-commentary about the response itself
52
- - Sentences that do not provide enough information to verify
53
- - Sentences that only mention a concept without providing specific information
54
-
55
- Respond with ONLY a comma-separated list of the sentence numbers to exclude. If none should be excluded, respond with "NONE".
56
-
57
- Sentences:
58
- ${numberedSentences}`;
59
- }
60
-
61
- function buildContextFilterPrompt(contextSentences, claimSentences) {
62
- const numberedContext = contextSentences
63
- .map((s, i) => `${i + 1}. "${s.trim()}"`)
64
- .join('\n');
65
- const numberedClaims = claimSentences
66
- .map((s, i) => `${i + 1}. "${s.trim()}"`)
67
- .join('\n');
68
-
69
- return `Given the following context sentences and claims, identify which context sentences are NOT relevant to verifying ANY of the claims.
70
-
71
- Context sentences:
72
- ${numberedContext}
73
-
74
- Claims to verify:
75
- ${numberedClaims}
76
-
77
- Respond with ONLY a comma-separated list of the context sentence numbers that are NOT relevant. If all are relevant, respond with "NONE".`;
78
- }
79
-
80
- /**
81
- * Parses a comma-separated list of indices from an LLM response.
82
- */
83
- export function parseExcludeIndices(content, maxIndex) {
84
- const excludeIndices = new Set();
85
- if (content.trim().toUpperCase() === 'NONE') {
86
- return excludeIndices;
87
- }
88
- const matches = content.match(/\d+/g) || [];
89
- for (const match of matches) {
90
- const idx = parseInt(match, 10);
91
- if (idx >= 1 && idx <= maxIndex) {
92
- excludeIndices.add(idx);
93
- }
94
- }
95
- return excludeIndices;
96
- }
97
-
98
- async function callFilterModel(prompt, { ip } = {}) {
99
- const data = {
100
- messages: [{ role: 'user', content: prompt }],
101
- temperature: 0.0,
102
- model: filterModel.name,
103
- };
104
- const jsonData = await callLLM(filterModel.apiUrl, filterModel.apiKey, data, {
105
- ip,
106
- });
107
- return jsonData.choices?.[0]?.message?.content || '';
108
- }
109
-
110
- export async function excludeClaimSentences(sentences, { ip } = {}) {
111
- if (sentences.length === 0) {
112
- return new Set();
113
- }
114
-
115
- try {
116
- const prompt = buildClaimFilterPrompt(sentences);
117
- const content = await callFilterModel(prompt, { ip });
118
- const excludedIndices = parseExcludeIndices(content, sentences.length);
119
- log('Claim filter response', excludedIndices.size);
120
- return excludedIndices;
121
- } catch (error) {
122
- log('Claim filter failed, skipping', error);
123
- return new Set();
124
- }
125
- }
126
-
127
- export async function excludeContextSentences(
128
- contextSentences,
129
- claimSentences,
130
- { ip } = {},
131
- ) {
132
- if (contextSentences.length <= MIN_CONTEXT_SENTENCES_FOR_FILTERING) {
133
- return new Set();
134
- }
135
-
136
- try {
137
- const prompt = buildContextFilterPrompt(contextSentences, claimSentences);
138
- const content = await callFilterModel(prompt, { ip });
139
- const excludedIndices = parseExcludeIndices(
140
- content,
141
- contextSentences.length,
142
- );
143
- log('Context filter response', excludedIndices.size);
144
- return excludedIndices;
145
- } catch (error) {
146
- log('Context filter failed, skipping', error);
147
- return new Set();
148
- }
149
- }
@@ -1,242 +0,0 @@
1
- import fetch from 'node-fetch';
2
-
3
- import {
4
- parseExcludeIndices,
5
- callLLM,
6
- excludeClaimSentences,
7
- excludeContextSentences,
8
- } from './filtering';
9
- jest.mock('node-fetch');
10
-
11
- describe('parseExcludeIndices', () => {
12
- it('parses single indices', () => {
13
- const result = parseExcludeIndices('1,3,5', 6);
14
- expect(result).toEqual(new Set([1, 3, 5]));
15
- });
16
-
17
- it('returns empty set for NONE', () => {
18
- const result = parseExcludeIndices('NONE', 10);
19
- expect(result).toEqual(new Set());
20
- });
21
-
22
- it('returns empty set for none (lowercase)', () => {
23
- const result = parseExcludeIndices(' none ', 10);
24
- expect(result).toEqual(new Set());
25
- });
26
-
27
- it('ignores indices below 1', () => {
28
- const result = parseExcludeIndices('0, 1, 3', 5);
29
- expect(result).toEqual(new Set([1, 3]));
30
- });
31
-
32
- it('ignores indices above maxIndex', () => {
33
- const result = parseExcludeIndices('1, 3, 99', 5);
34
- expect(result).toEqual(new Set([1, 3]));
35
- });
36
-
37
- it('handles whitespace variations', () => {
38
- const result = parseExcludeIndices(' 1 , 5 , 7 ', 10);
39
- expect(result).toEqual(new Set([1, 5, 7]));
40
- });
41
-
42
- it('extracts numbers even from unexpected formats', () => {
43
- // Parser uses match(/\d+/g) so it extracts all numbers
44
- const result = parseExcludeIndices('1-3, 5', 10);
45
- expect(result).toEqual(new Set([1, 3, 5]));
46
- });
47
-
48
- it('returns empty set for empty string with no numbers', () => {
49
- const result = parseExcludeIndices('no numbers here', 10);
50
- expect(result).toEqual(new Set());
51
- });
52
- });
53
-
54
- describe('callLLM', () => {
55
- beforeEach(() => {
56
- fetch.mockReset();
57
- });
58
-
59
- afterEach(() => {
60
- fetch.mockReset();
61
- });
62
-
63
- it('makes POST request with correct headers', async () => {
64
- const mockResponse = { choices: [{ message: { content: 'result' } }] };
65
- fetch.mockResolvedValue({
66
- json: () => Promise.resolve(mockResponse),
67
- });
68
-
69
- await callLLM('https://api.example.com', 'my-api-key', { model: 'gpt-4' });
70
-
71
- expect(fetch).toHaveBeenCalledWith(
72
- 'https://api.example.com',
73
- expect.objectContaining({
74
- method: 'POST',
75
- headers: expect.objectContaining({
76
- 'Content-Type': 'application/json',
77
- Authorization: 'Bearer my-api-key',
78
- }),
79
- }),
80
- );
81
- });
82
-
83
- it('omits Authorization header when no apiKey', async () => {
84
- fetch.mockResolvedValue({
85
- json: () => Promise.resolve({}),
86
- });
87
-
88
- await callLLM('https://api.example.com', null, { model: 'gpt-4' });
89
-
90
- const headers = fetch.mock.calls[0][1].headers;
91
- expect(headers['Authorization']).toBeUndefined();
92
- });
93
-
94
- it('adds X-Forwarded-For header when ip is provided', async () => {
95
- fetch.mockResolvedValue({
96
- json: () => Promise.resolve({}),
97
- });
98
-
99
- await callLLM('https://api.example.com', null, {}, { ip: '1.2.3.4' });
100
-
101
- const headers = fetch.mock.calls[0][1].headers;
102
- expect(headers['X-Forwarded-For']).toBe('1.2.3.4');
103
- });
104
-
105
- it('returns parsed JSON response', async () => {
106
- const mockResponse = { choices: [{ message: { content: 'hello' } }] };
107
- fetch.mockResolvedValue({
108
- json: () => Promise.resolve(mockResponse),
109
- });
110
-
111
- const result = await callLLM('https://api.example.com', null, {});
112
- expect(result).toEqual(mockResponse);
113
- });
114
- });
115
-
116
- describe('excludeClaimSentences', () => {
117
- beforeEach(() => {
118
- fetch.mockReset();
119
- });
120
-
121
- afterEach(() => {
122
- fetch.mockReset();
123
- });
124
-
125
- it('returns empty set for empty sentences array', async () => {
126
- const result = await excludeClaimSentences([]);
127
- expect(result).toEqual(new Set());
128
- expect(fetch).not.toHaveBeenCalled();
129
- });
130
-
131
- it('calls LLM and returns excluded sentence indices', async () => {
132
- fetch.mockResolvedValue({
133
- json: () =>
134
- Promise.resolve({
135
- choices: [{ message: { content: '1,3' } }],
136
- }),
137
- });
138
-
139
- const sentences = ['Sentence one.', 'Sentence two.', 'Sentence three.'];
140
- const result = await excludeClaimSentences(sentences);
141
-
142
- expect(result).toEqual(new Set([1, 3]));
143
- expect(fetch).toHaveBeenCalledTimes(1);
144
- });
145
-
146
- it('returns empty set when LLM returns NONE', async () => {
147
- fetch.mockResolvedValue({
148
- json: () =>
149
- Promise.resolve({
150
- choices: [{ message: { content: 'NONE' } }],
151
- }),
152
- });
153
-
154
- const sentences = ['Some factual sentence.', 'Another factual sentence.'];
155
- const result = await excludeClaimSentences(sentences);
156
-
157
- expect(result).toEqual(new Set());
158
- });
159
-
160
- it('returns empty set on LLM error', async () => {
161
- fetch.mockRejectedValue(new Error('Network error'));
162
-
163
- const sentences = ['Sentence one.'];
164
- const result = await excludeClaimSentences(sentences);
165
-
166
- expect(result).toEqual(new Set());
167
- });
168
-
169
- it('passes ip option to LLM call', async () => {
170
- fetch.mockResolvedValue({
171
- json: () =>
172
- Promise.resolve({ choices: [{ message: { content: 'NONE' } }] }),
173
- });
174
-
175
- await excludeClaimSentences(['test sentence'], { ip: '10.0.0.1' });
176
-
177
- const headers = fetch.mock.calls[0][1].headers;
178
- expect(headers['X-Forwarded-For']).toBe('10.0.0.1');
179
- });
180
- });
181
-
182
- describe('excludeContextSentences', () => {
183
- const MIN_SENTENCES = 75;
184
-
185
- beforeEach(() => {
186
- fetch.mockReset();
187
- });
188
-
189
- afterEach(() => {
190
- fetch.mockReset();
191
- });
192
-
193
- it('returns empty set when context sentences <= minimum threshold', async () => {
194
- const shortContext = Array(10).fill('Short sentence.');
195
- const claims = ['A claim.'];
196
- const result = await excludeContextSentences(shortContext, claims);
197
-
198
- expect(result).toEqual(new Set());
199
- expect(fetch).not.toHaveBeenCalled();
200
- });
201
-
202
- it('calls LLM when context exceeds minimum threshold', async () => {
203
- fetch.mockResolvedValue({
204
- json: () =>
205
- Promise.resolve({
206
- choices: [{ message: { content: '1,2' } }],
207
- }),
208
- });
209
-
210
- const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
211
- const claims = ['A claim.'];
212
- const result = await excludeContextSentences(longContext, claims);
213
-
214
- expect(result).toEqual(new Set([1, 2]));
215
- expect(fetch).toHaveBeenCalledTimes(1);
216
- });
217
-
218
- it('returns empty set on LLM error', async () => {
219
- fetch.mockRejectedValue(new Error('timeout'));
220
-
221
- const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
222
- const claims = ['A claim.'];
223
- const result = await excludeContextSentences(longContext, claims);
224
-
225
- expect(result).toEqual(new Set());
226
- });
227
-
228
- it('returns empty set when LLM returns NONE for context', async () => {
229
- fetch.mockResolvedValue({
230
- json: () =>
231
- Promise.resolve({
232
- choices: [{ message: { content: 'NONE' } }],
233
- }),
234
- });
235
-
236
- const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
237
- const claims = ['A claim.'];
238
- const result = await excludeContextSentences(longContext, claims);
239
-
240
- expect(result).toEqual(new Set());
241
- });
242
- });