expo-ai-kit 0.3.6 → 0.4.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.
- package/README.md +68 -1106
- package/build/ExpoAiKitModule.d.ts +1 -1
- package/build/ExpoAiKitModule.d.ts.map +1 -1
- package/build/ExpoAiKitModule.js.map +1 -1
- package/build/index.d.ts +1 -436
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -737
- package/build/index.js.map +1 -1
- package/build/types.d.ts +3 -244
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoAiKitModule.ts +2 -2
- package/src/index.ts +1 -943
- package/src/types.ts +3 -284
- package/build/hooks.d.ts +0 -147
- package/build/hooks.d.ts.map +0 -1
- package/build/hooks.js +0 -426
- package/build/hooks.js.map +0 -1
- package/build/memory.d.ts +0 -235
- package/build/memory.d.ts.map +0 -1
- package/build/memory.js +0 -353
- package/build/memory.js.map +0 -1
- package/src/__tests__/index.test.js +0 -630
- package/src/hooks.ts +0 -502
- package/src/memory.ts +0 -394
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
// Mock Platform before importing anything
|
|
2
|
-
let mockPlatformOS = 'ios';
|
|
3
|
-
|
|
4
|
-
jest.mock('react-native', () => ({
|
|
5
|
-
Platform: {
|
|
6
|
-
get OS() {
|
|
7
|
-
return mockPlatformOS;
|
|
8
|
-
},
|
|
9
|
-
},
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
// Store event listeners for testing - must be prefixed with 'mock' for jest
|
|
13
|
-
let mockEventListeners = {};
|
|
14
|
-
|
|
15
|
-
// Mock the native module
|
|
16
|
-
jest.mock('../ExpoAiKitModule', () => ({
|
|
17
|
-
__esModule: true,
|
|
18
|
-
default: {
|
|
19
|
-
isAvailable: jest.fn(() => true),
|
|
20
|
-
sendMessage: jest.fn(() => Promise.resolve({ text: 'Mock response' })),
|
|
21
|
-
startStreaming: jest.fn(() => Promise.resolve()),
|
|
22
|
-
stopStreaming: jest.fn(() => Promise.resolve()),
|
|
23
|
-
addListener: jest.fn((eventName, callback) => {
|
|
24
|
-
if (!mockEventListeners[eventName]) {
|
|
25
|
-
mockEventListeners[eventName] = [];
|
|
26
|
-
}
|
|
27
|
-
mockEventListeners[eventName].push(callback);
|
|
28
|
-
return {
|
|
29
|
-
remove: () => {
|
|
30
|
-
mockEventListeners[eventName] = mockEventListeners[eventName].filter(
|
|
31
|
-
(cb) => cb !== callback
|
|
32
|
-
);
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}),
|
|
36
|
-
},
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
// Helper to simulate native events
|
|
40
|
-
const simulateStreamEvent = (event) => {
|
|
41
|
-
const listeners = mockEventListeners['onStreamToken'] || [];
|
|
42
|
-
listeners.forEach((cb) => cb(event));
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const {
|
|
46
|
-
isAvailable,
|
|
47
|
-
sendMessage,
|
|
48
|
-
streamMessage,
|
|
49
|
-
summarize,
|
|
50
|
-
streamSummarize,
|
|
51
|
-
translate,
|
|
52
|
-
streamTranslate,
|
|
53
|
-
rewrite,
|
|
54
|
-
streamRewrite,
|
|
55
|
-
extractKeyPoints,
|
|
56
|
-
streamExtractKeyPoints,
|
|
57
|
-
answerQuestion,
|
|
58
|
-
streamAnswerQuestion,
|
|
59
|
-
} = require('../index');
|
|
60
|
-
const NativeModule = require('../ExpoAiKitModule').default;
|
|
61
|
-
|
|
62
|
-
describe('expo-ai-kit', () => {
|
|
63
|
-
beforeEach(() => {
|
|
64
|
-
jest.clearAllMocks();
|
|
65
|
-
mockPlatformOS = 'ios';
|
|
66
|
-
mockEventListeners = {};
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('isAvailable', () => {
|
|
70
|
-
it('returns native module result on iOS', async () => {
|
|
71
|
-
const result = await isAvailable();
|
|
72
|
-
expect(result).toBe(true);
|
|
73
|
-
expect(NativeModule.isAvailable).toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('returns native module result on Android', async () => {
|
|
77
|
-
mockPlatformOS = 'android';
|
|
78
|
-
const result = await isAvailable();
|
|
79
|
-
expect(result).toBe(true);
|
|
80
|
-
expect(NativeModule.isAvailable).toHaveBeenCalled();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('returns false on web', async () => {
|
|
84
|
-
mockPlatformOS = 'web';
|
|
85
|
-
const result = await isAvailable();
|
|
86
|
-
expect(result).toBe(false);
|
|
87
|
-
expect(NativeModule.isAvailable).not.toHaveBeenCalled();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('sendMessage', () => {
|
|
92
|
-
it('throws error for empty messages array', async () => {
|
|
93
|
-
await expect(sendMessage([])).rejects.toThrow(
|
|
94
|
-
'messages array cannot be empty'
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('calls native module with messages and default system prompt', async () => {
|
|
99
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
100
|
-
|
|
101
|
-
const result = await sendMessage(messages);
|
|
102
|
-
|
|
103
|
-
expect(result).toEqual({ text: 'Mock response' });
|
|
104
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
105
|
-
messages,
|
|
106
|
-
'You are a helpful, friendly assistant. Answer the user directly and concisely.'
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('uses custom system prompt from options', async () => {
|
|
111
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
112
|
-
|
|
113
|
-
await sendMessage(messages, { systemPrompt: 'You are a pirate.' });
|
|
114
|
-
|
|
115
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
116
|
-
messages,
|
|
117
|
-
'You are a pirate.'
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('passes empty string for system prompt when messages contain system message', async () => {
|
|
122
|
-
const messages = [
|
|
123
|
-
{ role: 'system', content: 'Be brief.' },
|
|
124
|
-
{ role: 'user', content: 'Hello' },
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
await sendMessage(messages, { systemPrompt: 'This should be ignored' });
|
|
128
|
-
|
|
129
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
130
|
-
messages,
|
|
131
|
-
'' // Empty because system message exists in array
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('returns empty response on unsupported platforms', async () => {
|
|
136
|
-
mockPlatformOS = 'web';
|
|
137
|
-
|
|
138
|
-
const result = await sendMessage([{ role: 'user', content: 'Hi' }]);
|
|
139
|
-
|
|
140
|
-
expect(result).toEqual({ text: '' });
|
|
141
|
-
expect(NativeModule.sendMessage).not.toHaveBeenCalled();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('message validation', () => {
|
|
146
|
-
it('accepts valid user message', async () => {
|
|
147
|
-
await expect(
|
|
148
|
-
sendMessage([{ role: 'user', content: 'Test' }])
|
|
149
|
-
).resolves.not.toThrow();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('accepts valid multi-turn conversation', async () => {
|
|
153
|
-
await expect(
|
|
154
|
-
sendMessage([
|
|
155
|
-
{ role: 'system', content: 'Be helpful.' },
|
|
156
|
-
{ role: 'user', content: 'Hi' },
|
|
157
|
-
{ role: 'assistant', content: 'Hello!' },
|
|
158
|
-
{ role: 'user', content: 'How are you?' },
|
|
159
|
-
])
|
|
160
|
-
).resolves.not.toThrow();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('streamMessage', () => {
|
|
165
|
-
it('returns promise and stop function', () => {
|
|
166
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
167
|
-
const onToken = jest.fn();
|
|
168
|
-
|
|
169
|
-
const result = streamMessage(messages, onToken);
|
|
170
|
-
|
|
171
|
-
expect(result).toHaveProperty('promise');
|
|
172
|
-
expect(result).toHaveProperty('stop');
|
|
173
|
-
expect(typeof result.stop).toBe('function');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('calls native startStreaming with correct arguments', () => {
|
|
177
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
178
|
-
const onToken = jest.fn();
|
|
179
|
-
|
|
180
|
-
streamMessage(messages, onToken);
|
|
181
|
-
|
|
182
|
-
expect(NativeModule.startStreaming).toHaveBeenCalledWith(
|
|
183
|
-
messages,
|
|
184
|
-
'You are a helpful, friendly assistant. Answer the user directly and concisely.',
|
|
185
|
-
expect.stringMatching(/^stream_\d+_\d+$/)
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('uses custom system prompt from options', () => {
|
|
190
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
191
|
-
const onToken = jest.fn();
|
|
192
|
-
|
|
193
|
-
streamMessage(messages, onToken, { systemPrompt: 'Be a pirate.' });
|
|
194
|
-
|
|
195
|
-
expect(NativeModule.startStreaming).toHaveBeenCalledWith(
|
|
196
|
-
messages,
|
|
197
|
-
'Be a pirate.',
|
|
198
|
-
expect.any(String)
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('passes empty string when messages contain system message', () => {
|
|
203
|
-
const messages = [
|
|
204
|
-
{ role: 'system', content: 'Be brief.' },
|
|
205
|
-
{ role: 'user', content: 'Hello' },
|
|
206
|
-
];
|
|
207
|
-
const onToken = jest.fn();
|
|
208
|
-
|
|
209
|
-
streamMessage(messages, onToken, { systemPrompt: 'This should be ignored' });
|
|
210
|
-
|
|
211
|
-
expect(NativeModule.startStreaming).toHaveBeenCalledWith(
|
|
212
|
-
messages,
|
|
213
|
-
'',
|
|
214
|
-
expect.any(String)
|
|
215
|
-
);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('calls onToken callback for each stream event', async () => {
|
|
219
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
220
|
-
const onToken = jest.fn();
|
|
221
|
-
|
|
222
|
-
const { promise } = streamMessage(messages, onToken);
|
|
223
|
-
|
|
224
|
-
// Get the sessionId that was used
|
|
225
|
-
const sessionId = NativeModule.startStreaming.mock.calls[0][2];
|
|
226
|
-
|
|
227
|
-
// Simulate streaming events
|
|
228
|
-
simulateStreamEvent({
|
|
229
|
-
sessionId,
|
|
230
|
-
token: 'Hello',
|
|
231
|
-
accumulatedText: 'Hello',
|
|
232
|
-
isDone: false,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
simulateStreamEvent({
|
|
236
|
-
sessionId,
|
|
237
|
-
token: ' world',
|
|
238
|
-
accumulatedText: 'Hello world',
|
|
239
|
-
isDone: false,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
simulateStreamEvent({
|
|
243
|
-
sessionId,
|
|
244
|
-
token: '',
|
|
245
|
-
accumulatedText: 'Hello world',
|
|
246
|
-
isDone: true,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const result = await promise;
|
|
250
|
-
|
|
251
|
-
expect(onToken).toHaveBeenCalledTimes(3);
|
|
252
|
-
expect(onToken).toHaveBeenNthCalledWith(1, {
|
|
253
|
-
sessionId,
|
|
254
|
-
token: 'Hello',
|
|
255
|
-
accumulatedText: 'Hello',
|
|
256
|
-
isDone: false,
|
|
257
|
-
});
|
|
258
|
-
expect(result).toEqual({ text: 'Hello world' });
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('ignores events from other sessions', () => {
|
|
262
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
263
|
-
const onToken = jest.fn();
|
|
264
|
-
|
|
265
|
-
streamMessage(messages, onToken);
|
|
266
|
-
|
|
267
|
-
// Simulate event from different session
|
|
268
|
-
simulateStreamEvent({
|
|
269
|
-
sessionId: 'other_session',
|
|
270
|
-
token: 'Other',
|
|
271
|
-
accumulatedText: 'Other',
|
|
272
|
-
isDone: false,
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
expect(onToken).not.toHaveBeenCalled();
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('calls stopStreaming when stop() is called', () => {
|
|
279
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
280
|
-
const onToken = jest.fn();
|
|
281
|
-
|
|
282
|
-
const { stop } = streamMessage(messages, onToken);
|
|
283
|
-
const sessionId = NativeModule.startStreaming.mock.calls[0][2];
|
|
284
|
-
|
|
285
|
-
stop();
|
|
286
|
-
|
|
287
|
-
expect(NativeModule.stopStreaming).toHaveBeenCalledWith(sessionId);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('returns empty response on unsupported platforms', () => {
|
|
291
|
-
mockPlatformOS = 'web';
|
|
292
|
-
|
|
293
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
294
|
-
const onToken = jest.fn();
|
|
295
|
-
|
|
296
|
-
const { promise, stop } = streamMessage(messages, onToken);
|
|
297
|
-
|
|
298
|
-
expect(NativeModule.startStreaming).not.toHaveBeenCalled();
|
|
299
|
-
expect(promise).resolves.toEqual({ text: '' });
|
|
300
|
-
expect(stop).toBeDefined();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('rejects promise for empty messages array', () => {
|
|
304
|
-
const onToken = jest.fn();
|
|
305
|
-
|
|
306
|
-
const { promise } = streamMessage([], onToken);
|
|
307
|
-
|
|
308
|
-
expect(promise).rejects.toThrow('messages array cannot be empty');
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// ============================================================================
|
|
313
|
-
// Prompt Helper Tests
|
|
314
|
-
// ============================================================================
|
|
315
|
-
|
|
316
|
-
describe('summarize', () => {
|
|
317
|
-
it('throws error for empty text', async () => {
|
|
318
|
-
await expect(summarize('')).rejects.toThrow('text cannot be empty');
|
|
319
|
-
await expect(summarize(' ')).rejects.toThrow('text cannot be empty');
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('calls sendMessage with summarization system prompt', async () => {
|
|
323
|
-
await summarize('Some long text to summarize');
|
|
324
|
-
|
|
325
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
326
|
-
[{ role: 'user', content: 'Some long text to summarize' }],
|
|
327
|
-
expect.stringContaining('summarization assistant')
|
|
328
|
-
);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('uses default options (medium length, paragraph style)', async () => {
|
|
332
|
-
await summarize('Text');
|
|
333
|
-
|
|
334
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
335
|
-
expect(systemPrompt).toContain('3-5 sentences');
|
|
336
|
-
expect(systemPrompt).toContain('flowing paragraph');
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('respects length option', async () => {
|
|
340
|
-
await summarize('Text', { length: 'short' });
|
|
341
|
-
|
|
342
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
343
|
-
expect(systemPrompt).toContain('1-2 sentences');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('respects style option', async () => {
|
|
347
|
-
await summarize('Text', { style: 'bullets' });
|
|
348
|
-
|
|
349
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
350
|
-
expect(systemPrompt).toContain('bullet points');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('returns empty response on unsupported platforms', async () => {
|
|
354
|
-
mockPlatformOS = 'web';
|
|
355
|
-
|
|
356
|
-
const result = await summarize('Text');
|
|
357
|
-
|
|
358
|
-
expect(result).toEqual({ text: '' });
|
|
359
|
-
expect(NativeModule.sendMessage).not.toHaveBeenCalled();
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
describe('streamSummarize', () => {
|
|
364
|
-
it('returns promise and stop function', () => {
|
|
365
|
-
const onToken = jest.fn();
|
|
366
|
-
const result = streamSummarize('Text', onToken);
|
|
367
|
-
|
|
368
|
-
expect(result).toHaveProperty('promise');
|
|
369
|
-
expect(result).toHaveProperty('stop');
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('rejects for empty text', () => {
|
|
373
|
-
const onToken = jest.fn();
|
|
374
|
-
const { promise } = streamSummarize('', onToken);
|
|
375
|
-
|
|
376
|
-
expect(promise).rejects.toThrow('text cannot be empty');
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('calls native startStreaming with summarization prompt', () => {
|
|
380
|
-
const onToken = jest.fn();
|
|
381
|
-
streamSummarize('Text', onToken, { style: 'tldr' });
|
|
382
|
-
|
|
383
|
-
const systemPrompt = NativeModule.startStreaming.mock.calls[0][1];
|
|
384
|
-
expect(systemPrompt).toContain('TL;DR');
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
describe('translate', () => {
|
|
389
|
-
it('throws error for empty text', async () => {
|
|
390
|
-
await expect(translate('', { to: 'Spanish' })).rejects.toThrow(
|
|
391
|
-
'text cannot be empty'
|
|
392
|
-
);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('calls sendMessage with translation system prompt', async () => {
|
|
396
|
-
await translate('Hello', { to: 'Spanish' });
|
|
397
|
-
|
|
398
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
399
|
-
[{ role: 'user', content: 'Hello' }],
|
|
400
|
-
expect.stringContaining('translation assistant')
|
|
401
|
-
);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('includes target language in prompt', async () => {
|
|
405
|
-
await translate('Hello', { to: 'Japanese' });
|
|
406
|
-
|
|
407
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
408
|
-
expect(systemPrompt).toContain('to Japanese');
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it('includes source language when provided', async () => {
|
|
412
|
-
await translate('Hello', { to: 'Spanish', from: 'English' });
|
|
413
|
-
|
|
414
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
415
|
-
expect(systemPrompt).toContain('from English');
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('respects tone option', async () => {
|
|
419
|
-
await translate('Hello', { to: 'Spanish', tone: 'formal' });
|
|
420
|
-
|
|
421
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
422
|
-
expect(systemPrompt).toContain('formal language');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('returns empty response on unsupported platforms', async () => {
|
|
426
|
-
mockPlatformOS = 'web';
|
|
427
|
-
|
|
428
|
-
const result = await translate('Hello', { to: 'Spanish' });
|
|
429
|
-
|
|
430
|
-
expect(result).toEqual({ text: '' });
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
describe('streamTranslate', () => {
|
|
435
|
-
it('returns promise and stop function', () => {
|
|
436
|
-
const onToken = jest.fn();
|
|
437
|
-
const result = streamTranslate('Hello', onToken, { to: 'Spanish' });
|
|
438
|
-
|
|
439
|
-
expect(result).toHaveProperty('promise');
|
|
440
|
-
expect(result).toHaveProperty('stop');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('rejects for empty text', () => {
|
|
444
|
-
const onToken = jest.fn();
|
|
445
|
-
const { promise } = streamTranslate('', onToken, { to: 'Spanish' });
|
|
446
|
-
|
|
447
|
-
expect(promise).rejects.toThrow('text cannot be empty');
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
describe('rewrite', () => {
|
|
452
|
-
it('throws error for empty text', async () => {
|
|
453
|
-
await expect(rewrite('', { style: 'formal' })).rejects.toThrow(
|
|
454
|
-
'text cannot be empty'
|
|
455
|
-
);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('calls sendMessage with rewrite system prompt', async () => {
|
|
459
|
-
await rewrite('hey whats up', { style: 'formal' });
|
|
460
|
-
|
|
461
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
462
|
-
[{ role: 'user', content: 'hey whats up' }],
|
|
463
|
-
expect.stringContaining('writing assistant')
|
|
464
|
-
);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('includes style instruction in prompt', async () => {
|
|
468
|
-
await rewrite('Text', { style: 'academic' });
|
|
469
|
-
|
|
470
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
471
|
-
expect(systemPrompt).toContain('academic');
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('returns empty response on unsupported platforms', async () => {
|
|
475
|
-
mockPlatformOS = 'web';
|
|
476
|
-
|
|
477
|
-
const result = await rewrite('Text', { style: 'formal' });
|
|
478
|
-
|
|
479
|
-
expect(result).toEqual({ text: '' });
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
describe('streamRewrite', () => {
|
|
484
|
-
it('returns promise and stop function', () => {
|
|
485
|
-
const onToken = jest.fn();
|
|
486
|
-
const result = streamRewrite('Text', onToken, { style: 'formal' });
|
|
487
|
-
|
|
488
|
-
expect(result).toHaveProperty('promise');
|
|
489
|
-
expect(result).toHaveProperty('stop');
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('rejects for empty text', () => {
|
|
493
|
-
const onToken = jest.fn();
|
|
494
|
-
const { promise } = streamRewrite('', onToken, { style: 'formal' });
|
|
495
|
-
|
|
496
|
-
expect(promise).rejects.toThrow('text cannot be empty');
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe('extractKeyPoints', () => {
|
|
501
|
-
it('throws error for empty text', async () => {
|
|
502
|
-
await expect(extractKeyPoints('')).rejects.toThrow('text cannot be empty');
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it('calls sendMessage with extraction system prompt', async () => {
|
|
506
|
-
await extractKeyPoints('Some article text');
|
|
507
|
-
|
|
508
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
509
|
-
[{ role: 'user', content: 'Some article text' }],
|
|
510
|
-
expect.stringContaining('analysis assistant')
|
|
511
|
-
);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('uses default maxPoints of 5', async () => {
|
|
515
|
-
await extractKeyPoints('Text');
|
|
516
|
-
|
|
517
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
518
|
-
expect(systemPrompt).toContain('5 most important');
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it('respects maxPoints option', async () => {
|
|
522
|
-
await extractKeyPoints('Text', { maxPoints: 3 });
|
|
523
|
-
|
|
524
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
525
|
-
expect(systemPrompt).toContain('3 most important');
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
it('returns empty response on unsupported platforms', async () => {
|
|
529
|
-
mockPlatformOS = 'web';
|
|
530
|
-
|
|
531
|
-
const result = await extractKeyPoints('Text');
|
|
532
|
-
|
|
533
|
-
expect(result).toEqual({ text: '' });
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
describe('streamExtractKeyPoints', () => {
|
|
538
|
-
it('returns promise and stop function', () => {
|
|
539
|
-
const onToken = jest.fn();
|
|
540
|
-
const result = streamExtractKeyPoints('Text', onToken);
|
|
541
|
-
|
|
542
|
-
expect(result).toHaveProperty('promise');
|
|
543
|
-
expect(result).toHaveProperty('stop');
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it('rejects for empty text', () => {
|
|
547
|
-
const onToken = jest.fn();
|
|
548
|
-
const { promise } = streamExtractKeyPoints('', onToken);
|
|
549
|
-
|
|
550
|
-
expect(promise).rejects.toThrow('text cannot be empty');
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
describe('answerQuestion', () => {
|
|
555
|
-
it('throws error for empty question', async () => {
|
|
556
|
-
await expect(answerQuestion('', 'Some context')).rejects.toThrow(
|
|
557
|
-
'question cannot be empty'
|
|
558
|
-
);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
it('throws error for empty context', async () => {
|
|
562
|
-
await expect(answerQuestion('What is it?', '')).rejects.toThrow(
|
|
563
|
-
'context cannot be empty'
|
|
564
|
-
);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('calls sendMessage with QA system prompt', async () => {
|
|
568
|
-
await answerQuestion('What is the topic?', 'The topic is AI.');
|
|
569
|
-
|
|
570
|
-
expect(NativeModule.sendMessage).toHaveBeenCalledWith(
|
|
571
|
-
[{ role: 'user', content: expect.stringContaining('Context:') }],
|
|
572
|
-
expect.stringContaining('question-answering assistant')
|
|
573
|
-
);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it('includes question and context in user message', async () => {
|
|
577
|
-
await answerQuestion('What is X?', 'X is a variable.');
|
|
578
|
-
|
|
579
|
-
const userMessage = NativeModule.sendMessage.mock.calls[0][0][0].content;
|
|
580
|
-
expect(userMessage).toContain('Context:\nX is a variable.');
|
|
581
|
-
expect(userMessage).toContain('Question: What is X?');
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it('uses default detail level of medium', async () => {
|
|
585
|
-
await answerQuestion('Q?', 'Context');
|
|
586
|
-
|
|
587
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
588
|
-
expect(systemPrompt).toContain('clear answer with some explanation');
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('respects detail option', async () => {
|
|
592
|
-
await answerQuestion('Q?', 'Context', { detail: 'brief' });
|
|
593
|
-
|
|
594
|
-
const systemPrompt = NativeModule.sendMessage.mock.calls[0][1];
|
|
595
|
-
expect(systemPrompt).toContain('brief, direct answer');
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it('returns empty response on unsupported platforms', async () => {
|
|
599
|
-
mockPlatformOS = 'web';
|
|
600
|
-
|
|
601
|
-
const result = await answerQuestion('Q?', 'Context');
|
|
602
|
-
|
|
603
|
-
expect(result).toEqual({ text: '' });
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
describe('streamAnswerQuestion', () => {
|
|
608
|
-
it('returns promise and stop function', () => {
|
|
609
|
-
const onToken = jest.fn();
|
|
610
|
-
const result = streamAnswerQuestion('Q?', 'Context', onToken);
|
|
611
|
-
|
|
612
|
-
expect(result).toHaveProperty('promise');
|
|
613
|
-
expect(result).toHaveProperty('stop');
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it('rejects for empty question', () => {
|
|
617
|
-
const onToken = jest.fn();
|
|
618
|
-
const { promise } = streamAnswerQuestion('', 'Context', onToken);
|
|
619
|
-
|
|
620
|
-
expect(promise).rejects.toThrow('question cannot be empty');
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
it('rejects for empty context', () => {
|
|
624
|
-
const onToken = jest.fn();
|
|
625
|
-
const { promise } = streamAnswerQuestion('Q?', '', onToken);
|
|
626
|
-
|
|
627
|
-
expect(promise).rejects.toThrow('context cannot be empty');
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
});
|