@eeacms/volto-eea-chatbot 2.0.1 → 2.0.2

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.
Files changed (80) hide show
  1. package/.eslintrc.js +6 -6
  2. package/CHANGELOG.md +2 -0
  3. package/jest-addon.config.js +1 -0
  4. package/package.json +1 -1
  5. package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
  6. package/src/ChatBlock/chat/AIMessage.tsx +20 -16
  7. package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
  8. package/src/ChatBlock/chat/ChatWindow.tsx +10 -11
  9. package/src/ChatBlock/chat/UserMessage.tsx +4 -4
  10. package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
  11. package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
  12. package/src/ChatBlock/components/EmptyState.jsx +1 -1
  13. package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
  14. package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
  15. package/src/ChatBlock/components/Source.jsx +2 -2
  16. package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
  17. package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
  18. package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
  19. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
  20. package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
  21. package/src/ChatBlock/hooks/useChatController.ts +7 -4
  22. package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
  23. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +1 -1
  24. package/src/ChatBlock/packets/MultiToolRenderer.tsx +11 -12
  25. package/src/ChatBlock/packets/RendererComponent.tsx +6 -3
  26. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
  27. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
  28. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
  29. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -8
  30. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +5 -5
  31. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +10 -10
  32. package/src/ChatBlock/services/messageProcessor.ts +6 -3
  33. package/src/ChatBlock/services/packetUtils.ts +2 -2
  34. package/src/ChatBlock/services/streamingService.ts +8 -2
  35. package/src/ChatBlock/utils/citations.ts +1 -1
  36. package/src/halloumi/filtering.test.js +199 -1
  37. package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
  38. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
  39. package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
  40. package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
  41. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
  42. package/src/ChatBlock/tests/Citation.test.jsx +0 -107
  43. package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
  44. package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
  45. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
  46. package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
  47. package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
  48. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
  49. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
  50. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
  51. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
  52. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
  53. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
  54. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
  55. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
  56. package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
  57. package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
  58. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
  59. package/src/ChatBlock/tests/Source.test.jsx +0 -79
  60. package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
  61. package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
  62. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
  63. package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
  64. package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
  65. package/src/ChatBlock/tests/citations.test.js +0 -114
  66. package/src/ChatBlock/tests/index.test.js +0 -51
  67. package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
  68. package/src/ChatBlock/tests/packetUtils.test.js +0 -158
  69. package/src/ChatBlock/tests/schema.test.js +0 -166
  70. package/src/ChatBlock/tests/streamingService.test.js +0 -467
  71. package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
  72. package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
  73. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
  74. package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
  75. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
  76. package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
  77. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
  78. package/src/ChatBlock/tests/utils.test.jsx +0 -241
  79. package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
  80. /package/src/ChatBlock/{schema.js → schema.jsx} +0 -0
