@soleri/core 8.1.0 → 9.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.
Files changed (128) hide show
  1. package/dist/brain/brain.d.ts +1 -8
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +5 -134
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/cognee/client.d.ts +5 -0
  6. package/dist/cognee/client.d.ts.map +1 -1
  7. package/dist/cognee/client.js +83 -16
  8. package/dist/cognee/client.js.map +1 -1
  9. package/dist/cognee/sync-manager.d.ts +67 -8
  10. package/dist/cognee/sync-manager.d.ts.map +1 -1
  11. package/dist/cognee/sync-manager.js +129 -32
  12. package/dist/cognee/sync-manager.js.map +1 -1
  13. package/dist/cognee/types.d.ts +16 -0
  14. package/dist/cognee/types.d.ts.map +1 -1
  15. package/dist/context/context-engine.d.ts +2 -5
  16. package/dist/context/context-engine.d.ts.map +1 -1
  17. package/dist/context/context-engine.js +4 -31
  18. package/dist/context/context-engine.js.map +1 -1
  19. package/dist/curator/curator.d.ts +2 -5
  20. package/dist/curator/curator.d.ts.map +1 -1
  21. package/dist/curator/curator.js +4 -23
  22. package/dist/curator/curator.js.map +1 -1
  23. package/dist/engine/bin/soleri-engine.js +6 -5
  24. package/dist/engine/bin/soleri-engine.js.map +1 -1
  25. package/dist/engine/core-ops.d.ts.map +1 -1
  26. package/dist/engine/core-ops.js +11 -6
  27. package/dist/engine/core-ops.js.map +1 -1
  28. package/dist/engine/register-engine.d.ts.map +1 -1
  29. package/dist/engine/register-engine.js +0 -7
  30. package/dist/engine/register-engine.js.map +1 -1
  31. package/dist/index.d.ts +5 -5
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -4
  34. package/dist/index.js.map +1 -1
  35. package/dist/intelligence/types.d.ts +7 -0
  36. package/dist/intelligence/types.d.ts.map +1 -1
  37. package/dist/persona/defaults.d.ts +16 -0
  38. package/dist/persona/defaults.d.ts.map +1 -0
  39. package/dist/persona/defaults.js +78 -0
  40. package/dist/persona/defaults.js.map +1 -0
  41. package/dist/persona/index.d.ts +5 -0
  42. package/dist/persona/index.d.ts.map +1 -0
  43. package/dist/persona/index.js +4 -0
  44. package/dist/persona/index.js.map +1 -0
  45. package/dist/persona/loader.d.ts +11 -0
  46. package/dist/persona/loader.d.ts.map +1 -0
  47. package/dist/persona/loader.js +45 -0
  48. package/dist/persona/loader.js.map +1 -0
  49. package/dist/persona/prompt-generator.d.ts +13 -0
  50. package/dist/persona/prompt-generator.d.ts.map +1 -0
  51. package/dist/persona/prompt-generator.js +89 -0
  52. package/dist/persona/prompt-generator.js.map +1 -0
  53. package/dist/persona/types.d.ts +56 -0
  54. package/dist/persona/types.d.ts.map +1 -0
  55. package/dist/persona/types.js +9 -0
  56. package/dist/persona/types.js.map +1 -0
  57. package/dist/plugins/types.d.ts +13 -13
  58. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  59. package/dist/runtime/admin-extra-ops.js +5 -27
  60. package/dist/runtime/admin-extra-ops.js.map +1 -1
  61. package/dist/runtime/admin-ops.d.ts.map +1 -1
  62. package/dist/runtime/admin-ops.js +5 -37
  63. package/dist/runtime/admin-ops.js.map +1 -1
  64. package/dist/runtime/capture-ops.d.ts.map +1 -1
  65. package/dist/runtime/capture-ops.js +32 -16
  66. package/dist/runtime/capture-ops.js.map +1 -1
  67. package/dist/runtime/cognee-sync-ops.d.ts +2 -2
  68. package/dist/runtime/cognee-sync-ops.d.ts.map +1 -1
  69. package/dist/runtime/cognee-sync-ops.js +45 -7
  70. package/dist/runtime/cognee-sync-ops.js.map +1 -1
  71. package/dist/runtime/facades/index.d.ts +1 -1
  72. package/dist/runtime/facades/index.d.ts.map +1 -1
  73. package/dist/runtime/facades/index.js +1 -10
  74. package/dist/runtime/facades/index.js.map +1 -1
  75. package/dist/runtime/runtime.d.ts.map +1 -1
  76. package/dist/runtime/runtime.js +14 -53
  77. package/dist/runtime/runtime.js.map +1 -1
  78. package/dist/runtime/types.d.ts +6 -8
  79. package/dist/runtime/types.d.ts.map +1 -1
  80. package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
  81. package/dist/runtime/vault-linking-ops.js +40 -0
  82. package/dist/runtime/vault-linking-ops.js.map +1 -1
  83. package/dist/runtime/vault-sharing-ops.d.ts.map +1 -1
  84. package/dist/runtime/vault-sharing-ops.js +53 -3
  85. package/dist/runtime/vault-sharing-ops.js.map +1 -1
  86. package/dist/vault/linking.d.ts +37 -0
  87. package/dist/vault/linking.d.ts.map +1 -1
  88. package/dist/vault/linking.js +73 -0
  89. package/dist/vault/linking.js.map +1 -1
  90. package/dist/vault/vault.d.ts +0 -2
  91. package/dist/vault/vault.d.ts.map +1 -1
  92. package/dist/vault/vault.js +0 -13
  93. package/dist/vault/vault.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/brain/brain.ts +4 -157
  96. package/src/context/context-engine.ts +3 -31
  97. package/src/curator/curator.ts +5 -28
  98. package/src/engine/bin/soleri-engine.ts +6 -5
  99. package/src/engine/core-ops.ts +11 -6
  100. package/src/engine/register-engine.ts +0 -7
  101. package/src/index.ts +20 -16
  102. package/src/intelligence/types.ts +8 -0
  103. package/src/persona/defaults.ts +96 -0
  104. package/src/persona/index.ts +9 -0
  105. package/src/persona/loader.ts +50 -0
  106. package/src/persona/prompt-generator.ts +109 -0
  107. package/src/persona/types.ts +72 -0
  108. package/src/runtime/admin-extra-ops.ts +5 -28
  109. package/src/runtime/admin-ops.ts +5 -38
  110. package/src/runtime/capture-ops.ts +33 -14
  111. package/src/runtime/facades/index.ts +1 -11
  112. package/src/runtime/runtime.ts +14 -54
  113. package/src/runtime/types.ts +6 -8
  114. package/src/runtime/vault-linking-ops.ts +41 -0
  115. package/src/runtime/vault-sharing-ops.ts +63 -4
  116. package/src/vault/linking.ts +94 -0
  117. package/src/vault/vault.ts +0 -14
  118. package/src/__tests__/cognee-client-gaps.test.ts +0 -474
  119. package/src/__tests__/cognee-client.test.ts +0 -524
  120. package/src/__tests__/cognee-hybrid-search.test.ts +0 -492
  121. package/src/__tests__/cognee-integration.test.ts +0 -80
  122. package/src/__tests__/cognee-sync-manager-deep.test.ts +0 -654
  123. package/src/__tests__/cognee-sync-manager.test.ts +0 -104
  124. package/src/cognee/client.ts +0 -370
  125. package/src/cognee/sync-manager.ts +0 -389
  126. package/src/cognee/types.ts +0 -62
  127. package/src/runtime/cognee-sync-ops.ts +0 -63
  128. package/src/runtime/facades/cognee-facade.ts +0 -164
