@kernl-sdk/turbopuffer 0.1.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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +60 -0
  3. package/CHANGELOG.md +33 -0
  4. package/LICENSE +201 -0
  5. package/README.md +60 -0
  6. package/dist/__tests__/convert.test.d.ts +2 -0
  7. package/dist/__tests__/convert.test.d.ts.map +1 -0
  8. package/dist/__tests__/convert.test.js +346 -0
  9. package/dist/__tests__/filter.test.d.ts +8 -0
  10. package/dist/__tests__/filter.test.d.ts.map +1 -0
  11. package/dist/__tests__/filter.test.js +649 -0
  12. package/dist/__tests__/filters.integration.test.d.ts +8 -0
  13. package/dist/__tests__/filters.integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/filters.integration.test.js +502 -0
  15. package/dist/__tests__/integration/filters.integration.test.d.ts +8 -0
  16. package/dist/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/integration/filters.integration.test.js +475 -0
  18. package/dist/__tests__/integration/integration.test.d.ts +2 -0
  19. package/dist/__tests__/integration/integration.test.d.ts.map +1 -0
  20. package/dist/__tests__/integration/integration.test.js +329 -0
  21. package/dist/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  22. package/dist/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/integration/lifecycle.integration.test.js +370 -0
  24. package/dist/__tests__/integration/memory.integration.test.d.ts +2 -0
  25. package/dist/__tests__/integration/memory.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/integration/memory.integration.test.js +287 -0
  27. package/dist/__tests__/integration/query.integration.test.d.ts +8 -0
  28. package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
  29. package/dist/__tests__/integration/query.integration.test.js +385 -0
  30. package/dist/__tests__/integration.test.d.ts +2 -0
  31. package/dist/__tests__/integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/integration.test.js +343 -0
  33. package/dist/__tests__/lifecycle.integration.test.d.ts +8 -0
  34. package/dist/__tests__/lifecycle.integration.test.d.ts.map +1 -0
  35. package/dist/__tests__/lifecycle.integration.test.js +385 -0
  36. package/dist/__tests__/query.integration.test.d.ts +8 -0
  37. package/dist/__tests__/query.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/query.integration.test.js +423 -0
  39. package/dist/__tests__/query.test.d.ts +8 -0
  40. package/dist/__tests__/query.test.d.ts.map +1 -0
  41. package/dist/__tests__/query.test.js +472 -0
  42. package/dist/convert/document.d.ts +20 -0
  43. package/dist/convert/document.d.ts.map +1 -0
  44. package/dist/convert/document.js +72 -0
  45. package/dist/convert/filter.d.ts +15 -0
  46. package/dist/convert/filter.d.ts.map +1 -0
  47. package/dist/convert/filter.js +109 -0
  48. package/dist/convert/index.d.ts +8 -0
  49. package/dist/convert/index.d.ts.map +1 -0
  50. package/dist/convert/index.js +7 -0
  51. package/dist/convert/query.d.ts +22 -0
  52. package/dist/convert/query.d.ts.map +1 -0
  53. package/dist/convert/query.js +111 -0
  54. package/dist/convert/schema.d.ts +39 -0
  55. package/dist/convert/schema.d.ts.map +1 -0
  56. package/dist/convert/schema.js +124 -0
  57. package/dist/convert.d.ts +68 -0
  58. package/dist/convert.d.ts.map +1 -0
  59. package/dist/convert.js +333 -0
  60. package/dist/handle.d.ts +34 -0
  61. package/dist/handle.d.ts.map +1 -0
  62. package/dist/handle.js +72 -0
  63. package/dist/index.d.ts +27 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +26 -0
  66. package/dist/search.d.ts +85 -0
  67. package/dist/search.d.ts.map +1 -0
  68. package/dist/search.js +167 -0
  69. package/dist/types.d.ts +14 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +1 -0
  72. package/package.json +57 -0
  73. package/src/__tests__/convert.test.ts +425 -0
  74. package/src/__tests__/filter.test.ts +730 -0
  75. package/src/__tests__/integration/filters.integration.test.ts +558 -0
  76. package/src/__tests__/integration/integration.test.ts +399 -0
  77. package/src/__tests__/integration/lifecycle.integration.test.ts +464 -0
  78. package/src/__tests__/integration/memory.integration.test.ts +353 -0
  79. package/src/__tests__/integration/query.integration.test.ts +471 -0
  80. package/src/__tests__/query.test.ts +636 -0
  81. package/src/convert/document.ts +95 -0
  82. package/src/convert/filter.ts +123 -0
  83. package/src/convert/index.ts +8 -0
  84. package/src/convert/query.ts +151 -0
  85. package/src/convert/schema.ts +163 -0
  86. package/src/handle.ts +104 -0
  87. package/src/index.ts +31 -0
  88. package/src/search.ts +207 -0
  89. package/src/types.ts +14 -0
  90. package/tsconfig.json +13 -0
  91. package/vitest.config.ts +15 -0
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Index and handle lifecycle edge case integration tests.
3
+ *
4
+ * Tests edge cases for index lifecycle operations and document handling
5
+ * against real Turbopuffer API.
6
+ */
7
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
8
+ import { TurbopufferSearchIndex } from "../../search.js";
9
+ const TURBOPUFFER_API_KEY = process.env.TURBOPUFFER_API_KEY;
10
+ const TURBOPUFFER_REGION = process.env.TURBOPUFFER_REGION ?? "api";
11
+ /**
12
+ * Helper to create a vector array.
13
+ */
14
+ function vec(dim, fill = 0.1) {
15
+ return new Array(dim).fill(fill);
16
+ }
17
+ describe("Lifecycle edge cases integration tests", () => {
18
+ if (!TURBOPUFFER_API_KEY) {
19
+ it.skip("requires TURBOPUFFER_API_KEY to be set", () => { });
20
+ return;
21
+ }
22
+ let tpuf;
23
+ const testPrefix = `kernl-lifecycle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
24
+ beforeAll(() => {
25
+ tpuf = new TurbopufferSearchIndex({
26
+ apiKey: TURBOPUFFER_API_KEY,
27
+ region: TURBOPUFFER_REGION,
28
+ });
29
+ });
30
+ afterAll(async () => {
31
+ // Clean up any test indexes that might have been created
32
+ try {
33
+ const page = await tpuf.listIndexes({ prefix: testPrefix });
34
+ for (const idx of page.items) {
35
+ try {
36
+ await tpuf.deleteIndex(idx.id);
37
+ }
38
+ catch {
39
+ // Ignore individual cleanup errors
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ // Ignore cleanup errors
45
+ }
46
+ });
47
+ // ============================================================
48
+ // INDEX LIFECYCLE EDGE CASES
49
+ // ============================================================
50
+ describe("index lifecycle edge cases", () => {
51
+ it("describes index immediately after creation", async () => {
52
+ const indexId = `${testPrefix}-describe-after-create`;
53
+ await tpuf.createIndex({
54
+ id: indexId,
55
+ schema: {
56
+ text: { type: "string" },
57
+ vector: { type: "vector", dimensions: 4 },
58
+ },
59
+ });
60
+ const stats = await tpuf.describeIndex(indexId);
61
+ expect(stats.id).toBe(indexId);
62
+ expect(stats.count).toBe(0);
63
+ await tpuf.deleteIndex(indexId);
64
+ });
65
+ it("describe after delete throws", async () => {
66
+ const indexId = `${testPrefix}-describe-after-delete`;
67
+ await tpuf.createIndex({
68
+ id: indexId,
69
+ schema: { text: { type: "string" } },
70
+ });
71
+ await tpuf.deleteIndex(indexId);
72
+ await expect(tpuf.describeIndex(indexId)).rejects.toThrow();
73
+ });
74
+ it("delete non-existent index throws", async () => {
75
+ const indexId = `${testPrefix}-nonexistent-${Date.now()}`;
76
+ await expect(tpuf.deleteIndex(indexId)).rejects.toThrow();
77
+ });
78
+ it("create index with same id twice is idempotent", async () => {
79
+ const indexId = `${testPrefix}-duplicate`;
80
+ await tpuf.createIndex({
81
+ id: indexId,
82
+ schema: { text: { type: "string" } },
83
+ });
84
+ // Second create should succeed (idempotent) - Turbopuffer creates indexes implicitly
85
+ await tpuf.createIndex({
86
+ id: indexId,
87
+ schema: { text: { type: "string" } },
88
+ });
89
+ // Verify index still exists and is accessible
90
+ const stats = await tpuf.describeIndex(indexId);
91
+ expect(stats.id).toBe(indexId);
92
+ await tpuf.deleteIndex(indexId);
93
+ });
94
+ it("listIndexes returns empty page for non-matching prefix", async () => {
95
+ const page = await tpuf.listIndexes({
96
+ prefix: `nonexistent-prefix-${Date.now()}`,
97
+ });
98
+ expect(page.items).toEqual([]);
99
+ });
100
+ });
101
+ // ============================================================
102
+ // DOCUMENT UPSERT EDGE CASES
103
+ // ============================================================
104
+ describe("document upsert edge cases", () => {
105
+ const indexId = `${testPrefix}-upsert-edge`;
106
+ beforeAll(async () => {
107
+ await tpuf.createIndex({
108
+ id: indexId,
109
+ schema: {
110
+ content: { type: "string" },
111
+ count: { type: "int" },
112
+ vector: { type: "vector", dimensions: 4 },
113
+ },
114
+ });
115
+ });
116
+ afterAll(async () => {
117
+ try {
118
+ await tpuf.deleteIndex(indexId);
119
+ }
120
+ catch {
121
+ // Ignore
122
+ }
123
+ });
124
+ it("multiple upserts same id keeps last write", async () => {
125
+ const index = tpuf.index(indexId);
126
+ // First write
127
+ await index.upsert({
128
+ id: "overwrite-test",
129
+ content: "First version",
130
+ count: 1,
131
+ vector: vec(4, 0.1),
132
+ });
133
+ // Second write
134
+ await index.upsert({
135
+ id: "overwrite-test",
136
+ content: "Second version",
137
+ count: 2,
138
+ vector: vec(4, 0.2),
139
+ });
140
+ // Third write
141
+ await index.upsert({
142
+ id: "overwrite-test",
143
+ content: "Final version",
144
+ count: 3,
145
+ vector: vec(4, 0.3),
146
+ });
147
+ // Wait for indexing
148
+ await new Promise((r) => setTimeout(r, 1000));
149
+ // Query and verify final state
150
+ const hits = await index.query({
151
+ query: [{ vector: [0.3, 0.3, 0.3, 0.3] }],
152
+ topK: 10,
153
+ filter: { id: "overwrite-test" },
154
+ include: ["content", "count"],
155
+ });
156
+ expect(hits.length).toBe(1);
157
+ expect(hits[0].document?.content).toBe("Final version");
158
+ expect(hits[0].document?.count).toBe(3);
159
+ });
160
+ it("upsert with empty fields object", async () => {
161
+ const index = tpuf.index(indexId);
162
+ // This should work - document with just id and vector
163
+ await index.upsert({
164
+ id: "minimal-doc",
165
+ vector: vec(4),
166
+ });
167
+ await new Promise((r) => setTimeout(r, 500));
168
+ const hits = await index.query({
169
+ query: [{ vector: [0.1, 0.1, 0.1, 0.1] }],
170
+ topK: 10,
171
+ filter: { id: "minimal-doc" },
172
+ });
173
+ expect(hits.some((h) => h.id === "minimal-doc")).toBe(true);
174
+ });
175
+ it("batch upsert with same id in batch throws", async () => {
176
+ const index = tpuf.index(indexId);
177
+ // Turbopuffer rejects batches with duplicate IDs
178
+ await expect(index.upsert([
179
+ {
180
+ id: "batch-dup",
181
+ content: "First",
182
+ count: 1,
183
+ vector: vec(4, 0.1),
184
+ },
185
+ {
186
+ id: "batch-dup",
187
+ content: "Second",
188
+ count: 2,
189
+ vector: vec(4, 0.2),
190
+ },
191
+ {
192
+ id: "batch-dup",
193
+ content: "Third",
194
+ count: 3,
195
+ vector: vec(4, 0.3),
196
+ },
197
+ ])).rejects.toThrow(/duplicate document IDs/i);
198
+ });
199
+ });
200
+ // ============================================================
201
+ // DOCUMENT DELETE EDGE CASES
202
+ // ============================================================
203
+ describe("document delete edge cases", () => {
204
+ const indexId = `${testPrefix}-delete-edge`;
205
+ beforeAll(async () => {
206
+ await tpuf.createIndex({
207
+ id: indexId,
208
+ schema: {
209
+ content: { type: "string" },
210
+ vector: { type: "vector", dimensions: 4 },
211
+ },
212
+ });
213
+ });
214
+ afterAll(async () => {
215
+ try {
216
+ await tpuf.deleteIndex(indexId);
217
+ }
218
+ catch {
219
+ // Ignore
220
+ }
221
+ });
222
+ it("delete non-existent id does not throw", async () => {
223
+ const index = tpuf.index(indexId);
224
+ // Should not throw - just a no-op
225
+ const result = await index.delete("nonexistent-doc-id-12345");
226
+ // count reflects input, not actual deletions
227
+ expect(result.count).toBe(1);
228
+ });
229
+ it("batch delete with non-existent ids does not throw", async () => {
230
+ const index = tpuf.index(indexId);
231
+ const result = await index.delete([
232
+ "nonexistent-1",
233
+ "nonexistent-2",
234
+ "nonexistent-3",
235
+ ]);
236
+ expect(result.count).toBe(3);
237
+ });
238
+ it("delete empty array returns count 0", async () => {
239
+ const index = tpuf.index(indexId);
240
+ const result = await index.delete([]);
241
+ expect(result.count).toBe(0);
242
+ });
243
+ it("delete then query returns empty", async () => {
244
+ const index = tpuf.index(indexId);
245
+ // Insert
246
+ await index.upsert({
247
+ id: "to-delete",
248
+ content: "Will be deleted",
249
+ vector: vec(4, 0.9),
250
+ });
251
+ await new Promise((r) => setTimeout(r, 500));
252
+ // Verify exists
253
+ let hits = await index.query({
254
+ query: [{ vector: [0.9, 0.9, 0.9, 0.9] }],
255
+ topK: 10,
256
+ filter: { id: "to-delete" },
257
+ });
258
+ expect(hits.length).toBe(1);
259
+ // Delete
260
+ await index.delete("to-delete");
261
+ await new Promise((r) => setTimeout(r, 500));
262
+ // Verify gone
263
+ hits = await index.query({
264
+ query: [{ vector: [0.9, 0.9, 0.9, 0.9] }],
265
+ topK: 10,
266
+ filter: { id: "to-delete" },
267
+ });
268
+ expect(hits.length).toBe(0);
269
+ });
270
+ });
271
+ // ============================================================
272
+ // QUERY EDGE CASES
273
+ // ============================================================
274
+ describe("query edge cases", () => {
275
+ const indexId = `${testPrefix}-query-edge`;
276
+ beforeAll(async () => {
277
+ await tpuf.createIndex({
278
+ id: indexId,
279
+ schema: {
280
+ content: { type: "string", fts: true },
281
+ vector: { type: "vector", dimensions: 4 },
282
+ },
283
+ });
284
+ });
285
+ afterAll(async () => {
286
+ try {
287
+ await tpuf.deleteIndex(indexId);
288
+ }
289
+ catch {
290
+ // Ignore
291
+ }
292
+ });
293
+ it("query on empty index returns empty array", async () => {
294
+ const index = tpuf.index(indexId);
295
+ const hits = await index.query({
296
+ query: [{ vector: [0.1, 0.2, 0.3, 0.4] }],
297
+ topK: 10,
298
+ });
299
+ expect(hits).toEqual([]);
300
+ });
301
+ it("query with filter matching nothing returns empty", async () => {
302
+ const index = tpuf.index(indexId);
303
+ // Add a doc first
304
+ await index.upsert({
305
+ id: "query-edge-doc",
306
+ content: "Test content",
307
+ vector: vec(4),
308
+ });
309
+ await new Promise((r) => setTimeout(r, 500));
310
+ // Query with filter that matches nothing
311
+ const hits = await index.query({
312
+ query: [{ vector: [0.1, 0.1, 0.1, 0.1] }],
313
+ topK: 10,
314
+ filter: { id: "nonexistent-id" },
315
+ });
316
+ expect(hits).toEqual([]);
317
+ });
318
+ it("topK of 0 throws", async () => {
319
+ const index = tpuf.index(indexId);
320
+ // Turbopuffer requires topK between 1 and 1200
321
+ await expect(index.query({
322
+ query: [{ vector: [0.1, 0.1, 0.1, 0.1] }],
323
+ topK: 0,
324
+ })).rejects.toThrow(/top_k must be between 1 and 1200/i);
325
+ });
326
+ });
327
+ // ============================================================
328
+ // VECTOR DIMENSION EDGE CASES
329
+ // ============================================================
330
+ describe("vector dimension edge cases", () => {
331
+ it("upsert with wrong vector dimension throws", async () => {
332
+ const indexId = `${testPrefix}-wrong-dim`;
333
+ await tpuf.createIndex({
334
+ id: indexId,
335
+ schema: {
336
+ vector: { type: "vector", dimensions: 4 },
337
+ },
338
+ });
339
+ const index = tpuf.index(indexId);
340
+ // Try to upsert with wrong dimension (8 instead of 4)
341
+ await expect(index.upsert({
342
+ id: "wrong-dim-doc",
343
+ vector: vec(8), // Wrong dimension!
344
+ })).rejects.toThrow();
345
+ await tpuf.deleteIndex(indexId);
346
+ });
347
+ it("query with wrong vector dimension throws", async () => {
348
+ const indexId = `${testPrefix}-wrong-query-dim`;
349
+ await tpuf.createIndex({
350
+ id: indexId,
351
+ schema: {
352
+ vector: { type: "vector", dimensions: 4 },
353
+ },
354
+ });
355
+ const index = tpuf.index(indexId);
356
+ // Insert valid doc
357
+ await index.upsert({
358
+ id: "valid-doc",
359
+ vector: vec(4),
360
+ });
361
+ await new Promise((r) => setTimeout(r, 500));
362
+ // Query with wrong dimension
363
+ await expect(index.query({
364
+ query: [{ vector: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8] }], // 8 dims
365
+ topK: 10,
366
+ })).rejects.toThrow();
367
+ await tpuf.deleteIndex(indexId);
368
+ });
369
+ });
370
+ });
@@ -0,0 +1,2 @@
1
+ import "@kernl-sdk/ai/openai";
2
+ //# sourceMappingURL=memory.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.integration.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/integration/memory.integration.test.ts"],"names":[],"mappings":"AAIA,OAAO,sBAAsB,CAAC"}
@@ -0,0 +1,287 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
2
+ import { Pool } from "pg";
3
+ import { Kernl, Agent } from "kernl";
4
+ import "@kernl-sdk/ai/openai"; // Register OpenAI embedding provider
5
+ import { postgres } from "@kernl-sdk/pg";
6
+ import { turbopuffer } from "../../index.js";
7
+ const TEST_DB_URL = process.env.KERNL_PG_TEST_URL;
8
+ const TURBOPUFFER_API_KEY = process.env.TURBOPUFFER_API_KEY;
9
+ const TURBOPUFFER_REGION = process.env.TURBOPUFFER_REGION ?? "api";
10
+ describe.sequential("Memory Integration with Turbopuffer", { timeout: 60000 }, () => {
11
+ if (!TEST_DB_URL) {
12
+ it.skip("requires KERNL_PG_TEST_URL environment variable", () => { });
13
+ return;
14
+ }
15
+ if (!TURBOPUFFER_API_KEY) {
16
+ it.skip("requires TURBOPUFFER_API_KEY environment variable", () => { });
17
+ return;
18
+ }
19
+ if (!process.env.OPENAI_API_KEY) {
20
+ it.skip("requires OPENAI_API_KEY environment variable", () => { });
21
+ return;
22
+ }
23
+ let pool;
24
+ let kernl;
25
+ let agent;
26
+ const vectorIndexId = `kernl-test-memories-${Date.now()}`;
27
+ // Unique ID generator to avoid stale data between tests
28
+ let testCounter = 0;
29
+ const uid = (base) => `${base}-${Date.now()}-${++testCounter}`;
30
+ beforeAll(async () => {
31
+ pool = new Pool({ connectionString: TEST_DB_URL });
32
+ // Clean slate for postgres
33
+ await pool.query('DROP SCHEMA IF EXISTS "kernl" CASCADE');
34
+ // Create Kernl with PG for relational + Turbopuffer for vector
35
+ kernl = new Kernl({
36
+ storage: {
37
+ db: postgres({ pool }),
38
+ vector: turbopuffer({
39
+ apiKey: TURBOPUFFER_API_KEY,
40
+ region: TURBOPUFFER_REGION,
41
+ }),
42
+ },
43
+ memory: {
44
+ embeddingModel: "openai/text-embedding-3-small",
45
+ dimensions: 1536,
46
+ indexId: vectorIndexId,
47
+ },
48
+ });
49
+ // Register a dummy agent for test scope
50
+ const model = {
51
+ spec: "1.0",
52
+ provider: "test",
53
+ modelId: "test-model",
54
+ };
55
+ agent = new Agent({
56
+ id: "test-agent",
57
+ name: "Test Agent",
58
+ instructions: () => "test instructions",
59
+ model,
60
+ });
61
+ kernl.register(agent);
62
+ // Initialize storage (creates "kernl" schema and tables)
63
+ await kernl.storage.init();
64
+ });
65
+ afterAll(async () => {
66
+ // Clean up Turbopuffer namespace
67
+ try {
68
+ const tpuf = turbopuffer({
69
+ apiKey: TURBOPUFFER_API_KEY,
70
+ region: TURBOPUFFER_REGION,
71
+ });
72
+ await tpuf.deleteIndex(vectorIndexId);
73
+ }
74
+ catch {
75
+ // Ignore if already deleted
76
+ }
77
+ if (kernl) {
78
+ await kernl.storage.close();
79
+ }
80
+ });
81
+ beforeEach(async () => {
82
+ // Clean memories in postgres
83
+ await pool.query('DELETE FROM "kernl"."memories"');
84
+ // Note: We don't delete the Turbopuffer namespace between tests because
85
+ // MemoryIndexHandle caches its init state. The namespace is cleaned up
86
+ // in afterAll. Tests use unique memory IDs so they don't conflict.
87
+ });
88
+ it("creates memory and indexes it in Turbopuffer", async () => {
89
+ const id = uid("m");
90
+ const memory = await agent.memories.create({
91
+ id,
92
+ namespace: "test",
93
+ collection: "facts",
94
+ content: { text: "The user loves TypeScript programming" },
95
+ });
96
+ expect(memory.id).toBe(id);
97
+ expect(memory.content.text).toBe("The user loves TypeScript programming");
98
+ // Verify memory exists in postgres
99
+ const dbResult = await pool.query('SELECT * FROM "kernl"."memories" WHERE id = $1', [id]);
100
+ expect(dbResult.rows).toHaveLength(1);
101
+ // Verify memory is searchable in Turbopuffer
102
+ const results = await agent.memories.search({
103
+ query: "TypeScript",
104
+ limit: 10,
105
+ });
106
+ expect(results.length).toBeGreaterThan(0);
107
+ expect(results[0].document?.id).toBe(id);
108
+ });
109
+ it("searches memories using vector search", async () => {
110
+ // Create several memories with unique IDs
111
+ const id1 = uid("m");
112
+ const id2 = uid("m");
113
+ const id3 = uid("m");
114
+ await agent.memories.create({
115
+ id: id1,
116
+ namespace: "test",
117
+ collection: "facts",
118
+ content: { text: "The user loves TypeScript programming" },
119
+ });
120
+ await agent.memories.create({
121
+ id: id2,
122
+ namespace: "test",
123
+ collection: "facts",
124
+ content: { text: "The user enjoys cooking Italian food" },
125
+ });
126
+ await agent.memories.create({
127
+ id: id3,
128
+ namespace: "test",
129
+ collection: "facts",
130
+ content: { text: "TypeScript has excellent type safety" },
131
+ });
132
+ // Search for TypeScript-related memories (vector-only since hybrid not supported)
133
+ const results = await agent.memories.search({
134
+ query: "programming languages",
135
+ limit: 10,
136
+ });
137
+ expect(results.length).toBeGreaterThan(0);
138
+ // Should find TypeScript-related memories with higher scores
139
+ const ids = results.map((r) => r.document?.id);
140
+ expect(ids).toContain(id1); // Direct match
141
+ expect(ids).toContain(id3); // Related to TypeScript
142
+ });
143
+ it("returns no results when filters exclude all matches", async () => {
144
+ const id = uid("m");
145
+ const ns = uid("ns");
146
+ await agent.memories.create({
147
+ id,
148
+ namespace: ns,
149
+ collection: "facts",
150
+ content: { text: "User likes hiking" },
151
+ });
152
+ // Filter for a different namespace that has no memories
153
+ const results = await agent.memories.search({
154
+ query: "hiking",
155
+ filter: { scope: { namespace: "nonexistent-ns" } },
156
+ limit: 10,
157
+ });
158
+ expect(results.length).toBe(0);
159
+ });
160
+ it("filters search results by scope", async () => {
161
+ const id1 = uid("m");
162
+ const id2 = uid("m");
163
+ const ns1 = uid("user");
164
+ const ns2 = uid("user");
165
+ await agent.memories.create({
166
+ id: id1,
167
+ namespace: ns1,
168
+ collection: "facts",
169
+ content: { text: "User 1 likes cats" },
170
+ });
171
+ await agent.memories.create({
172
+ id: id2,
173
+ namespace: ns2,
174
+ collection: "facts",
175
+ content: { text: "User 2 likes cats" },
176
+ });
177
+ // Search only in ns1 namespace
178
+ const results = await agent.memories.search({
179
+ query: "cats",
180
+ filter: { scope: { namespace: ns1 } },
181
+ limit: 10,
182
+ });
183
+ expect(results.length).toBe(1);
184
+ expect(results[0].document?.id).toBe(id1);
185
+ });
186
+ it("respects topK limit", async () => {
187
+ const ns = uid("ns");
188
+ await agent.memories.create({
189
+ namespace: ns,
190
+ collection: "facts",
191
+ content: { text: "The user likes TypeScript" },
192
+ });
193
+ await agent.memories.create({
194
+ namespace: ns,
195
+ collection: "facts",
196
+ content: { text: "The user likes JavaScript" },
197
+ });
198
+ await agent.memories.create({
199
+ namespace: ns,
200
+ collection: "facts",
201
+ content: { text: "The user likes Rust" },
202
+ });
203
+ // Filter by namespace to only get our test's memories
204
+ const results = await agent.memories.search({
205
+ query: "programming languages",
206
+ filter: { scope: { namespace: ns } },
207
+ limit: 1,
208
+ });
209
+ expect(results.length).toBe(1);
210
+ });
211
+ it("updates memory content and re-indexes", async () => {
212
+ const id = uid("m");
213
+ const ns = uid("ns");
214
+ await agent.memories.create({
215
+ id,
216
+ namespace: ns,
217
+ collection: "facts",
218
+ content: { text: "Original content about dogs" },
219
+ });
220
+ // Update content (still use kernl.memories for update - not yet on agent API)
221
+ await kernl.memories.update({
222
+ id,
223
+ content: { text: "Updated content about cats" },
224
+ });
225
+ // Search should find updated content
226
+ const results = await agent.memories.search({
227
+ query: "cats",
228
+ filter: { scope: { namespace: ns } },
229
+ limit: 10,
230
+ });
231
+ expect(results.length).toBeGreaterThan(0);
232
+ const match = results.find((r) => r.document?.id === id);
233
+ expect(match).toBeDefined();
234
+ expect(match?.document?.text).toBe("Updated content about cats");
235
+ });
236
+ it("creates memories with multimodal content", async () => {
237
+ const id = uid("m");
238
+ const ns = uid("ns");
239
+ await agent.memories.create({
240
+ id,
241
+ namespace: ns,
242
+ collection: "media",
243
+ content: {
244
+ text: "A beautiful sunset",
245
+ image: {
246
+ data: "base64encodedimage",
247
+ mime: "image/png",
248
+ alt: "Sunset over the ocean",
249
+ },
250
+ },
251
+ });
252
+ // Should be searchable by text
253
+ const results = await agent.memories.search({
254
+ query: "sunset",
255
+ filter: { scope: { namespace: ns } },
256
+ limit: 10,
257
+ });
258
+ expect(results.length).toBeGreaterThan(0);
259
+ const match = results.find((r) => r.document?.id === id);
260
+ expect(match).toBeDefined();
261
+ });
262
+ it("filters by collection", async () => {
263
+ const id1 = uid("m");
264
+ const id2 = uid("m");
265
+ const ns = uid("ns");
266
+ await agent.memories.create({
267
+ id: id1,
268
+ namespace: ns,
269
+ collection: "facts",
270
+ content: { text: "This is a fact about programming" },
271
+ });
272
+ await agent.memories.create({
273
+ id: id2,
274
+ namespace: ns,
275
+ collection: "preferences",
276
+ content: { text: "User prefers programming in TypeScript" },
277
+ });
278
+ // Filter by collection and namespace
279
+ const results = await agent.memories.search({
280
+ query: "programming",
281
+ filter: { scope: { namespace: ns }, collections: ["facts"] },
282
+ limit: 10,
283
+ });
284
+ expect(results.length).toBe(1);
285
+ expect(results[0].document?.id).toBe(id1);
286
+ });
287
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comprehensive query modes integration tests.
3
+ *
4
+ * Tests vector search, BM25 text search, hybrid queries, fusion modes,
5
+ * topK behavior, include semantics, and rank ordering against real Turbopuffer API.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=query.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.integration.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/integration/query.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}