@eeacms/volto-eea-chatbot 3.0.0 → 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,386 +0,0 @@
1
- import {
2
- halloumiGenerativeAPI,
3
- convertGenerativesClaimToVerifyClaimResponse,
4
- applyPlattScaling,
5
- getVerifyClaimResponse,
6
- } from './generative';
7
- import path from 'path';
8
-
9
- jest.mock('node-fetch');
10
-
11
- describe('halloumiGenerativeAPI reads from mock file', () => {
12
- const originalEnv = process.env;
13
-
14
- beforeEach(() => {
15
- jest.resetModules(); // Most important - reset modules between test runs
16
- process.env = {
17
- ...originalEnv,
18
- MOCK_HALLOUMI_FILE_PATH: path.join(__dirname, '../dummy/qa-raw-3.json'),
19
- };
20
- });
21
-
22
- afterEach(() => {
23
- process.env = originalEnv; // Restore original env
24
- jest.restoreAllMocks(); // Restore all mocks
25
- });
26
-
27
- it('should read from the mock file when MOCK_LLM_CALL is true', async () => {
28
- const model = { name: 'test-model', apiUrl: 'http://test.com' };
29
- const prompt = {
30
- prompt: 'test-prompt',
31
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
32
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
33
- };
34
-
35
- // We are testing halloumiGenerativeAPI which internally calls getLLMResponse
36
- // and getLLMResponse uses the MOCK_LLM_CALL env variable.
37
- const response = await halloumiGenerativeAPI(model, prompt);
38
-
39
- expect(response[0].claimString).toEqual(
40
- '**France – total waste generation (latest available data)** \n',
41
- );
42
- expect(response[0].segments).toEqual([
43
- 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
44
- ]);
45
- });
46
- });
47
-
48
- describe('applyPlattScaling', () => {
49
- it('returns calibrated probability', () => {
50
- const platt = { a: -0.5764, b: 0.1665 };
51
- const result = applyPlattScaling(platt, 0.5);
52
- expect(result).toBeGreaterThan(0);
53
- expect(result).toBeLessThan(1);
54
- });
55
-
56
- it('clamps very low probability', () => {
57
- const platt = { a: -1, b: 0 };
58
- const result = applyPlattScaling(platt, 0);
59
- expect(result).toBeGreaterThan(0);
60
- expect(result).toBeLessThan(1);
61
- });
62
-
63
- it('clamps very high probability', () => {
64
- const platt = { a: -1, b: 0 };
65
- const result = applyPlattScaling(platt, 1);
66
- expect(result).toBeGreaterThan(0);
67
- expect(result).toBeLessThan(1);
68
- });
69
-
70
- it('returns 0.5 when platt params are zero', () => {
71
- const platt = { a: 0, b: 0 };
72
- const result = applyPlattScaling(platt, 0.5);
73
- expect(result).toBeCloseTo(0.5, 1);
74
- });
75
- });
76
-
77
- describe('getVerifyClaimResponse', () => {
78
- it('returns empty response when sources is empty', async () => {
79
- const result = await getVerifyClaimResponse({}, [], 'claims');
80
- expect(result).toEqual({
81
- claims: [],
82
- segments: {},
83
- reason: 'Context is empty',
84
- });
85
- });
86
-
87
- it('returns empty response when sources is null', async () => {
88
- const result = await getVerifyClaimResponse({}, null, 'claims');
89
- expect(result).toEqual({
90
- claims: [],
91
- segments: {},
92
- reason: 'Context is empty',
93
- });
94
- });
95
-
96
- it('returns empty response when claims is falsy', async () => {
97
- const result = await getVerifyClaimResponse({}, ['source'], null);
98
- expect(result).toEqual({
99
- claims: [],
100
- segments: {},
101
- reason: 'Context is empty',
102
- });
103
- });
104
- });
105
-
106
- describe('halloumiGenerativeAPI with plattScaling', () => {
107
- const originalEnv = process.env;
108
-
109
- beforeEach(() => {
110
- jest.resetModules();
111
- process.env = {
112
- ...originalEnv,
113
- MOCK_HALLOUMI_FILE_PATH: path.join(__dirname, '../dummy/qa-raw-3.json'),
114
- };
115
- });
116
-
117
- afterEach(() => {
118
- process.env = originalEnv;
119
- jest.restoreAllMocks();
120
- });
121
-
122
- it('applies platt scaling when model has plattScaling config', async () => {
123
- const model = {
124
- name: 'test-model',
125
- apiUrl: 'http://test.com',
126
- plattScaling: { a: -0.5764, b: 0.1665 },
127
- };
128
- const prompt = {
129
- prompt: 'test-prompt',
130
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
131
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
132
- };
133
-
134
- const response = await halloumiGenerativeAPI(model, prompt);
135
-
136
- // With platt scaling, probabilities should be calibrated
137
- expect(response[0].probabilities).toBeDefined();
138
- const supported = response[0].probabilities.get('supported');
139
- const unsupported = response[0].probabilities.get('unsupported');
140
- expect(supported + unsupported).toBeCloseTo(1, 5);
141
- });
142
- });
143
-
144
- describe('halloumiGenerativeAPI via real fetch', () => {
145
- const originalEnv = process.env;
146
-
147
- beforeEach(() => {
148
- jest.resetModules();
149
- // Remove mock file path to exercise the fetch path
150
- process.env = { ...originalEnv };
151
- delete process.env.MOCK_HALLOUMI_FILE_PATH;
152
- });
153
-
154
- afterEach(() => {
155
- process.env = originalEnv;
156
- jest.restoreAllMocks();
157
- });
158
-
159
- it('calls fetch with correct parameters and auth header', async () => {
160
- // Mock postprocessing to control output and avoid format issues
161
- jest.doMock('./postprocessing', () => ({
162
- getClaimsFromResponse: jest.fn(() => [
163
- {
164
- claimId: 1,
165
- claimString: 'Test claim',
166
- subclaims: [],
167
- segments: [1],
168
- explanation: 'ok',
169
- supported: true,
170
- },
171
- ]),
172
- getTokenProbabilitiesFromLogits: jest.fn(() => [
173
- new Map([
174
- ['supported', 0.9],
175
- ['unsupported', 0.1],
176
- ]),
177
- ]),
178
- }));
179
-
180
- const { halloumiGenerativeAPI } = require('./generative');
181
- const nodeFetch = require('node-fetch');
182
-
183
- const mockResponse = {
184
- choices: [
185
- {
186
- message: { content: 'mock content' },
187
- logprobs: { content: [] },
188
- },
189
- ],
190
- };
191
-
192
- nodeFetch.mockResolvedValueOnce({
193
- json: () => Promise.resolve(mockResponse),
194
- });
195
-
196
- const model = {
197
- name: 'test-model',
198
- apiUrl: 'http://test.com/api',
199
- apiKey: 'test-key',
200
- };
201
- const prompt = {
202
- prompt: 'test prompt',
203
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
204
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
205
- };
206
-
207
- const response = await halloumiGenerativeAPI(model, prompt);
208
- expect(response).toBeDefined();
209
- expect(Array.isArray(response)).toBe(true);
210
- expect(response[0].probabilities.get('supported')).toBe(0.9);
211
-
212
- // Verify fetch was called with auth header
213
- expect(nodeFetch).toHaveBeenCalledWith(
214
- 'http://test.com/api',
215
- expect.objectContaining({
216
- method: 'POST',
217
- headers: expect.objectContaining({
218
- Authorization: 'Bearer test-key',
219
- }),
220
- }),
221
- );
222
- });
223
-
224
- it('calls fetch without auth header when no apiKey', async () => {
225
- jest.doMock('./postprocessing', () => ({
226
- getClaimsFromResponse: jest.fn(() => [
227
- {
228
- claimId: 1,
229
- claimString: 'Test',
230
- subclaims: [],
231
- segments: [],
232
- explanation: 'ok',
233
- supported: true,
234
- },
235
- ]),
236
- getTokenProbabilitiesFromLogits: jest.fn(() => [
237
- new Map([
238
- ['supported', 0.8],
239
- ['unsupported', 0.2],
240
- ]),
241
- ]),
242
- }));
243
-
244
- const { halloumiGenerativeAPI } = require('./generative');
245
- const nodeFetch = require('node-fetch');
246
-
247
- nodeFetch.mockResolvedValueOnce({
248
- json: () =>
249
- Promise.resolve({
250
- choices: [
251
- {
252
- message: { content: 'mock' },
253
- logprobs: { content: [] },
254
- },
255
- ],
256
- }),
257
- });
258
-
259
- const model = { name: 'test-model', apiUrl: 'http://test.com/api' };
260
- const prompt = {
261
- prompt: 'test',
262
- contextOffsets: new Map(),
263
- responseOffsets: new Map(),
264
- };
265
-
266
- await halloumiGenerativeAPI(model, prompt);
267
-
268
- const callHeaders = nodeFetch.mock.calls[0][1].headers;
269
- expect(callHeaders.Authorization).toBeUndefined();
270
- });
271
-
272
- it('defaults to 0.5 when token probabilities and claims do not match', async () => {
273
- jest.doMock('./postprocessing', () => ({
274
- getClaimsFromResponse: jest.fn(() => [
275
- { claimId: 1, claimString: 'Claim 1' },
276
- { claimId: 2, claimString: 'Claim 2' },
277
- ]),
278
- getTokenProbabilitiesFromLogits: jest.fn(() => [
279
- new Map([['supported', 0.9]]),
280
- ]),
281
- }));
282
-
283
- const { halloumiGenerativeAPI } = require('./generative');
284
- const nodeFetch = require('node-fetch');
285
-
286
- nodeFetch.mockResolvedValueOnce({
287
- json: () =>
288
- Promise.resolve({
289
- choices: [
290
- {
291
- message: { content: 'mock' },
292
- logprobs: { content: [] },
293
- },
294
- ],
295
- }),
296
- });
297
-
298
- const model = { name: 'test-model', apiUrl: 'http://test.com' };
299
- const prompt = {
300
- prompt: 'test',
301
- contextOffsets: new Map(),
302
- responseOffsets: new Map(),
303
- };
304
-
305
- const result = await halloumiGenerativeAPI(model, prompt);
306
- // First claim gets the available probability
307
- expect(result[0].probabilities.get('supported')).toBe(0.9);
308
- // Second claim defaults to 0.5
309
- expect(result[1].probabilities.get('supported')).toBe(0.5);
310
- });
311
- });
312
-
313
- describe('convertGenerativesClaimToVerifyClaimResponse', () => {
314
- it('should correctly convert generative claims to verify claim response', () => {
315
- const generativeClaims = [
316
- {
317
- claimId: 1,
318
- claimString: 'Test claim string',
319
- subclaims: ['subclaim1', 'subclaim2'],
320
- segments: [1, 2, 3],
321
- explanation: 'Test explanation',
322
- supported: true,
323
- probabilities: new Map([
324
- ['supported', 0.9],
325
- ['unsupported', 0.1],
326
- ]),
327
- },
328
- ];
329
-
330
- const prompt = {
331
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
332
- responseOffsets: new Map([[1, { startOffset: 100, endOffset: 120 }]]),
333
- joinedContext: 'Test conte',
334
- };
335
-
336
- const result = convertGenerativesClaimToVerifyClaimResponse(
337
- generativeClaims,
338
- prompt,
339
- );
340
-
341
- expect(result).toEqual({
342
- claims: [
343
- {
344
- claimId: 1,
345
- claimString: 'Test claim string',
346
- startOffset: 100,
347
- endOffset: 120,
348
- rationale: 'Test explanation',
349
- segmentIds: ['1', '2', '3'],
350
- score: 0.9,
351
- },
352
- ],
353
- segments: {
354
- 1: {
355
- id: '1',
356
- startOffset: 0,
357
- endOffset: 10,
358
- text: 'Test conte',
359
- },
360
- },
361
- });
362
- });
363
-
364
- it('throws when claim ID is not found in response offsets', () => {
365
- const generativeClaims = [
366
- {
367
- claimId: 999,
368
- claimString: 'Unknown claim',
369
- subclaims: [],
370
- segments: [],
371
- explanation: 'Missing',
372
- supported: false,
373
- probabilities: new Map([['supported', 0.1]]),
374
- },
375
- ];
376
-
377
- const prompt = {
378
- contextOffsets: new Map(),
379
- responseOffsets: new Map(),
380
- };
381
-
382
- expect(() =>
383
- convertGenerativesClaimToVerifyClaimResponse(generativeClaims, prompt),
384
- ).toThrow('Claim 999 not found in response offsets');
385
- });
386
- });
@@ -1,172 +0,0 @@
1
- import nlp from 'compromise';
2
-
3
- const MIN_SENTENCE_LENGTH = 15;
4
-
5
- /**
6
- * Splits markdown text into segments meaningful for fact-checking.
7
- *
8
- * Strategy:
9
- * 1. Split into markdown blocks (paragraphs, list items, table rows, headers)
10
- * 2. For prose blocks, further split into sentences using Intl.Segmenter
11
- * 3. Merge short fragments (< MIN_SENTENCE_LENGTH) into the next sentence
12
- *
13
- * Structural blocks (table rows, headers, list items) are kept as-is since
14
- * they are already atomic units.
15
- *
16
- * @param {string} text Markdown text to split.
17
- * @returns {string[]} Array of segment strings.
18
- */
19
- export function splitMarkdown(text) {
20
- const blocks = splitIntoBlocks(text);
21
- const segments = [];
22
-
23
- for (const block of blocks) {
24
- if (isStructuralBlock(block)) {
25
- if (block.trim().length > 0) {
26
- segments.push(block);
27
- }
28
- } else {
29
- segments.push(...splitProse(block));
30
- }
31
- }
32
-
33
- return segments;
34
- }
35
-
36
- /**
37
- * Splits markdown text into structural blocks.
38
- * Separates: headers, table rows, list items, horizontal rules, and prose.
39
- * Prose lines within the same paragraph are joined together.
40
- */
41
- function splitIntoBlocks(text) {
42
- const lines = text.split('\n');
43
- const blocks = [];
44
- let currentProse = '';
45
-
46
- const flushProse = () => {
47
- if (currentProse) {
48
- blocks.push(currentProse);
49
- currentProse = '';
50
- }
51
- };
52
-
53
- for (const line of lines) {
54
- const trimmed = line.trimStart();
55
-
56
- if (isTableRow(trimmed)) {
57
- flushProse();
58
- blocks.push(line);
59
- } else if (isTableSeparator(trimmed)) {
60
- flushProse();
61
- // Skip separator rows — not verifiable content
62
- } else if (isHeader(trimmed)) {
63
- flushProse();
64
- blocks.push(line);
65
- } else if (isHorizontalRule(trimmed)) {
66
- flushProse();
67
- // Skip horizontal rules
68
- } else if (isListItem(trimmed)) {
69
- flushProse();
70
- blocks.push(line);
71
- } else if (trimmed === '') {
72
- flushProse();
73
- } else {
74
- // Prose continuation — join into a single paragraph
75
- currentProse += (currentProse ? ' ' : '') + line.trim();
76
- }
77
- }
78
-
79
- flushProse();
80
- return blocks;
81
- }
82
-
83
- function isTableRow(line) {
84
- return /^\|.*\|/.test(line) && !isTableSeparator(line);
85
- }
86
-
87
- function isTableSeparator(line) {
88
- return /^\|[\s\-:|]+\|$/.test(line);
89
- }
90
-
91
- function isHeader(line) {
92
- return /^#{1,6}\s/.test(line);
93
- }
94
-
95
- function isHorizontalRule(line) {
96
- return /^(\*{3,}|-{3,}|_{3,})\s*$/.test(line);
97
- }
98
-
99
- function isListItem(line) {
100
- return /^(\d+\.\s+|[-*+]\s+)/.test(line);
101
- }
102
-
103
- /**
104
- * Returns true if a block is structural (table row, header, list item)
105
- * and should not be further split into sentences.
106
- */
107
- function isStructuralBlock(block) {
108
- const trimmed = block.trimStart();
109
- return isTableRow(trimmed) || isHeader(trimmed) || isListItem(trimmed);
110
- }
111
-
112
- /**
113
- * Splits a prose paragraph into sentences using sbd (sentence boundary detection),
114
- * merging short fragments that aren't independently verifiable.
115
- * sbd handles abbreviations (Dr., Mr., U.S., Ph.D.) correctly.
116
- */
117
- export function splitProse(text, maxSentences = 0) {
118
- // const segments = segment('en', text);
119
- const doc = nlp(text);
120
- const initialSentences = doc.sentences().out('array');
121
-
122
- // Find each sentence's position in the original text
123
- const positions = getPositions(text, initialSentences);
124
-
125
- // Merge short sentences (< MIN_SENTENCE_LENGTH) into the next sentence
126
- const merged = [];
127
- const mergedPositions = [];
128
- let pendingStart = null;
129
- for (let i = 0; i < initialSentences.length; i++) {
130
- if (pendingStart === null) {
131
- pendingStart = positions[i].start;
132
- }
133
- if (
134
- initialSentences[i].replaceAll('\n', '').length < MIN_SENTENCE_LENGTH &&
135
- i < initialSentences.length - 1
136
- ) {
137
- // Too short — will be merged with the next sentence
138
- continue;
139
- }
140
- const s = text.slice(pendingStart, positions[i].end);
141
- merged.push(s);
142
- mergedPositions.push({ start: pendingStart, end: positions[i].end });
143
- pendingStart = null;
144
- }
145
-
146
- if (maxSentences && merged.length > maxSentences) {
147
- // Merge groups by slicing original text to preserve separators
148
- const groupSize = Math.ceil(merged.length / maxSentences);
149
- const groupedSentences = [];
150
- for (let i = 0; i < merged.length; i += groupSize) {
151
- const groupStart = mergedPositions[i].start;
152
- const groupEnd =
153
- mergedPositions[Math.min(i + groupSize, merged.length) - 1].end;
154
- groupedSentences.push(text.slice(groupStart, groupEnd));
155
- }
156
- return groupedSentences;
157
- }
158
-
159
- return merged;
160
- }
161
-
162
- function getPositions(text, sentences) {
163
- const positions = [];
164
- let searchFrom = 0;
165
- for (const sentence of sentences) {
166
- const start = text.indexOf(sentence, searchFrom);
167
- const end = start + sentence.length;
168
- positions.push({ start, end });
169
- searchFrom = end;
170
- }
171
- return positions;
172
- }
@@ -1,133 +0,0 @@
1
- import { splitMarkdown } from './markdown-splitter';
2
-
3
- const DUMMY_LLM_RESPONSE = `## European species most at risk
4
-
5
- Below is a concise, evidence-based snapshot of the taxonomic groups and flagship species. The list combines information on trend magnitude and risk status.
6
-
7
- | Taxonomic group | Species at risk | Main drivers | Signal |
8
- |-----------------|-----------------|--------------|--------|
9
- | **Birds** | Skylark (*Alauda arvensis*), European turtle-dove | Intensive agriculture, pesticide use | Farmland-bird index down 27% (1990-2019) |
10
- | **Fish** | Houting (*Coregonus oxyrhynchus*), Danube salmon | River regulation, dam construction | Both listed as critically endangered |
11
- | **Butterflies** | Large blue (*Phengaris arion*), Adonis blue | Loss of semi-natural grasslands | Grassland-butterfly index down 25% |
12
-
13
- ### Key take-aways
14
-
15
- 1. **Agricultural intensification** is the single biggest pressure across taxa.
16
- 2. **Habitat loss and degradation** underlie the perilous status of many specialists.
17
- 3. **Chemical stressors** affect both insects and the birds that depend on them.
18
- 4. **Climate change** amplifies existing pressures such as drought stress on amphibians.
19
-
20
- ---
21
-
22
- ### Conservation priorities
23
-
24
- - **Protect and restore semi-natural habitats** including high-diversity grasslands and river floodplains.
25
- - **Implement agri-environment schemes** that limit pesticide use and maintain field margins.
26
- - **Focus monitoring on flagship species** to gauge the effectiveness of policy actions.
27
-
28
- If you need a deeper dive into a particular taxon, just let me know!`;
29
-
30
- describe('splitMarkdown', () => {
31
- it('should split the dummy LLM response into meaningful segments', () => {
32
- const segments = splitMarkdown(DUMMY_LLM_RESPONSE);
33
-
34
- // Headers are separate segments
35
- expect(segments).toContain('## European species most at risk');
36
- expect(segments).toContain('### Key take-aways');
37
- expect(segments).toContain('### Conservation priorities');
38
-
39
- // Table rows are individual segments (separator skipped)
40
- const tableRows = segments.filter((s) => s.startsWith('|'));
41
- expect(tableRows.length).toBe(4); // header row + 3 data rows
42
-
43
- // Numbered list items are separate
44
- const numberedItems = segments.filter((s) => /^\d+\./.test(s.trimStart()));
45
- expect(numberedItems.length).toBe(4);
46
-
47
- // Bullet list items are separate
48
- const bulletItems = segments.filter((s) => /^-\s/.test(s.trimStart()));
49
- expect(bulletItems.length).toBe(3);
50
-
51
- // Prose paragraphs are split into sentences
52
- expect(segments).toContainEqual(
53
- expect.stringContaining('evidence-based snapshot'),
54
- );
55
- expect(segments).toContainEqual(expect.stringContaining('trend magnitude'));
56
-
57
- // Horizontal rules are skipped
58
- expect(segments).not.toContainEqual(expect.stringMatching(/^-{3,}$/));
59
-
60
- // No empty segments
61
- expect(segments.every((s) => s.trim().length > 0)).toBe(true);
62
- });
63
-
64
- it('should handle plain prose without markdown', () => {
65
- const text =
66
- 'This is sentence one. This is sentence two. This is sentence three.';
67
- const segments = splitMarkdown(text);
68
- expect(segments).toEqual([
69
- 'This is sentence one.',
70
- 'This is sentence two.',
71
- 'This is sentence three.',
72
- ]);
73
- });
74
-
75
- it('should handle headers followed by prose', () => {
76
- const text = '## My Header\n\nSome prose paragraph here.';
77
- const segments = splitMarkdown(text);
78
- expect(segments).toEqual(['## My Header', 'Some prose paragraph here.']);
79
- });
80
-
81
- it('should skip table separator rows', () => {
82
- const text = '| A | B |\n|---|---|\n| 1 | 2 |';
83
- const segments = splitMarkdown(text);
84
- expect(segments).toEqual(['| A | B |', '| 1 | 2 |']);
85
- });
86
-
87
- it('should handle numbered lists', () => {
88
- const text =
89
- '1. First item with enough text.\n2. Second item also long enough.';
90
- const segments = splitMarkdown(text);
91
- expect(segments).toEqual([
92
- '1. First item with enough text.',
93
- '2. Second item also long enough.',
94
- ]);
95
- });
96
-
97
- it('should handle bullet lists', () => {
98
- const text =
99
- '- First bullet point.\n- Second bullet point.\n- Third bullet point.';
100
- const segments = splitMarkdown(text);
101
- expect(segments).toEqual([
102
- '- First bullet point.',
103
- '- Second bullet point.',
104
- '- Third bullet point.',
105
- ]);
106
- });
107
-
108
- it('should skip horizontal rules', () => {
109
- const text = 'Before the rule.\n\n---\n\nAfter the rule.';
110
- const segments = splitMarkdown(text);
111
- expect(segments).toEqual(['Before the rule.', 'After the rule.']);
112
- });
113
-
114
- it('should return an empty array for empty string', () => {
115
- expect(splitMarkdown('')).toEqual([]);
116
- });
117
-
118
- it('should merge short prose fragments', () => {
119
- const text = 'Hi. This is a longer sentence here.';
120
- const segments = splitMarkdown(text);
121
- expect(segments).toEqual(['Hi. This is a longer sentence here.']);
122
- });
123
-
124
- it('should join multi-line prose into a single paragraph before splitting', () => {
125
- const text =
126
- 'This is the first line of a paragraph.\nThis is the second line of the same paragraph.';
127
- const segments = splitMarkdown(text);
128
- // Should be joined and then split by Intl.Segmenter
129
- expect(segments.length).toBeGreaterThanOrEqual(1);
130
- expect(segments.join('')).toContain('first line');
131
- expect(segments.join('')).toContain('second line');
132
- });
133
- });