@@ -169,6 +169,28 @@ export class LinkManager {
169
169
  return result;
170
170
  }
171
171
 
172
+ // ===========================================================================
173
+ // BULK QUERIES
174
+ // ===========================================================================
175
+
176
+ /**
177
+ * Get all links where either source or target is in the given ID set.
178
+ * Used for pack export — to find links within an export set.
179
+ */
180
+ getAllLinksForEntries(entryIds: string[]): VaultLink[] {
181
+ if (entryIds.length === 0) return [];
182
+ try {
183
+ const placeholders = entryIds.map(() => '?').join(',');
184
+ const rows = this.provider.all<VaultLinkRow>(
185
+ `SELECT * FROM vault_links WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`,
186
+ [...entryIds, ...entryIds],
187
+ );
188
+ return rows.map(rowToVaultLink);
189
+ } catch {
190
+ return [];
191
+ }
192
+ }
193
+
172
194
  // ===========================================================================
173
195
  // ORPHAN DETECTION
174
196
  // ===========================================================================
@@ -292,6 +314,78 @@ export class LinkManager {
292
314
  }
293
315
  }
294
316
 
317
+ // ===========================================================================
318
+ // BACKFILL — one-time link generation for existing entries
319
+ // ===========================================================================
320
+
321
+ /**
322
+ * Generate links for orphan entries using FTS5 suggestions.
323
+ * Processes orphans in batches and creates links above the threshold.
324
+ *
325
+ * @param opts.threshold Minimum suggestion score to auto-create link (default: 0.7)
326
+ * @param opts.maxLinks Max links per entry (default: 3)
327
+ * @param opts.dryRun Preview without creating links (default: false)
328
+ * @param opts.batchSize Entries per batch (default: 50)
329
+ * @param opts.onProgress Progress callback
330
+ * @returns Stats: processed, linksCreated, durationMs
331
+ */
332
+ backfillLinks(opts?: {
333
+ threshold?: number;
334
+ maxLinks?: number;
335
+ dryRun?: boolean;
336
+ batchSize?: number;
337
+ onProgress?: (stats: { processed: number; total: number; linksCreated: number }) => void;
338
+ }): {
339
+ processed: number;
340
+ linksCreated: number;
341
+ durationMs: number;
342
+ preview?: Array<{ sourceId: string; targetId: string; linkType: string; score: number }>;
343
+ } {
344
+ const threshold = opts?.threshold ?? 0.7;
345
+ const maxLinks = opts?.maxLinks ?? 3;
346
+ const dryRun = opts?.dryRun ?? false;
347
+ const batchSize = opts?.batchSize ?? 50;
348
+ const start = Date.now();
349
+
350
+ const orphans = this.getOrphans(10000);
351
+ let processed = 0;
352
+ let linksCreated = 0;
353
+ const preview: Array<{ sourceId: string; targetId: string; linkType: string; score: number }> =
354
+ [];
355
+
356
+ for (let i = 0; i < orphans.length; i += batchSize) {
357
+ const batch = orphans.slice(i, i + batchSize);
358
+ for (const entry of batch) {
359
+ const suggestions = this.suggestLinks(entry.id, maxLinks + 2);
360
+ const qualifying = suggestions.filter((s) => s.score >= threshold).slice(0, maxLinks);
361
+
362
+ for (const s of qualifying) {
363
+ if (dryRun) {
364
+ preview.push({
365
+ sourceId: entry.id,
366
+ targetId: s.entryId,
367
+ linkType: s.suggestedType,
368
+ score: s.score,
369
+ });
370
+ } else {
371
+ this.addLink(entry.id, s.entryId, s.suggestedType);
372
+ }
373
+ linksCreated++;
374
+ }
375
+ processed++;
376
+ }
377
+
378
+ opts?.onProgress?.({ processed, total: orphans.length, linksCreated });
379
+ }
380
+
381
+ return {
382
+ processed,
383
+ linksCreated,
384
+ durationMs: Date.now() - start,
385
+ ...(dryRun ? { preview } : {}),
386
+ };
387
+ }
388
+
295
389
  // ===========================================================================