@@ -1,166 +0,0 @@
1
- import '@testing-library/jest-dom';
2
- import { ChatBlockSchema } from '../schema';
3
-
4
- describe('ChatBlockSchema', () => {
5
- const mockAssistants = [
6
- { id: 1, name: 'Assistant 1' },
7
- { id: 2, name: 'Assistant 2' },
8
- ];
9
-
10
- it('returns schema object with title', () => {
11
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
12
- expect(schema.title).toBe('Chatbot');
13
- });
14
-
15
- it('returns schema with fieldsets', () => {
16
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
17
- expect(schema.fieldsets).toBeDefined();
18
- expect(Array.isArray(schema.fieldsets)).toBe(true);
19
- expect(schema.fieldsets.length).toBeGreaterThan(0);
20
- });
21
-
22
- it('returns schema with properties', () => {
23
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
24
- expect(schema.properties).toBeDefined();
25
- });
26
-
27
- it('includes assistant choices from assistants array', () => {
28
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
29
- expect(schema.properties.assistant.choices).toEqual([
30
- ['1', 'Assistant 1'],
31
- ['2', 'Assistant 2'],
32
- ]);
33
- });
34
-
35
- it('handles empty assistants array', () => {
36
- const schema = ChatBlockSchema({ assistants: [], data: {} });
37
- expect(schema.properties.assistant.choices).toEqual([]);
38
- });
39
-
40
- it('handles undefined assistants', () => {
41
- const schema = ChatBlockSchema({ assistants: undefined, data: {} });
42
- expect(schema.properties.assistant.choices).toEqual([]);
43
- });
44
-
45
- it('includes starterPrompts field when enableStarterPrompts is true', () => {
46
- const schema = ChatBlockSchema({
47
- assistants: mockAssistants,
48
- data: { enableStarterPrompts: true },
49
- });
50
- expect(schema.fieldsets[0].fields).toContain('starterPrompts');
51
- });
52
-
53
- it('excludes starterPrompts field when enableStarterPrompts is false', () => {
54
- const schema = ChatBlockSchema({
55
- assistants: mockAssistants,
56
- data: { enableStarterPrompts: false },
57
- });
58
- expect(schema.fieldsets[0].fields).not.toContain('starterPrompts');
59
- });
60
-
61
- it('includes feedbackReasons field when enableFeedback is true', () => {
62
- const schema = ChatBlockSchema({
63
- assistants: mockAssistants,
64
- data: { enableFeedback: true },
65
- });
66
- expect(schema.fieldsets[0].fields).toContain('feedbackReasons');
67
- });
68
-
69
- it('excludes feedbackReasons field when enableFeedback is false', () => {
70
- const schema = ChatBlockSchema({
71
- assistants: mockAssistants,
72
- data: { enableFeedback: false },
73
- });
74
- expect(schema.fieldsets[0].fields).not.toContain('feedbackReasons');
75
- });
76
-
77
- it('includes quality check fields when qualityCheck is enabled', () => {
78
- const schema = ChatBlockSchema({
79
- assistants: mockAssistants,
80
- data: { qualityCheck: 'enabled' },
81
- });
82
- expect(schema.fieldsets[0].fields).toContain('maxContextSegments');
83
- expect(schema.fieldsets[0].fields).toContain('noSupportDocumentsMessage');
84
- expect(schema.fieldsets[0].fields).toContain('qualityCheckContext');
85
- expect(schema.fieldsets[0].fields).toContain('qualityCheckStages');
86
- });
87
-
88
- it('excludes quality check fields when qualityCheck is disabled', () => {
89
- const schema = ChatBlockSchema({
90
- assistants: mockAssistants,
91
- data: { qualityCheck: 'disabled' },
92
- });
93
- expect(schema.fieldsets[0].fields).not.toContain('maxContextSegments');
94
- expect(schema.fieldsets[0].fields).not.toContain('qualityCheckContext');
95
- });
96
-
97
- it('includes onDemandInputToggle when qualityCheck is ondemand_toggle', () => {
98
- const schema = ChatBlockSchema({
99
- assistants: mockAssistants,
100
- data: { qualityCheck: 'ondemand_toggle' },
101
- });
102
- expect(schema.fieldsets[0].fields).toContain('onDemandInputToggle');
103
- });
104
-
105
- it('excludes onDemandInputToggle when qualityCheck is not ondemand_toggle', () => {
106
- const schema = ChatBlockSchema({
107
- assistants: mockAssistants,
108
- data: { qualityCheck: 'enabled' },
109
- });
110
- expect(schema.fieldsets[0].fields).not.toContain('onDemandInputToggle');
111
- });
112
-
113
- it('includes totalFailMessage when enableShowTotalFailMessage is true', () => {
114
- const schema = ChatBlockSchema({
115
- assistants: mockAssistants,
116
- data: { enableShowTotalFailMessage: true },
117
- });
118
- expect(schema.fieldsets[0].fields).toContain('totalFailMessage');
119
- });
120
-
121
- it('excludes totalFailMessage when enableShowTotalFailMessage is false', () => {
122
- const schema = ChatBlockSchema({
123
- assistants: mockAssistants,
124
- data: { enableShowTotalFailMessage: false },
125
- });
126
- expect(schema.fieldsets[0].fields).not.toContain('totalFailMessage');
127
- });
128
-
129
- it('has required array', () => {
130
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
131
- expect(schema.required).toBeDefined();
132
- expect(Array.isArray(schema.required)).toBe(true);
133
- });
134
-
135
- it('has default values for key properties', () => {
136
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
137
- expect(schema.properties.placeholderPrompt.default).toBe('Ask a question');
138
- expect(schema.properties.chatTitle.default).toBe('Online public chat');
139
- expect(schema.properties.enableFeedback.default).toBe(true);
140
- expect(schema.properties.qualityCheck.default).toBe('disabled');
141
- });
142
-
143
- it('has qualityCheckStages with default score ranges', () => {
144
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
145
- expect(schema.properties.qualityCheckStages.default).toBeDefined();
146
- expect(Array.isArray(schema.properties.qualityCheckStages.default)).toBe(
147
- true,
148
- );
149
- expect(schema.properties.qualityCheckStages.default.length).toBe(5);
150
- });
151
-
152
- it('has feedbackReasons with default choices', () => {
153
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
154
- expect(schema.properties.feedbackReasons.default).toBeDefined();
155
- expect(Array.isArray(schema.properties.feedbackReasons.default)).toBe(true);
156
- });
157
-
158
- it('has deepResearch property with choices', () => {
159
- const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
160
- expect(schema.properties.deepResearch.choices).toBeDefined();
161
- expect(schema.properties.deepResearch.choices).toContainEqual([
162
- 'always_on',
163
- 'Always on',
164
- ]);
165
- });
166
- });
@@ -1,467 +0,0 @@
1
- import { TextEncoder, TextDecoder } from 'util';
2
-
3
- import {
4
- processRawChunkString,
5
- handleStream,
6
- createChatSession,
7
- submitFeedback,
8
- regenerateMessage,
9
- } from '../services/streamingService';
10
- import { PacketType } from '../types/streamingModels';
11
-
12
- global.TextEncoder = TextEncoder;
13
- global.TextDecoder = TextDecoder;
14
-
15
- // ── processRawChunkString ──────────────────────────────────────────────
16
-
17
- describe('processRawChunkString', () => {
18
- it('returns empty array for empty string', () => {
19
- const [chunks, partial] = processRawChunkString('', null);
20
- expect(chunks).toEqual([]);
21
- expect(partial).toBeNull();
22
- });
23
-
24
- it('parses a single complete JSON chunk', () => {
25
- const raw = JSON.stringify({ ind: 1, obj: { type: 'message_delta' } });
26
- const [chunks, partial] = processRawChunkString(raw, null);
27
- expect(chunks).toHaveLength(1);
28
- expect(chunks[0]).toEqual({ ind: 1, obj: { type: 'message_delta' } });
29
- expect(partial).toBeNull();
30
- });
31
-
32
- it('parses multiple newline-separated JSON chunks', () => {
33
- const c1 = JSON.stringify({ ind: 1, obj: { type: 'message_start' } });
34
- const c2 = JSON.stringify({ ind: 2, obj: { type: 'message_delta' } });
35
- const raw = `${c1}\n${c2}`;
36
- const [chunks, partial] = processRawChunkString(raw, null);
37
- expect(chunks).toHaveLength(2);
38
- expect(chunks[0].ind).toBe(1);
39
- expect(chunks[1].ind).toBe(2);
40
- expect(partial).toBeNull();
41
- });
42
-
43
- it('handles incomplete chunk and returns it as partial', () => {
44
- const raw = '{"ind": 1, "obj": {"type"';
45
- const [chunks, partial] = processRawChunkString(raw, null);
46
- expect(chunks).toHaveLength(0);
47
- expect(partial).toBe(raw);
48
- });
49
-
50
- it('combines previous partial chunk with new data', () => {
51
- const prev = '{"ind": 1, "obj": {"type"';
52
- const rest = ': "message_delta"}}';
53
- const [chunks, partial] = processRawChunkString(rest, prev);
54
- expect(chunks).toHaveLength(1);
55
- expect(chunks[0]).toEqual({ ind: 1, obj: { type: 'message_delta' } });
56
- expect(partial).toBeNull();
57
- });
58
-
59
- it('filters out empty lines', () => {
60
- const c1 = JSON.stringify({ ind: 1, obj: { type: 'stop' } });
61
- const raw = `\n${c1}\n\n`;
62
- const [chunks, partial] = processRawChunkString(raw, null);
63
- expect(chunks).toHaveLength(1);
64
- expect(partial).toBeNull();
65
- });
66
-
67
- it('returns null for falsy input', () => {
68
- const [chunks, partial] = processRawChunkString(null, null);
69
- expect(chunks).toEqual([]);
70
- expect(partial).toBeNull();
71
- });
72
- });
73
-
74
- // ── handleStream ───────────────────────────────────────────────────────
75
-
76
- describe('handleStream', () => {
77
- function makeReadableStream(chunks) {
78
- let index = 0;
79
- return {
80
- body: {
81
- getReader() {
82
- return {
83
- async read() {
84
- if (index >= chunks.length) {
85
- return { done: true, value: undefined };
86
- }
87
- const value = new TextEncoder().encode(chunks[index++]);
88
- return { done: false, value };
89
- },
90
- };
91
- },
92
- },
93
- };
94
- }
95
-
96
- it('yields packets from a stream with ind/obj format', async () => {
97
- const packet = JSON.stringify({
98
- ind: 1,
99
- obj: { type: PacketType.MESSAGE_DELTA, content: 'hello' },
100
- });
101
- const response = makeReadableStream([packet]);
102
- const allPackets = [];
103
- for await (const packets of handleStream(response)) {
104
- allPackets.push(...packets);
105
- }
106
- expect(allPackets).toHaveLength(1);
107
- expect(allPackets[0].obj.type).toBe(PacketType.MESSAGE_DELTA);
108
- });
109
-
110
- it('handles MessageResponseIDInfo format', async () => {
111
- const chunk = JSON.stringify({
112
- user_message_id: 42,
113
- reserved_assistant_message_id: 43,
114
- });
115
- const response = makeReadableStream([chunk]);
116
- const allPackets = [];
117
- for await (const packets of handleStream(response)) {
118
- allPackets.push(...packets);
119
- }
120
- expect(allPackets).toHaveLength(1);
121
- expect(allPackets[0].obj.type).toBe(PacketType.MESSAGE_END_ID_INFO);
122
- expect(allPackets[0].obj.user_message_id).toBe(42);
123
- });
124
-
125
- it('handles error format', async () => {
126
- const chunk = JSON.stringify({ error: 'something went wrong' });
127
- const response = makeReadableStream([chunk]);
128
- const allPackets = [];
129
- for await (const packets of handleStream(response)) {
130
- allPackets.push(...packets);
131
- }
132
- expect(allPackets).toHaveLength(1);
133
- expect(allPackets[0].obj.type).toBe(PacketType.ERROR);
134
- expect(allPackets[0].obj.error).toBe('something went wrong');
135
- });
136
-
137
- it('throws when no reader is available', async () => {
138
- const response = { body: null };
139
- const gen = handleStream(response);
140
- await expect(gen.next()).rejects.toThrow('No reader available');
141
- });
142
-
143
- it('handles multiple chunks across reads', async () => {
144
- const c1 = JSON.stringify({
145
- ind: 1,
146
- obj: { type: PacketType.MESSAGE_START, content: 'hi' },
147
- });
148
- const c2 = JSON.stringify({
149
- ind: 2,
150
- obj: { type: PacketType.MESSAGE_DELTA, content: ' world' },
151
- });
152
- const response = makeReadableStream([c1, c2]);
153
- const allPackets = [];
154
- for await (const packets of handleStream(response)) {
155
- allPackets.push(...packets);
156
- }
157
- expect(allPackets).toHaveLength(2);
158
- });
159
-
160
- it('skips non-object chunks', async () => {
161
- // A string that parses to a non-object (number)
162
- const response = makeReadableStream(['42']);
163
- const allPackets = [];
164
- for await (const packets of handleStream(response)) {
165
- allPackets.push(...packets);
166
- }
167
- expect(allPackets).toHaveLength(0);
168
- });
169
-
170
- it('handles split chunk across two reads', async () => {
171
- const full = JSON.stringify({
172
- ind: 1,
173
- obj: { type: PacketType.STOP },
174
- });
175
- const half1 = full.slice(0, 10);
176
- const half2 = full.slice(10);
177
- const response = makeReadableStream([half1, half2]);
178
- const allPackets = [];
179
- for await (const packets of handleStream(response)) {
180
- allPackets.push(...packets);
181
- }
182
- expect(allPackets).toHaveLength(1);
183
- expect(allPackets[0].obj.type).toBe(PacketType.STOP);
184
- });
185
- });
186
-
187
- // ── createChatSession ──────────────────────────────────────────────────
188
-
189
- describe('createChatSession', () => {
190
- beforeEach(() => {
191
- global.fetch = jest.fn();
192
- });
193
-
194
- afterEach(() => {
195
- jest.restoreAllMocks();
196
- });
197
-
198
- it('returns chat session id on success', async () => {
199
- global.fetch.mockResolvedValue({
200
- ok: true,
201
- json: () => Promise.resolve({ chat_session_id: 'abc-123' }),
202
- });
203
-
204
- const result = await createChatSession(1, 'Test session');
205
- expect(result).toBe('abc-123');
206
- expect(global.fetch).toHaveBeenCalledWith(
207
- '/_da/chat/create-chat-session',
208
- expect.objectContaining({
209
- method: 'POST',
210
- body: JSON.stringify({ persona_id: 1, description: 'Test session' }),
211
- }),
212
- );
213
- });
214
-
215
- it('throws on failed response', async () => {
216
- global.fetch.mockResolvedValue({ ok: false });
217
- await expect(createChatSession(1)).rejects.toThrow(
218
- 'Failed to create chat session',
219
- );
220
- });
221
- });
222
-
223
- // ── submitFeedback ─────────────────────────────────────────────────────
224
-
225
- describe('submitFeedback', () => {
226
- beforeEach(() => {
227
- global.fetch = jest.fn();
228
- });
229
-
230
- afterEach(() => {
231
- jest.restoreAllMocks();
232
- });
233
-
234
- it('sends positive feedback', async () => {
235
- global.fetch.mockResolvedValue({
236
- ok: true,
237
- json: () => Promise.resolve({}),
238
- });
239
-
240
- await submitFeedback({
241
- chatMessageId: 1,
242
- isPositive: true,
243
- feedbackText: 'Great!',
244
- });
245
-
246
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
247
- expect(body.is_positive).toBe(true);
248
- expect(body.chat_message_id).toBe(1);
249
- expect(body.predefined_feedback).toBeUndefined();
250
- });
251
-
252
- it('sends negative feedback with predefined reason', async () => {
253
- global.fetch.mockResolvedValue({
254
- ok: true,
255
- json: () => Promise.resolve({}),
256
- });
257
-
258
- await submitFeedback({
259
- chatMessageId: 2,
260
- isPositive: false,
261
- predefinedFeedback: 'Inaccurate',
262
- });
263
-
264
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
265
- expect(body.is_positive).toBe(false);
266
- expect(body.predefined_feedback).toBe('Inaccurate');
267
- });
268
-
269
- it('throws on failed response', async () => {
270
- global.fetch.mockResolvedValue({ ok: false });
271
- await expect(
272
- submitFeedback({ chatMessageId: 1, isPositive: true }),
273
- ).rejects.toThrow('Failed to submit feedback');
274
- });
275
- });
276
-
277
- // ── sendMessage ──────────────────────────────────────────────────────
278
-
279
- describe('sendMessage', () => {
280
- const { sendMessage } = require('../services/streamingService');
281
-
282
- beforeEach(() => {
283
- global.fetch = jest.fn();
284
- });
285
-
286
- afterEach(() => {
287
- jest.restoreAllMocks();
288
- });
289
-
290
- function makeStreamResponse(chunks) {
291
- let index = 0;
292
- return {
293
- ok: true,
294
- body: {
295
- getReader() {
296
- return {
297
- async read() {
298
- if (index >= chunks.length) {
299
- return { done: true, value: undefined };
300
- }
301
- const value = new TextEncoder().encode(chunks[index++]);
302
- return { done: false, value };
303
- },
304
- };
305
- },
306
- },
307
- };
308
- }
309
-
310
- it('sends message and yields packets', async () => {
311
- const chunk = JSON.stringify({
312
- ind: 1,
313
- obj: { type: 'message_delta', content: 'hello' },
314
- });
315
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
316
-
317
- const allPackets = [];
318
- for await (const packets of sendMessage({
319
- message: 'test',
320
- chatSessionId: 'session-1',
321
- parentMessageId: null,
322
- regenerate: false,
323
- filters: null,
324
- selectedDocumentIds: [],
325
- })) {
326
- allPackets.push(...packets);
327
- }
328
-
329
- expect(allPackets).toHaveLength(1);
330
- expect(allPackets[0].obj.content).toBe('hello');
331
- expect(global.fetch).toHaveBeenCalledWith(
332
- '/_da/chat/send-message',
333
- expect.objectContaining({ method: 'POST' }),
334
- );
335
- });
336
-
337
- it('uses _rq middleware for related questions', async () => {
338
- const chunk = JSON.stringify({
339
- ind: 1,
340
- obj: { type: 'stop' },
341
- });
342
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
343
-
344
- const allPackets = [];
345
- for await (const packets of sendMessage(
346
- {
347
- message: 'test',
348
- chatSessionId: 'session-1',
349
- parentMessageId: null,
350
- regenerate: false,
351
- filters: null,
352
- selectedDocumentIds: [],
353
- },
354
- true,
355
- )) {
356
- allPackets.push(...packets);
357
- }
358
-
359
- expect(global.fetch).toHaveBeenCalledWith(
360
- '/_rq/chat/send-message',
361
- expect.objectContaining({ method: 'POST' }),
362
- );
363
- });
364
-
365
- it('throws on failed response', async () => {
366
- global.fetch.mockResolvedValue({
367
- ok: false,
368
- json: () => Promise.resolve({ message: 'Bad request' }),
369
- });
370
-
371
- const gen = sendMessage({
372
- message: 'test',
373
- chatSessionId: 'session-1',
374
- parentMessageId: null,
375
- regenerate: false,
376
- filters: null,
377
- selectedDocumentIds: [],
378
- });
379
-
380
- await expect(gen.next()).rejects.toThrow('Failed to send message');
381
- });
382
-
383
- it('includes selectedDocumentIds in payload when provided', async () => {
384
- const chunk = JSON.stringify({
385
- ind: 1,
386
- obj: { type: 'stop' },
387
- });
388
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
389
-
390
- // eslint-disable-next-line no-unused-vars
391
- for await (const _ of sendMessage({
392
- message: 'test',
393
- chatSessionId: 'session-1',
394
- parentMessageId: null,
395
- regenerate: false,
396
- filters: null,
397
- selectedDocumentIds: ['doc1', 'doc2'],
398
- })) {
399
- // consume
400
- }
401
-
402
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
403
- expect(body.search_doc_ids).toEqual(['doc1', 'doc2']);
404
- });
405
-
406
- it('includes LLM override when temperature is set', async () => {
407
- const chunk = JSON.stringify({
408
- ind: 1,
409
- obj: { type: 'stop' },
410
- });
411
- global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
412
-
413
- // eslint-disable-next-line no-unused-vars
414
- for await (const _ of sendMessage({
415
- message: 'test',
416
- chatSessionId: 'session-1',
417
- parentMessageId: null,
418
- regenerate: false,
419
- filters: null,
420
- selectedDocumentIds: [],
421
- temperature: 0.7,
422
- modelVersion: 'gpt-4',
423
- })) {
424
- // consume
425
- }
426
-
427
- const body = JSON.parse(global.fetch.mock.calls[0][1].body);
428
- expect(body.llm_override).toEqual(
429
- expect.objectContaining({
430
- temperature: 0.7,
431
- model_version: 'gpt-4',
432
- }),
433
- );
434
- });
435
- });
436
-
437
- // ── regenerateMessage ──────────────────────────────────────────────────
438
-
439
- describe('regenerateMessage', () => {
440
- beforeEach(() => {
441
- global.fetch = jest.fn();
442
- });
443
-
444
- afterEach(() => {
445
- jest.restoreAllMocks();
446
- });
447
-
448
- it('sends regenerate request', async () => {
449
- global.fetch.mockResolvedValue({ ok: true });
450
- await regenerateMessage(1, 2);
451
-
452
- expect(global.fetch).toHaveBeenCalledWith(
453
- '/_da/chat/regenerate-message',
454
- expect.objectContaining({
455
- method: 'POST',
456
- body: JSON.stringify({ message_id: 1, chat_session_id: 2 }),
457
- }),
458
- );
459
- });
460
-
461
- it('throws on failed response', async () => {
462
- global.fetch.mockResolvedValue({ ok: false });
463
- await expect(regenerateMessage(1, 2)).rejects.toThrow(
464
- 'Failed to regenerate message',
465
- );
466
- });
467
- });