@liquidmetal-ai/precip 1.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.
- package/.prettierrc +9 -0
- package/CHANGELOG.md +8 -0
- package/eslint.config.mjs +28 -0
- package/package.json +53 -0
- package/src/engine/agent.ts +478 -0
- package/src/engine/llm-provider.test.ts +275 -0
- package/src/engine/llm-provider.ts +330 -0
- package/src/engine/stream-parser.ts +170 -0
- package/src/index.ts +142 -0
- package/src/mounts/mount-manager.test.ts +516 -0
- package/src/mounts/mount-manager.ts +327 -0
- package/src/mounts/mount-registry.ts +196 -0
- package/src/mounts/zod-to-string.test.ts +154 -0
- package/src/mounts/zod-to-string.ts +213 -0
- package/src/presets/agent-tools.ts +57 -0
- package/src/presets/index.ts +5 -0
- package/src/sandbox/README.md +1321 -0
- package/src/sandbox/bridges/README.md +571 -0
- package/src/sandbox/bridges/actor.test.ts +229 -0
- package/src/sandbox/bridges/actor.ts +195 -0
- package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
- package/src/sandbox/bridges/bucket.test.ts +300 -0
- package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
- package/src/sandbox/bridges/console-multiple.test.ts +187 -0
- package/src/sandbox/bridges/console.test.ts +157 -0
- package/src/sandbox/bridges/console.ts +122 -0
- package/src/sandbox/bridges/fetch.ts +93 -0
- package/src/sandbox/bridges/index.ts +78 -0
- package/src/sandbox/bridges/readable-stream.ts +323 -0
- package/src/sandbox/bridges/response.test.ts +154 -0
- package/src/sandbox/bridges/response.ts +123 -0
- package/src/sandbox/bridges/review-fixes.test.ts +331 -0
- package/src/sandbox/bridges/search.test.ts +475 -0
- package/src/sandbox/bridges/search.ts +264 -0
- package/src/sandbox/bridges/shared/body-methods.ts +93 -0
- package/src/sandbox/bridges/shared/cleanup.ts +112 -0
- package/src/sandbox/bridges/shared/convert.ts +76 -0
- package/src/sandbox/bridges/shared/headers.ts +181 -0
- package/src/sandbox/bridges/shared/index.ts +36 -0
- package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
- package/src/sandbox/bridges/shared/path-parser.ts +109 -0
- package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
- package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
- package/src/sandbox/bridges/shared/response-object.ts +280 -0
- package/src/sandbox/bridges/shared/result-builder.ts +130 -0
- package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
- package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
- package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
- package/src/sandbox/bridges/storage.ts +421 -0
- package/src/sandbox/bridges/text-decoder.ts +190 -0
- package/src/sandbox/bridges/text-encoder.ts +102 -0
- package/src/sandbox/bridges/types.ts +39 -0
- package/src/sandbox/bridges/utils.ts +123 -0
- package/src/sandbox/index.ts +6 -0
- package/src/sandbox/quickjs-wasm.d.ts +9 -0
- package/src/sandbox/sandbox.test.ts +191 -0
- package/src/sandbox/sandbox.ts +831 -0
- package/src/sandbox/test-helper.ts +43 -0
- package/src/sandbox/test-mocks.ts +154 -0
- package/src/sandbox/user-stream.test.ts +77 -0
- package/src/skills/frontmatter.test.ts +305 -0
- package/src/skills/frontmatter.ts +200 -0
- package/src/skills/index.ts +9 -0
- package/src/skills/skills-loader.test.ts +237 -0
- package/src/skills/skills-loader.ts +200 -0
- package/src/tools/actor-storage-tools.ts +250 -0
- package/src/tools/code-tools.test.ts +199 -0
- package/src/tools/code-tools.ts +444 -0
- package/src/tools/file-tools.ts +206 -0
- package/src/tools/registry.ts +125 -0
- package/src/tools/script-tools.ts +145 -0
- package/src/tools/smartbucket-tools.ts +203 -0
- package/src/tools/sql-tools.ts +213 -0
- package/src/tools/tool-factory.ts +119 -0
- package/src/types.ts +512 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +33 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { executeWithAsyncHost } from '../test-helper.js';
|
|
3
|
+
import { installSearch } from './search.js';
|
|
4
|
+
import type { SearchMountInfo } from './search.js';
|
|
5
|
+
|
|
6
|
+
function createMockSmartBucket() {
|
|
7
|
+
const searches = new Map<string, any>();
|
|
8
|
+
|
|
9
|
+
const mockSmartBucket: any = {
|
|
10
|
+
search: vi.fn(async ({ input, requestId }: { input: string; requestId: string }) => {
|
|
11
|
+
// Store the search with requestId for pagination
|
|
12
|
+
const page = 1;
|
|
13
|
+
const pageSize = 5;
|
|
14
|
+
|
|
15
|
+
// Simulate 12 results total (3 pages of 5)
|
|
16
|
+
const allResults = Array.from({ length: 12 }, (_, i) => ({
|
|
17
|
+
text: `Result ${i + 1} for ${input}`,
|
|
18
|
+
source: `source-${i + 1}.txt`,
|
|
19
|
+
score: 0.9 - (i * 0.05),
|
|
20
|
+
chunkSignature: `chunk-${i + 1}`
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const results = allResults.slice(0, pageSize);
|
|
24
|
+
searches.set(requestId, allResults);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
results,
|
|
28
|
+
pagination: {
|
|
29
|
+
total: allResults.length,
|
|
30
|
+
page,
|
|
31
|
+
pageSize,
|
|
32
|
+
hasMore: true
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
chunkSearch: vi.fn(async ({ input, requestId: _requestId }: { input: string; requestId: string }) => {
|
|
38
|
+
// RAG-style search returns all results at once
|
|
39
|
+
return {
|
|
40
|
+
results: [
|
|
41
|
+
{
|
|
42
|
+
text: `Chunk result 1 for ${input}`,
|
|
43
|
+
source: 'doc1.txt',
|
|
44
|
+
score: 0.95,
|
|
45
|
+
chunkSignature: 'chunk-1'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
text: `Chunk result 2 for ${input}`,
|
|
49
|
+
source: 'doc1.txt',
|
|
50
|
+
score: 0.88,
|
|
51
|
+
chunkSignature: 'chunk-2'
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
getPaginatedResults: vi.fn(async ({ requestId, page }: { requestId: string; page: number }) => {
|
|
58
|
+
const allResults = searches.get(requestId);
|
|
59
|
+
if (!allResults) {
|
|
60
|
+
throw new Error('Search not found');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pageSize = 5;
|
|
64
|
+
const startIdx = (page - 1) * pageSize;
|
|
65
|
+
const results = allResults.slice(startIdx, startIdx + pageSize);
|
|
66
|
+
const hasMore = startIdx + pageSize < allResults.length;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
results,
|
|
70
|
+
pagination: {
|
|
71
|
+
total: allResults.length,
|
|
72
|
+
page,
|
|
73
|
+
pageSize,
|
|
74
|
+
hasMore
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
})
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return mockSmartBucket;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('Search Bridge', () => {
|
|
84
|
+
describe('Basic Search', () => {
|
|
85
|
+
it('should perform search and return results', async () => {
|
|
86
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
87
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
88
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const result = await executeWithAsyncHost(
|
|
92
|
+
`
|
|
93
|
+
const results = await search("/search/", "test query");
|
|
94
|
+
return {
|
|
95
|
+
count: results.results.length,
|
|
96
|
+
total: results.total,
|
|
97
|
+
hasMore: results.hasMore,
|
|
98
|
+
page: results.page,
|
|
99
|
+
pageSize: results.pageSize
|
|
100
|
+
};
|
|
101
|
+
`,
|
|
102
|
+
{},
|
|
103
|
+
{
|
|
104
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result.success).toBe(true);
|
|
109
|
+
expect(result.result.count).toBe(5);
|
|
110
|
+
expect(result.result.total).toBe(12);
|
|
111
|
+
expect(result.result.hasMore).toBe(true);
|
|
112
|
+
expect(result.result.page).toBe(1);
|
|
113
|
+
expect(result.result.pageSize).toBe(5);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return results with correct structure', async () => {
|
|
117
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
118
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
119
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const result = await executeWithAsyncHost(
|
|
123
|
+
`
|
|
124
|
+
const results = await search("/search/", "test query");
|
|
125
|
+
const firstResult = results.results[0];
|
|
126
|
+
return {
|
|
127
|
+
hasText: typeof firstResult.text === 'string',
|
|
128
|
+
hasSource: typeof firstResult.source === 'string',
|
|
129
|
+
hasScore: typeof firstResult.score === 'number',
|
|
130
|
+
hasChunkSignature: typeof firstResult.chunkSignature === 'string'
|
|
131
|
+
};
|
|
132
|
+
`,
|
|
133
|
+
{},
|
|
134
|
+
{
|
|
135
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(result.success).toBe(true);
|
|
140
|
+
expect(result.result.hasText).toBe(true);
|
|
141
|
+
expect(result.result.hasSource).toBe(true);
|
|
142
|
+
expect(result.result.hasScore).toBe(true);
|
|
143
|
+
expect(result.result.hasChunkSignature).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle invalid mount path', async () => {
|
|
147
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
148
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
149
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const result = await executeWithAsyncHost(
|
|
153
|
+
`
|
|
154
|
+
try {
|
|
155
|
+
await search("/invalid/", "test");
|
|
156
|
+
return { error: false };
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return { error: true, message: String(e) };
|
|
159
|
+
}
|
|
160
|
+
`,
|
|
161
|
+
{},
|
|
162
|
+
{
|
|
163
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(result.success).toBe(true);
|
|
168
|
+
expect(result.result.error).toBe(true);
|
|
169
|
+
expect(result.result.message).toContain('not found');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Pagination', () => {
|
|
174
|
+
it('should navigate to next page', async () => {
|
|
175
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
176
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
177
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const result = await executeWithAsyncHost(
|
|
181
|
+
`
|
|
182
|
+
const page1 = await search("/search/", "test query");
|
|
183
|
+
const page2 = await page1.nextPage();
|
|
184
|
+
return {
|
|
185
|
+
page1Count: page1.results.length,
|
|
186
|
+
page2Count: page2.results.length,
|
|
187
|
+
page2Page: page2.page,
|
|
188
|
+
page2HasMore: page2.hasMore
|
|
189
|
+
};
|
|
190
|
+
`,
|
|
191
|
+
{},
|
|
192
|
+
{
|
|
193
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(result.success).toBe(true);
|
|
198
|
+
expect(result.result.page1Count).toBe(5);
|
|
199
|
+
expect(result.result.page2Count).toBe(5);
|
|
200
|
+
expect(result.result.page2Page).toBe(2);
|
|
201
|
+
expect(result.result.page2HasMore).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return empty results when no more pages', async () => {
|
|
205
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
206
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
207
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const result = await executeWithAsyncHost(
|
|
211
|
+
`
|
|
212
|
+
const page1 = await search("/search/", "test query");
|
|
213
|
+
const page2 = await page1.nextPage();
|
|
214
|
+
const page3 = await page2.nextPage();
|
|
215
|
+
const page4 = await page3.nextPage();
|
|
216
|
+
return {
|
|
217
|
+
page3Count: page3.results.length,
|
|
218
|
+
page4Count: page4.results.length,
|
|
219
|
+
page4HasMore: page4.hasMore
|
|
220
|
+
};
|
|
221
|
+
`,
|
|
222
|
+
{},
|
|
223
|
+
{
|
|
224
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(result.success).toBe(true);
|
|
229
|
+
expect(result.result.page3Count).toBe(2); // Last page has 2 items
|
|
230
|
+
expect(result.result.page4Count).toBe(0);
|
|
231
|
+
expect(result.result.page4HasMore).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should paginate through all pages and iterate total results', async () => {
|
|
235
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
236
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
237
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const result = await executeWithAsyncHost(
|
|
241
|
+
`
|
|
242
|
+
let current = await search("/search/", "test query");
|
|
243
|
+
let total = 0;
|
|
244
|
+
while (current) {
|
|
245
|
+
total += current.results.length;
|
|
246
|
+
if (current.hasMore) {
|
|
247
|
+
current = await current.nextPage();
|
|
248
|
+
} else {
|
|
249
|
+
current = null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return total;
|
|
253
|
+
`,
|
|
254
|
+
{},
|
|
255
|
+
{
|
|
256
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(result.success).toBe(true);
|
|
261
|
+
expect(result.result).toBe(12);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('Async Iterator', () => {
|
|
266
|
+
it('should support for-await iteration', async () => {
|
|
267
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
268
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
269
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
const result = await executeWithAsyncHost(
|
|
273
|
+
`
|
|
274
|
+
const results = await search("/search/", "test query");
|
|
275
|
+
let count = 0;
|
|
276
|
+
for await (const item of results) {
|
|
277
|
+
count++;
|
|
278
|
+
}
|
|
279
|
+
return count;
|
|
280
|
+
`,
|
|
281
|
+
{},
|
|
282
|
+
{
|
|
283
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(result.success).toBe(true);
|
|
288
|
+
expect(result.result).toBe(12);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should iterate through all pages automatically', async () => {
|
|
292
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
293
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
294
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
const result = await executeWithAsyncHost(
|
|
298
|
+
`
|
|
299
|
+
const results = await search("/search/", "test query");
|
|
300
|
+
const texts = [];
|
|
301
|
+
for await (const item of results) {
|
|
302
|
+
texts.push(item.text);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
count: texts.length,
|
|
306
|
+
firstText: texts[0],
|
|
307
|
+
lastText: texts[texts.length - 1]
|
|
308
|
+
};
|
|
309
|
+
`,
|
|
310
|
+
{},
|
|
311
|
+
{
|
|
312
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(result.success).toBe(true);
|
|
317
|
+
expect(result.result.count).toBe(12);
|
|
318
|
+
expect(result.result.firstText).toContain('Result 1');
|
|
319
|
+
expect(result.result.lastText).toContain('Result 12');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('chunkSearch', () => {
|
|
324
|
+
it('should perform chunk search without pagination', async () => {
|
|
325
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
326
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
327
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
const result = await executeWithAsyncHost(
|
|
331
|
+
`
|
|
332
|
+
const results = await chunkSearch("/search/", "rag query");
|
|
333
|
+
return {
|
|
334
|
+
count: results.results.length,
|
|
335
|
+
hasText: typeof results.results[0].text === 'string'
|
|
336
|
+
};
|
|
337
|
+
`,
|
|
338
|
+
{},
|
|
339
|
+
{
|
|
340
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(result.success).toBe(true);
|
|
345
|
+
expect(result.result.count).toBe(2);
|
|
346
|
+
expect(result.result.hasText).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should return chunk results with all expected fields', async () => {
|
|
350
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
351
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
352
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
const result = await executeWithAsyncHost(
|
|
356
|
+
`
|
|
357
|
+
const results = await chunkSearch("/search/", "rag query");
|
|
358
|
+
const first = results.results[0];
|
|
359
|
+
return {
|
|
360
|
+
text: first.text,
|
|
361
|
+
source: first.source,
|
|
362
|
+
score: first.score,
|
|
363
|
+
chunkSignature: first.chunkSignature
|
|
364
|
+
};
|
|
365
|
+
`,
|
|
366
|
+
{},
|
|
367
|
+
{
|
|
368
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(result.success).toBe(true);
|
|
373
|
+
expect(result.result.text).toContain('Chunk result 1');
|
|
374
|
+
expect(result.result.source).toBe('doc1.txt');
|
|
375
|
+
expect(typeof result.result.score).toBe('number');
|
|
376
|
+
expect(result.result.chunkSignature).toBe('chunk-1');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('Edge Cases', () => {
|
|
381
|
+
it('should handle empty search results', async () => {
|
|
382
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
383
|
+
// Override search to return empty results
|
|
384
|
+
mockSmartBucket.search = vi.fn(async () => ({
|
|
385
|
+
results: [],
|
|
386
|
+
pagination: {
|
|
387
|
+
total: 0,
|
|
388
|
+
page: 1,
|
|
389
|
+
pageSize: 5,
|
|
390
|
+
hasMore: false
|
|
391
|
+
}
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
395
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
const result = await executeWithAsyncHost(
|
|
399
|
+
`
|
|
400
|
+
const results = await search("/search/", "no results query");
|
|
401
|
+
return {
|
|
402
|
+
count: results.results.length,
|
|
403
|
+
hasMore: results.hasMore,
|
|
404
|
+
total: results.total
|
|
405
|
+
};
|
|
406
|
+
`,
|
|
407
|
+
{},
|
|
408
|
+
{
|
|
409
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(result.success).toBe(true);
|
|
414
|
+
expect(result.result.count).toBe(0);
|
|
415
|
+
expect(result.result.hasMore).toBe(false);
|
|
416
|
+
expect(result.result.total).toBe(0);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should handle single page of results', async () => {
|
|
420
|
+
const mockSmartBucket = createMockSmartBucket();
|
|
421
|
+
// Override search to return exactly one page
|
|
422
|
+
mockSmartBucket.search = vi.fn(async () => ({
|
|
423
|
+
results: [
|
|
424
|
+
{
|
|
425
|
+
text: 'Single result',
|
|
426
|
+
source: 'doc.txt',
|
|
427
|
+
score: 0.9,
|
|
428
|
+
chunkSignature: 'chunk-1'
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
pagination: {
|
|
432
|
+
total: 1,
|
|
433
|
+
page: 1,
|
|
434
|
+
pageSize: 5,
|
|
435
|
+
hasMore: false
|
|
436
|
+
}
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
mockSmartBucket.getPaginatedResults = vi.fn(async () => ({
|
|
440
|
+
results: [],
|
|
441
|
+
pagination: {
|
|
442
|
+
total: 1,
|
|
443
|
+
page: 2,
|
|
444
|
+
pageSize: 5,
|
|
445
|
+
hasMore: false
|
|
446
|
+
}
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
const searchMounts = new Map<string, SearchMountInfo>([
|
|
450
|
+
['search', { name: 'search', smartbucket: mockSmartBucket as any }]
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
const result = await executeWithAsyncHost(
|
|
454
|
+
`
|
|
455
|
+
const results = await search("/search/", "single result query");
|
|
456
|
+
const page2 = await results.nextPage();
|
|
457
|
+
return {
|
|
458
|
+
page1Count: results.results.length,
|
|
459
|
+
page1HasMore: results.hasMore,
|
|
460
|
+
page2Count: page2.results.length
|
|
461
|
+
};
|
|
462
|
+
`,
|
|
463
|
+
{},
|
|
464
|
+
{
|
|
465
|
+
bridgeInstallers: [ctx => installSearch(ctx, searchMounts)]
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
expect(result.success).toBe(true);
|
|
470
|
+
expect(result.result.page1Count).toBe(1);
|
|
471
|
+
expect(result.result.page1HasMore).toBe(false);
|
|
472
|
+
expect(result.result.page2Count).toBe(0);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|