296
390
  // PRIVATE
297
391
  // ===========================================================================
@@ -52,7 +52,6 @@ export interface MemoryStats {
52
52
  export class Vault {
53
53
  private provider: PersistenceProvider;
54
54
  private sqliteProvider: SQLitePersistenceProvider | null;
55
- private syncManager: import('../cognee/sync-manager.js').CogneeSyncManager | null = null;
56
55
  private linkManager: LinkManager | null = null;
57
56
  private autoLinkEnabled = true;
58
57
  /** Minimum number of FTS5 suggestions to auto-link. Top N are linked. */
@@ -104,10 +103,6 @@ export class Vault {
104
103
  // current < FORMAT_VERSION → future: run migration scripts here
105
104
  }
106
105
 
107
- setSyncManager(mgr: import('../cognee/sync-manager.js').CogneeSyncManager): void {
108
- this.syncManager = mgr;
109
- }
110
-
111
106
  setLinkManager(mgr: LinkManager, opts?: { enabled?: boolean; maxLinks?: number }): void {
112
107
  this.linkManager = mgr;
113
108
  if (opts?.enabled !== undefined) this.autoLinkEnabled = opts.enabled;
@@ -414,9 +409,6 @@ export class Vault {
414
409
  origin: entry.origin ?? 'agent',
415
410
  });
416
411
  count++;
417
- if (this.syncManager) {
418
- this.syncManager.enqueue('ingest', entry.id, entry);
419
- }
420
412
  }
421
413
  // Auto-link after all entries are inserted (so they can link to each other).
422
414
  // Skip for large batches (>100) — use relink_vault for bulk imports.
@@ -608,9 +600,6 @@ export class Vault {
608
600
  }
609
601
  remove(id: string): boolean {
610
602
  const deleted = this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes > 0;
611
- if (deleted && this.syncManager) {
612
- this.syncManager.enqueue('delete', id);
613
- }
614
603
  return deleted;
615
604
  }
616
605
 
@@ -684,9 +673,6 @@ export class Vault {
684
673
  let count = 0;
685
674
  for (const id of ids) {
686
675
  count += this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes;
687
- if (this.syncManager) {
688
- this.syncManager.enqueue('delete', id);
689
- }
690
676
  }
691
677
  return count;
692
678
  });
@@ -1,474 +0,0 @@
1
- /**
2
- * CogneeClient gap tests — covers behaviors missing from the original test suite.
3
- *
4
- * Source of truth: these tests define expected behavior.
5
- * Code adapts to fulfill them.
6
- */
7
-
8
- import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest';
9
- import { CogneeClient } from '../cognee/client.js';
10
- import type { IntelligenceEntry } from '../intelligence/types.js';
11
-
12
- // ─── Helpers ──────────────────────────────────────────────────────
13
-
14
- function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
15
- return {
16
- id: overrides.id ?? 'test-1',
17
- type: overrides.type ?? 'pattern',
18
- domain: overrides.domain ?? 'testing',
19
- title: overrides.title ?? 'Test Pattern',
20
- severity: overrides.severity ?? 'warning',
21
- description: overrides.description ?? 'A test pattern for unit tests.',
22
- tags: overrides.tags ?? ['testing', 'assertions'],
23
- ...(overrides.context !== undefined ? { context: overrides.context } : {}),
24
- ...(overrides.example !== undefined ? { example: overrides.example } : {}),
25
- ...(overrides.why !== undefined ? { why: overrides.why } : {}),
26
- ...(overrides.counterExample !== undefined ? { counterExample: overrides.counterExample } : {}),
27
- };
28
- }
29
-
30
- function isHealthCheck(url: string, init?: RequestInit): boolean {
31
- return url.endsWith(':8000/') || (url.endsWith('/') && (!init?.method || init.method === 'GET'));
32
- }
33
-
34
- function isAuthCall(url: string): boolean {
35
- return url.includes('/api/v1/auth/');
36
- }
37
-
38
- function mockWithAuth(apiHandler?: (url: string, init?: RequestInit) => Promise<Response>) {
39
- const mock = vi.fn(async (url: string, init?: RequestInit) => {
40
- if (isHealthCheck(url, init)) return new Response('ok', { status: 200 });
41
- if (isAuthCall(url)) {
42
- if (url.includes('/login')) {
43
- return new Response(JSON.stringify({ access_token: 'test-jwt' }), { status: 200 });
44
- }
45
- if (url.includes('/register')) {
46
- return new Response(JSON.stringify({ id: 'new-user' }), { status: 200 });
47
- }
48
- }
49
- if (apiHandler) return apiHandler(url, init);
50
- return new Response('ok', { status: 200 });
51
- });
52
- vi.stubGlobal('fetch', mock);
53
- return mock;
54
- }
55
-
56
- // ─── Tests ────────────────────────────────────────────────────────
57
-
58
- describe('CogneeClient — gap coverage', () => {
59
- afterEach(() => {
60
- vi.restoreAllMocks();
61
- });
62
-
63
- // ─── deleteEntries ────────────────────────────────────────────
64
-
65
- describe('deleteEntries', () => {
66
- it('should call POST /api/v1/delete with dataset and entryIds', async () => {
67
- let capturedBody = '';
68
- mockWithAuth(async (_url, init) => {
69
- capturedBody = init?.body as string;
70
- return new Response('ok', { status: 200 });
71
- });
72
- const client = new CogneeClient({ dataset: 'my-ds' });
73
- await client.healthCheck();
74
- const result = await client.deleteEntries(['e1', 'e2']);
75
- expect(result.deleted).toBe(2);
76
- const parsed = JSON.parse(capturedBody);
77
- expect(parsed.datasetName).toBe('my-ds');
78
- expect(parsed.entryIds).toEqual(['e1', 'e2']);
79
- });
80
-
81
- it('should return 0 when not available', async () => {
82
- const client = new CogneeClient();
83
- const result = await client.deleteEntries(['e1']);
84
- expect(result.deleted).toBe(0);
85
- });
86
-
87
- it('should return 0 for empty entryIds', async () => {
88
- mockWithAuth();
89
- const client = new CogneeClient();
90
- await client.healthCheck();
91
- const result = await client.deleteEntries([]);
92
- expect(result.deleted).toBe(0);
93
- });
94
-
95
- it('should return 0 on HTTP error without throwing', async () => {
96
- mockWithAuth(async () => new Response('error', { status: 500 }));
97
- const client = new CogneeClient();
98
- await client.healthCheck();
99
- const result = await client.deleteEntries(['e1']);
100
- expect(result.deleted).toBe(0);
101
- });
102
-
103
- it('should return 0 on network error without throwing', async () => {
104
- mockWithAuth(async () => {
105
- throw new Error('ECONNRESET');
106
- });
107
- const client = new CogneeClient();
108
- await client.healthCheck();
109
- const result = await client.deleteEntries(['e1']);
110
- expect(result.deleted).toBe(0);
111
- });
112
- });
113
-
114
- // ─── Entry serialization ──────────────────────────────────────
115
-
116
- describe('entry serialization (via addEntries FormData)', () => {
117
- it('should include vault-id prefix in serialized text', async () => {
118
- let capturedText = '';
119
- mockWithAuth(async (_url, init) => {
120
- const body = init?.body as FormData;
121
- if (body instanceof FormData) {
122
- const file = body.get('data') as File;
123
- if (file) capturedText = await file.text();
124
- }
125
- return new Response('ok', { status: 200 });
126
- });
127
- const client = new CogneeClient();
128
- await client.healthCheck();
129
- await client.addEntries([makeEntry({ id: 'my-entry-42', title: 'My Title' })]);
130
- expect(capturedText).toContain('[vault-id:my-entry-42]');
131
- expect(capturedText).toContain('My Title');
132
- client.resetPendingCognify();
133
- });
134
-
135
- it('should include description in serialized text', async () => {
136
- let capturedText = '';
137
- mockWithAuth(async (_url, init) => {
138
- const body = init?.body as FormData;
139
- if (body instanceof FormData) {
140
- const file = body.get('data') as File;
141
- if (file) capturedText = await file.text();
142
- }
143
- return new Response('ok', { status: 200 });
144
- });
145
- const client = new CogneeClient();
146
- await client.healthCheck();
147
- await client.addEntries([makeEntry({ description: 'Unique description text here' })]);
148
- expect(capturedText).toContain('Unique description text here');
149
- client.resetPendingCognify();
150
- });
151
-
152
- it('should include context when present', async () => {
153
- let capturedText = '';
154
- mockWithAuth(async (_url, init) => {
155
- const body = init?.body as FormData;
156
- if (body instanceof FormData) {
157
- const file = body.get('data') as File;
158
- if (file) capturedText = await file.text();
159
- }
160
- return new Response('ok', { status: 200 });
161
- });
162
- const client = new CogneeClient();
163
- await client.healthCheck();
164
- await client.addEntries([makeEntry({ context: 'Apply in production code only' })]);
165
- expect(capturedText).toContain('Apply in production code only');
166
- client.resetPendingCognify();
167
- });
168
-
169
- it('should include tags when present', async () => {
170
- let capturedText = '';
171
- mockWithAuth(async (_url, init) => {
172
- const body = init?.body as FormData;
173
- if (body instanceof FormData) {
174
- const file = body.get('data') as File;
175
- if (file) capturedText = await file.text();
176
- }
177
- return new Response('ok', { status: 200 });
178
- });
179
- const client = new CogneeClient();
180
- await client.healthCheck();
181
- await client.addEntries([makeEntry({ tags: ['react', 'performance'] })]);
182
- expect(capturedText).toContain('Tags: react, performance');
183
- client.resetPendingCognify();
184
- });
185
-
186
- it('should not include Tags line when tags are empty', async () => {
187
- let capturedText = '';
188
- mockWithAuth(async (_url, init) => {
189
- const body = init?.body as FormData;
190
- if (body instanceof FormData) {
191
- const file = body.get('data') as File;
192
- if (file) capturedText = await file.text();
193
- }
194
- return new Response('ok', { status: 200 });
195
- });
196
- const client = new CogneeClient();
197
- await client.healthCheck();
198
- await client.addEntries([makeEntry({ tags: [] })]);
199
- expect(capturedText).not.toContain('Tags:');
200
- client.resetPendingCognify();
201
- });
202
-
203
- it('should use entry.id as filename', async () => {
204
- let capturedFilename = '';
205
- mockWithAuth(async (_url, init) => {
206
- const body = init?.body as FormData;
207
- if (body instanceof FormData) {
208
- const file = body.get('data') as File;
209
- if (file) capturedFilename = file.name;
210
- }
211
- return new Response('ok', { status: 200 });
212
- });
213
- const client = new CogneeClient();
214
- await client.healthCheck();
215
- await client.addEntries([makeEntry({ id: 'pattern-arch-123' })]);
216
- expect(capturedFilename).toBe('pattern-arch-123.txt');
217
- client.resetPendingCognify();
218
- });
219
- });
220
-
221
- // ─── Debounce sliding window ──────────────────────────────────
222
-
223
- describe('cognify debounce — sliding window', () => {
224
- beforeEach(() => {
225
- vi.useFakeTimers();
226
- });
227
-
228
- afterEach(() => {
229
- vi.useRealTimers();
230
- vi.restoreAllMocks();
231
- });
232
-
233
- it('should extend debounce window on rapid ingests (sliding, not fixed)', async () => {
234
- let cognifyCount = 0;
235
- mockWithAuth(async (url) => {
236
- if (url.includes('/cognify')) cognifyCount++;
237
- return new Response('ok', { status: 200 });
238
- });
239
- const client = new CogneeClient({ cognifyDebounceMs: 100 });
240
- await client.healthCheck();
241
-
242
- // First ingest at t=0
243
- await client.addEntries([makeEntry({ id: 'e1' })]);
244
- // Advance 80ms (within window), second ingest resets the timer
245
- await vi.advanceTimersByTimeAsync(80);
246
- await client.addEntries([makeEntry({ id: 'e2' })]);
247
- // Advance 80ms (within the RESET window) — cognify should NOT have fired yet
248
- await vi.advanceTimersByTimeAsync(80);
249
- expect(cognifyCount).toBe(0);
250
-
251
- // Advance past the reset window (another 30ms = 110ms from second ingest)
252
- await vi.advanceTimersByTimeAsync(30);
253
- expect(cognifyCount).toBe(1);
254
- client.resetPendingCognify();
255
- });
256
-
257
- it('should fire separate cognify for different datasets', async () => {
258
- const cognifyDatasets: string[][] = [];
259
- mockWithAuth(async (url, init) => {
260
- if (url.includes('/cognify')) {
261
- const body = JSON.parse(init?.body as string);
262
- cognifyDatasets.push(body.datasets);
263
- }
264
- return new Response('ok', { status: 200 });
265
- });
266
- // Two clients with different datasets
267
- const client1 = new CogneeClient({ dataset: 'ds-alpha', cognifyDebounceMs: 50 });
268
- const client2 = new CogneeClient({ dataset: 'ds-beta', cognifyDebounceMs: 50 });
269
- await client1.healthCheck();
270
- await client2.healthCheck();
271
-
272
- await client1.addEntries([makeEntry({ id: 'a1' })]);
273
- await client2.addEntries([makeEntry({ id: 'b1' })]);
274
-
275
- await vi.advanceTimersByTimeAsync(60);
276
-
277
- // Each dataset should cognify independently
278
- expect(cognifyDatasets).toHaveLength(2);
279
- const allDatasets = cognifyDatasets.flat();
280
- expect(allDatasets).toContain('ds-alpha');
281
- expect(allDatasets).toContain('ds-beta');
282
-
283
- client1.resetPendingCognify();
284
- client2.resetPendingCognify();
285
- });
286
-
287
- it('should coalesce multiple ingests to same dataset into one cognify', async () => {
288
- let cognifyCount = 0;
289
- mockWithAuth(async (url) => {
290
- if (url.includes('/cognify')) cognifyCount++;
291
- return new Response('ok', { status: 200 });
292
- });
293
- const client = new CogneeClient({ cognifyDebounceMs: 50 });
294
- await client.healthCheck();
295
-
296
- await client.addEntries([makeEntry({ id: 'e1' })]);
297
- await client.addEntries([makeEntry({ id: 'e2' })]);
298
- await client.addEntries([makeEntry({ id: 'e3' })]);
299
-
300
- await vi.advanceTimersByTimeAsync(60);
301
-
302
- expect(cognifyCount).toBe(1);
303
- client.resetPendingCognify();
304
- });
305
- });
306
-
307
- // ─── Concurrent operations ────────────────────────────────────
308
-
309
- describe('concurrent operations', () => {
310
- it('should handle parallel addEntries without interference', async () => {
311
- mockWithAuth();
312
- const client = new CogneeClient();
313
- await client.healthCheck();
314
-
315
- const results = await Promise.all([
316
- client.addEntries([makeEntry({ id: 'p1' })]),
317
- client.addEntries([makeEntry({ id: 'p2' })]),
318
- client.addEntries([makeEntry({ id: 'p3' })]),
319
- client.addEntries([makeEntry({ id: 'p4' })]),
320
- client.addEntries([makeEntry({ id: 'p5' })]),
321
- ]);
322
-
323
- expect(results.every((r) => r.added === 1)).toBe(true);
324
- client.resetPendingCognify();
325
- });
326
-
327
- it('should handle parallel search calls', async () => {
328
- mockWithAuth(
329
- async () =>
330
- new Response(JSON.stringify([{ id: 'r1', text: 'Result', score: 0.9 }]), { status: 200 }),
331
- );
332
- const client = new CogneeClient();
333
- await client.healthCheck();
334
-
335
- const results = await Promise.all([
336
- client.search('query 1'),
337
- client.search('query 2'),
338
- client.search('query 3'),
339
- ]);
340
-
341
- expect(results.every((r) => r.length === 1)).toBe(true);
342
- });
343
- });
344
-
345
- // ─── Search edge cases ────────────────────────────────────────
346
-
347
- describe('search edge cases', () => {
348
- it('should handle empty string query gracefully', async () => {
349
- mockWithAuth(async () => new Response(JSON.stringify([]), { status: 200 }));
350
- const client = new CogneeClient();
351
- await client.healthCheck();
352
- const results = await client.search('');
353
- expect(Array.isArray(results)).toBe(true);
354
- });
355
-
356
- it('should handle results with non-string text field', async () => {
357
- mockWithAuth(
358
- async () =>
359
- new Response(
360
- JSON.stringify([
361
- { id: 'r1', text: 42, score: 0.8 },
362
- { id: 'r2', score: 0.7 },
363
- ]),
364
- { status: 200 },
365
- ),
366
- );
367
- const client = new CogneeClient();
368
- await client.healthCheck();
369
- const results = await client.search('query');
370
- // text should be coerced to string
371
- expect(typeof results[0].text).toBe('string');
372
- expect(typeof results[1].text).toBe('string');
373
- });
374
-
375
- it('should handle null/undefined text field', async () => {
376
- mockWithAuth(
377
- async () =>
378
- new Response(JSON.stringify([{ id: 'r1', text: null, score: 0.5 }]), { status: 200 }),
379
- );
380
- const client = new CogneeClient();
381
- await client.healthCheck();
382
- const results = await client.search('query');
383
- expect(typeof results[0].text).toBe('string');
384
- });
385
- });
386
-
387
- // ─── Auth edge cases ──────────────────────────────────────────
388
-
389
- describe('auth edge cases', () => {
390
- it('should cache auth token across multiple API calls', async () => {
391
- let loginCount = 0;
392
- const mock = vi.fn(async (url: string, init?: RequestInit) => {
393
- if (isHealthCheck(url, init)) return new Response('ok', { status: 200 });
394
- if (url.includes('/auth/login')) {
395
- loginCount++;
396
- return new Response(JSON.stringify({ access_token: 'cached-jwt' }), { status: 200 });
397
- }
398
- return new Response(JSON.stringify([]), { status: 200 });
399
- });
400
- vi.stubGlobal('fetch', mock);
401
-
402
- const client = new CogneeClient();
403
- await client.healthCheck();
404
-
405
- // Multiple API calls should reuse the same token
406
- await client.search('q1');
407
- await client.search('q2');
408
- await client.cognify();
409
-
410
- expect(loginCount).toBe(1);
411
- });
412
- });
413
-
414
- // ─── Position scoring ─────────────────────────────────────────
415
-
416
- describe('position-based scoring', () => {
417
- it('should give first result score 1.0 and last result score 0.05', async () => {
418
- const items = Array.from({ length: 10 }, (_, i) => ({
419
- id: `r${i}`,
420
- text: `Result ${i}`,
421
- }));
422
- mockWithAuth(async () => new Response(JSON.stringify(items), { status: 200 }));
423
- const client = new CogneeClient();
424
- await client.healthCheck();
425
- const results = await client.search('query', { limit: 10 });
426
- expect(results[0].score).toBe(1.0);
427
- expect(results[results.length - 1].score).toBeCloseTo(0.05, 2);
428
- });
429
-
430
- it('should produce monotonically decreasing scores', async () => {
431
- const items = Array.from({ length: 5 }, (_, i) => ({
432
- id: `r${i}`,
433
- text: `Result ${i}`,
434
- }));
435
- mockWithAuth(async () => new Response(JSON.stringify(items), { status: 200 }));
436
- const client = new CogneeClient();
437
- await client.healthCheck();
438
- const results = await client.search('query', { limit: 5 });
439
- for (let i = 1; i < results.length; i++) {
440
- expect(results[i].score).toBeLessThan(results[i - 1].score);
441
- }
442
- });
443
-
444
- it('should give single result score 1.0', async () => {
445
- mockWithAuth(
446
- async () =>
447
- new Response(JSON.stringify([{ id: 'only', text: 'Sole result' }]), { status: 200 }),
448
- );
449
- const client = new CogneeClient();
450
- await client.healthCheck();
451
- const results = await client.search('query');
452
- expect(results[0].score).toBe(1.0);
453
- });
454
-
455
- it('should prefer explicit Cognee scores over position', async () => {
456
- mockWithAuth(
457
- async () =>
458
- new Response(
459
- JSON.stringify([
460
- { id: 'r1', text: 'First', score: 0.3 },
461
- { id: 'r2', text: 'Second', score: 0.9 },
462
- ]),
463
- { status: 200 },
464
- ),
465
- );
466
- const client = new CogneeClient();
467
- await client.healthCheck();
468
- const results = await client.search('query');
469
- // Explicit scores should be used, not position
470
- expect(results[0].score).toBe(0.3);
471
- expect(results[1].score).toBe(0.9);
472
- });
473
- });
474
- });