@qwickapps/qwickbrain-proxy 1.0.2 → 1.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 (73) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/db/schema.d.ts +63 -6
  3. package/dist/db/schema.d.ts.map +1 -1
  4. package/dist/db/schema.js +17 -2
  5. package/dist/db/schema.js.map +1 -1
  6. package/dist/lib/__tests__/cache-manager.test.js +146 -83
  7. package/dist/lib/__tests__/cache-manager.test.js.map +1 -1
  8. package/dist/lib/__tests__/connection-manager.test.js +2 -2
  9. package/dist/lib/__tests__/connection-manager.test.js.map +1 -1
  10. package/dist/lib/__tests__/proxy-server.test.js +16 -44
  11. package/dist/lib/__tests__/proxy-server.test.js.map +1 -1
  12. package/dist/lib/__tests__/qwickbrain-client.test.js +3 -1
  13. package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -1
  14. package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts +2 -0
  15. package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts.map +1 -0
  16. package/dist/lib/__tests__/sse-invalidation-listener.test.js +245 -0
  17. package/dist/lib/__tests__/sse-invalidation-listener.test.js.map +1 -0
  18. package/dist/lib/__tests__/write-queue-manager.test.d.ts +2 -0
  19. package/dist/lib/__tests__/write-queue-manager.test.d.ts.map +1 -0
  20. package/dist/lib/__tests__/write-queue-manager.test.js +291 -0
  21. package/dist/lib/__tests__/write-queue-manager.test.js.map +1 -0
  22. package/dist/lib/cache-manager.d.ts +35 -6
  23. package/dist/lib/cache-manager.d.ts.map +1 -1
  24. package/dist/lib/cache-manager.js +154 -41
  25. package/dist/lib/cache-manager.js.map +1 -1
  26. package/dist/lib/connection-manager.d.ts +7 -0
  27. package/dist/lib/connection-manager.d.ts.map +1 -1
  28. package/dist/lib/connection-manager.js +57 -8
  29. package/dist/lib/connection-manager.js.map +1 -1
  30. package/dist/lib/proxy-server.d.ts +12 -0
  31. package/dist/lib/proxy-server.d.ts.map +1 -1
  32. package/dist/lib/proxy-server.js +184 -87
  33. package/dist/lib/proxy-server.js.map +1 -1
  34. package/dist/lib/qwickbrain-client.d.ts +4 -0
  35. package/dist/lib/qwickbrain-client.d.ts.map +1 -1
  36. package/dist/lib/qwickbrain-client.js +152 -13
  37. package/dist/lib/qwickbrain-client.js.map +1 -1
  38. package/dist/lib/sse-invalidation-listener.d.ts +31 -0
  39. package/dist/lib/sse-invalidation-listener.d.ts.map +1 -0
  40. package/dist/lib/sse-invalidation-listener.js +151 -0
  41. package/dist/lib/sse-invalidation-listener.js.map +1 -0
  42. package/dist/lib/tools.d.ts +21 -0
  43. package/dist/lib/tools.d.ts.map +1 -0
  44. package/dist/lib/tools.js +513 -0
  45. package/dist/lib/tools.js.map +1 -0
  46. package/dist/lib/write-queue-manager.d.ts +88 -0
  47. package/dist/lib/write-queue-manager.d.ts.map +1 -0
  48. package/dist/lib/write-queue-manager.js +191 -0
  49. package/dist/lib/write-queue-manager.js.map +1 -0
  50. package/dist/types/config.d.ts +7 -42
  51. package/dist/types/config.d.ts.map +1 -1
  52. package/dist/types/config.js +1 -6
  53. package/dist/types/config.js.map +1 -1
  54. package/drizzle/0002_lru_cache_migration.sql +94 -0
  55. package/drizzle/meta/_journal.json +7 -0
  56. package/package.json +6 -2
  57. package/scripts/rebuild-sqlite.sh +26 -0
  58. package/src/db/schema.ts +17 -2
  59. package/src/lib/__tests__/cache-manager.test.ts +180 -90
  60. package/src/lib/__tests__/connection-manager.test.ts +2 -2
  61. package/src/lib/__tests__/proxy-server.test.ts +16 -51
  62. package/src/lib/__tests__/qwickbrain-client.test.ts +3 -1
  63. package/src/lib/__tests__/sse-invalidation-listener.test.ts +326 -0
  64. package/src/lib/__tests__/write-queue-manager.test.ts +383 -0
  65. package/src/lib/cache-manager.ts +198 -46
  66. package/src/lib/connection-manager.ts +67 -8
  67. package/src/lib/proxy-server.ts +231 -90
  68. package/src/lib/qwickbrain-client.ts +166 -12
  69. package/src/lib/sse-invalidation-listener.ts +185 -0
  70. package/src/lib/tools.ts +525 -0
  71. package/src/lib/write-queue-manager.ts +271 -0
  72. package/src/types/config.ts +1 -6
  73. package/.github/workflows/publish.yml +0 -92
@@ -6,7 +6,7 @@ import { createDatabase, runMigrations } from '../../db/client.js';
6
6
  import { CacheManager } from '../cache-manager.js';
7
7
  import type { Config } from '../../types/config.js';
8
8
 
9
- describe('CacheManager', () => {
9
+ describe('CacheManager - LRU Two-Tier Storage', () => {
10
10
  let tmpDir: string;
11
11
  let cacheManager: CacheManager;
12
12
  let db: ReturnType<typeof createDatabase>['db'];
@@ -22,12 +22,7 @@ describe('CacheManager', () => {
22
22
 
23
23
  const config: Config['cache'] = {
24
24
  dir: tmpDir,
25
- ttl: {
26
- workflows: 3600,
27
- rules: 3600,
28
- documents: 1800,
29
- memories: 900,
30
- },
25
+ maxCacheSizeBytes: 10 * 1024, // 10KB limit for testing
31
26
  preload: [],
32
27
  };
33
28
 
@@ -49,7 +44,7 @@ describe('CacheManager', () => {
49
44
  expect(cached?.data.content).toBe('content here');
50
45
  expect(cached?.data.doc_type).toBe('workflow');
51
46
  expect(cached?.data.name).toBe('test-workflow');
52
- expect(cached?.isExpired).toBe(false);
47
+ expect(cached?.age).toBeGreaterThanOrEqual(0);
53
48
  });
54
49
 
55
50
  it('should store document with metadata', async () => {
@@ -97,29 +92,39 @@ describe('CacheManager', () => {
97
92
  expect(cached?.data.content).toBe('version 2');
98
93
  });
99
94
 
100
- it('should mark document as expired after TTL', async () => {
101
- // Override config to have very short TTL for testing
102
- const shortTTLConfig: Config['cache'] = {
103
- dir: tmpDir,
104
- ttl: {
105
- workflows: 0, // Expire immediately
106
- rules: 0,
107
- documents: 0,
108
- memories: 0,
109
- },
110
- preload: [],
111
- };
112
-
113
- const shortCacheManager = new CacheManager(db, shortTTLConfig);
114
- await shortCacheManager.setDocument('workflow', 'test', 'content');
115
-
116
- // Wait to ensure timestamp difference
117
- await new Promise(resolve => setTimeout(resolve, 100));
95
+ it('should mark critical document types as critical', async () => {
96
+ // Critical types: workflow, rule, agent, template
97
+ await cacheManager.setDocument('workflow', 'test-workflow', 'content');
98
+ await cacheManager.setDocument('rule', 'test-rule', 'content');
99
+ await cacheManager.setDocument('agent', 'test-agent', 'content');
100
+ await cacheManager.setDocument('template', 'test-template', 'content');
118
101
 
119
- const cached = await shortCacheManager.getDocument('workflow', 'test');
102
+ // Non-critical type
103
+ await cacheManager.setDocument('frd', 'test-frd', 'content');
120
104
 
121
- expect(cached).not.toBeNull();
122
- expect(cached?.isExpired).toBe(true);
105
+ const stats = await cacheManager.getCacheStats();
106
+
107
+ expect(stats.criticalCount).toBe(4);
108
+ expect(stats.dynamicCount).toBeGreaterThan(0); // FRD is dynamic
109
+ });
110
+
111
+ it('should update lastAccessedAt on read', async () => {
112
+ await cacheManager.setDocument('workflow', 'test', 'content');
113
+
114
+ const cached1 = await cacheManager.getDocument('workflow', 'test');
115
+ const cachedAt1 = cached1?.cachedAt.getTime();
116
+
117
+ // Wait 200ms
118
+ await new Promise(resolve => setTimeout(resolve, 200));
119
+
120
+ const cached2 = await cacheManager.getDocument('workflow', 'test');
121
+ const cachedAt2 = cached2?.cachedAt.getTime();
122
+
123
+ // cachedAt should remain the same (shows when first cached)
124
+ expect(cachedAt2).toBe(cachedAt1);
125
+
126
+ // But age should increase over time
127
+ expect(cached2?.age).toBeGreaterThanOrEqual(0);
123
128
  });
124
129
  });
125
130
 
@@ -132,7 +137,6 @@ describe('CacheManager', () => {
132
137
  expect(cached).not.toBeNull();
133
138
  expect(cached?.data.content).toBe('memory content');
134
139
  expect(cached?.data.name).toBe('test-memory');
135
- expect(cached?.isExpired).toBe(false);
136
140
  });
137
141
 
138
142
  it('should store memory with metadata', async () => {
@@ -168,83 +172,169 @@ describe('CacheManager', () => {
168
172
  const cached = await cacheManager.getMemory('non-existent');
169
173
  expect(cached).toBeNull();
170
174
  });
175
+ });
171
176
 
172
- it('should mark memory as expired after TTL', async () => {
173
- const shortTTLConfig: Config['cache'] = {
174
- dir: tmpDir,
175
- ttl: {
176
- workflows: 0,
177
- rules: 0,
178
- documents: 0,
179
- memories: 0,
180
- },
181
- preload: [],
182
- };
183
-
184
- const shortCacheManager = new CacheManager(db, shortTTLConfig);
185
- await shortCacheManager.setMemory('test', 'content');
177
+ describe('LRU Eviction - Two-Tier Storage', () => {
178
+ it('should NOT evict critical documents when storage limit reached', async () => {
179
+ // Create large critical documents (workflows)
180
+ const largeContent = 'x'.repeat(3000); // 3KB each
181
+
182
+ // Add 4 critical documents = 12KB (exceeds 10KB limit)
183
+ await cacheManager.setDocument('workflow', 'critical1', largeContent);
184
+ await cacheManager.setDocument('workflow', 'critical2', largeContent);
185
+ await cacheManager.setDocument('workflow', 'critical3', largeContent);
186
+ await cacheManager.setDocument('workflow', 'critical4', largeContent);
187
+
188
+ // All should still be present (critical tier bypasses limit)
189
+ const doc1 = await cacheManager.getDocument('workflow', 'critical1');
190
+ const doc2 = await cacheManager.getDocument('workflow', 'critical2');
191
+ const doc3 = await cacheManager.getDocument('workflow', 'critical3');
192
+ const doc4 = await cacheManager.getDocument('workflow', 'critical4');
193
+
194
+ expect(doc1).not.toBeNull();
195
+ expect(doc2).not.toBeNull();
196
+ expect(doc3).not.toBeNull();
197
+ expect(doc4).not.toBeNull();
198
+ });
186
199
 
187
- await new Promise(resolve => setTimeout(resolve, 100));
200
+ it('should evict oldest dynamic documents when storage limit reached', async () => {
201
+ const largeContent = 'x'.repeat(3000); // 3KB each
188
202
 
189
- const cached = await shortCacheManager.getMemory('test');
203
+ // Add dynamic documents
204
+ await cacheManager.setDocument('frd', 'dynamic1', largeContent);
205
+ await new Promise(resolve => setTimeout(resolve, 10));
190
206
 
191
- expect(cached).not.toBeNull();
192
- expect(cached?.isExpired).toBe(true);
207
+ await cacheManager.setDocument('frd', 'dynamic2', largeContent);
208
+ await new Promise(resolve => setTimeout(resolve, 10));
209
+
210
+ await cacheManager.setDocument('frd', 'dynamic3', largeContent);
211
+ await new Promise(resolve => setTimeout(resolve, 10));
212
+
213
+ // Now add another that pushes us over limit (9KB current, 3KB new = 12KB > 10KB)
214
+ // Should evict dynamic1 (oldest)
215
+ await cacheManager.setDocument('frd', 'dynamic4', largeContent);
216
+
217
+ // dynamic1 should be evicted
218
+ const doc1 = await cacheManager.getDocument('frd', 'dynamic1');
219
+ expect(doc1).toBeNull();
220
+
221
+ // dynamic2, dynamic3, dynamic4 should remain
222
+ const doc2 = await cacheManager.getDocument('frd', 'dynamic2');
223
+ const doc3 = await cacheManager.getDocument('frd', 'dynamic3');
224
+ const doc4 = await cacheManager.getDocument('frd', 'dynamic4');
225
+
226
+ expect(doc2).not.toBeNull();
227
+ expect(doc3).not.toBeNull();
228
+ expect(doc4).not.toBeNull();
193
229
  });
194
- });
195
230
 
196
- describe('cleanupExpiredEntries', () => {
197
- it('should delete expired documents and memories', async () => {
198
- const shortTTLConfig: Config['cache'] = {
199
- dir: tmpDir,
200
- ttl: {
201
- workflows: 0,
202
- rules: 0,
203
- documents: 0,
204
- memories: 0,
205
- },
206
- preload: [],
207
- };
208
-
209
- const shortCacheManager = new CacheManager(db, shortTTLConfig);
210
-
211
- // Add some items that will immediately expire
212
- await shortCacheManager.setDocument('workflow', 'test1', 'content1');
213
- await shortCacheManager.setDocument('rule', 'test2', 'content2');
214
- await shortCacheManager.setMemory('memory1', 'content3');
215
-
216
- // Wait to ensure they're expired (need sufficient time for clock to advance)
217
- await new Promise(resolve => setTimeout(resolve, 100));
231
+ it('should use LRU ordering based on lastAccessedAt', async () => {
232
+ const largeContent = 'x'.repeat(3000); // 3KB each
218
233
 
219
- // Clean up
220
- const result = await shortCacheManager.cleanupExpiredEntries();
234
+ // Add 3 dynamic documents with delays to ensure distinct timestamps
235
+ await cacheManager.setDocument('frd', 'doc1', largeContent);
236
+ await new Promise(resolve => setTimeout(resolve, 50));
221
237
 
222
- expect(result.documentsDeleted).toBe(2);
223
- expect(result.memoriesDeleted).toBe(1);
238
+ await cacheManager.setDocument('frd', 'doc2', largeContent);
239
+ await new Promise(resolve => setTimeout(resolve, 50));
224
240
 
225
- // Verify they're gone
226
- const doc1 = await shortCacheManager.getDocument('workflow', 'test1');
227
- const mem1 = await shortCacheManager.getMemory('memory1');
241
+ await cacheManager.setDocument('frd', 'doc3', largeContent);
242
+ await new Promise(resolve => setTimeout(resolve, 50));
228
243
 
229
- expect(doc1).toBeNull();
230
- expect(mem1).toBeNull();
244
+ // Access doc2 and doc3 (updates their lastAccessedAt to be newest)
245
+ await cacheManager.getDocument('frd', 'doc2');
246
+ await new Promise(resolve => setTimeout(resolve, 50));
247
+ await cacheManager.getDocument('frd', 'doc3');
248
+ await new Promise(resolve => setTimeout(resolve, 50));
249
+
250
+ // Now doc1 has oldest lastAccessedAt, should be evicted first
251
+ // Add doc4, should evict doc1 (oldest access)
252
+ await cacheManager.setDocument('frd', 'doc4', largeContent);
253
+
254
+ const doc1 = await cacheManager.getDocument('frd', 'doc1');
255
+ const doc2 = await cacheManager.getDocument('frd', 'doc2');
256
+ const doc3 = await cacheManager.getDocument('frd', 'doc3');
257
+ const doc4 = await cacheManager.getDocument('frd', 'doc4');
258
+
259
+ expect(doc1).toBeNull(); // Oldest access, evicted
260
+ expect(doc2).not.toBeNull(); // Accessed recently, kept
261
+ expect(doc3).not.toBeNull(); // Accessed recently, kept
262
+ expect(doc4).not.toBeNull(); // Just added
263
+ });
264
+
265
+ it('should evict memories when document eviction insufficient', async () => {
266
+ const largeContent = 'x'.repeat(3000); // 3KB each
267
+
268
+ // Add one dynamic document
269
+ await cacheManager.setDocument('frd', 'doc1', largeContent);
270
+
271
+ // Add memories
272
+ await cacheManager.setMemory('mem1', largeContent);
273
+ await cacheManager.setMemory('mem2', largeContent);
274
+
275
+ // Current: 9KB (over limit when we add another 3KB)
276
+ // Should evict doc1 and mem1 to make room
277
+ await cacheManager.setDocument('frd', 'doc2', largeContent);
278
+
279
+ const doc1 = await cacheManager.getDocument('frd', 'doc1');
280
+ const mem1 = await cacheManager.getMemory('mem1');
281
+ const mem2 = await cacheManager.getMemory('mem2');
282
+
283
+ // At least one should be evicted (LRU order)
284
+ const evictedCount = [doc1, mem1, mem2].filter(item => item === null).length;
285
+ expect(evictedCount).toBeGreaterThan(0);
231
286
  });
287
+ });
232
288
 
233
- it('should not delete non-expired items', async () => {
289
+ describe('Cache invalidation', () => {
290
+ it('should invalidate specific document', async () => {
234
291
  await cacheManager.setDocument('workflow', 'test', 'content');
235
- await cacheManager.setMemory('memory', 'content');
236
292
 
237
- const result = await cacheManager.cleanupExpiredEntries();
293
+ const cached1 = await cacheManager.getDocument('workflow', 'test');
294
+ expect(cached1).not.toBeNull();
295
+
296
+ await cacheManager.invalidateDocument('workflow', 'test');
297
+
298
+ const cached2 = await cacheManager.getDocument('workflow', 'test');
299
+ expect(cached2).toBeNull();
300
+ });
301
+
302
+ it('should invalidate specific memory', async () => {
303
+ await cacheManager.setMemory('test', 'content');
304
+
305
+ const cached1 = await cacheManager.getMemory('test');
306
+ expect(cached1).not.toBeNull();
307
+
308
+ await cacheManager.invalidateMemory('test');
309
+
310
+ const cached2 = await cacheManager.getMemory('test');
311
+ expect(cached2).toBeNull();
312
+ });
313
+ });
314
+
315
+ describe('Cache statistics', () => {
316
+ it('should return accurate cache statistics', async () => {
317
+ const content = 'x'.repeat(1000); // 1KB each
318
+
319
+ // Add critical documents
320
+ await cacheManager.setDocument('workflow', 'wf1', content);
321
+ await cacheManager.setDocument('rule', 'rule1', content);
322
+
323
+ // Add dynamic documents
324
+ await cacheManager.setDocument('frd', 'frd1', content);
238
325
 
239
- expect(result.documentsDeleted).toBe(0);
240
- expect(result.memoriesDeleted).toBe(0);
326
+ // Add memories
327
+ await cacheManager.setMemory('mem1', content);
241
328
 
242
- // Verify they're still there
243
- const doc = await cacheManager.getDocument('workflow', 'test');
244
- const mem = await cacheManager.getMemory('memory');
329
+ const stats = await cacheManager.getCacheStats();
245
330
 
246
- expect(doc).not.toBeNull();
247
- expect(mem).not.toBeNull();
331
+ expect(stats.criticalCount).toBe(2);
332
+ expect(stats.dynamicCount).toBeGreaterThan(0);
333
+ expect(stats.memoryCount).toBe(1);
334
+ expect(stats.criticalSize).toBeGreaterThan(0);
335
+ expect(stats.dynamicSize).toBeGreaterThan(0);
336
+ expect(stats.totalSize).toBe(stats.criticalSize + stats.dynamicSize);
337
+ expect(stats.totalCount).toBe(stats.criticalCount + stats.dynamicCount);
248
338
  });
249
339
  });
250
340
 
@@ -154,11 +154,11 @@ describe('ConnectionManager', () => {
154
154
  }
155
155
  });
156
156
 
157
- it('should emit maxReconnectAttemptsReached after max attempts', async () => {
157
+ it('should emit dormant after max attempts', async () => {
158
158
  mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
159
159
 
160
160
  const maxAttemptsListener = vi.fn();
161
- connectionManager.on('maxReconnectAttemptsReached', maxAttemptsListener);
161
+ connectionManager.on('dormant', maxAttemptsListener);
162
162
 
163
163
  await connectionManager.start();
164
164
 
@@ -53,12 +53,7 @@ describe('ProxyServer', () => {
53
53
  },
54
54
  cache: {
55
55
  dir: tmpDir,
56
- ttl: {
57
- workflows: 3600,
58
- rules: 3600,
59
- documents: 1800,
60
- memories: 900,
61
- },
56
+ maxCacheSizeBytes: 100 * 1024 * 1024, // 100MB
62
57
  preload: [],
63
58
  },
64
59
  connection: {
@@ -102,63 +97,33 @@ describe('ProxyServer', () => {
102
97
  });
103
98
  });
104
99
 
105
- describe('cache cleanup on startup', () => {
106
- it('should clean up expired cache entries on start', async () => {
107
- // Add some expired entries
108
- const shortTTLConfig = { ...config };
109
- shortTTLConfig.cache.ttl = {
110
- workflows: 0,
111
- rules: 0,
112
- documents: 0,
113
- memories: 0,
114
- };
115
-
116
- const tempServer = new ProxyServer(db, shortTTLConfig);
100
+ describe('cache behavior', () => {
101
+ it('should use LRU cache with no expiration', async () => {
102
+ // With LRU cache, items never expire by time
103
+ await server['cacheManager'].setDocument('workflow', 'test', 'content');
117
104
 
118
- await tempServer['cacheManager'].setDocument('workflow', 'test', 'content');
105
+ // Wait some time
119
106
  await new Promise(resolve => setTimeout(resolve, 100));
120
107
 
121
- // Verify entry exists before cleanup
122
- const beforeCleanup = await tempServer['cacheManager'].getDocument('workflow', 'test');
123
- expect(beforeCleanup).not.toBeNull();
124
-
125
- await tempServer.start();
126
-
127
- // Entry should be removed after cleanup
128
- const afterCleanup = await tempServer['cacheManager'].getDocument('workflow', 'test');
129
- expect(afterCleanup).toBeNull();
130
-
131
- await tempServer.stop();
108
+ // Entry should still be present (no TTL expiration)
109
+ const cached = await server['cacheManager'].getDocument('workflow', 'test');
110
+ expect(cached).not.toBeNull();
111
+ expect(cached?.data.content).toBe('content');
132
112
  });
133
113
  });
134
114
 
135
115
  describe('graceful degradation', () => {
136
- it('should serve stale cache when disconnected', async () => {
137
- // Create server with short TTL to make cache expire
138
- const shortTTLConfig = { ...config };
139
- shortTTLConfig.cache.ttl = {
140
- workflows: 0,
141
- rules: 0,
142
- documents: 0,
143
- memories: 0,
144
- };
145
- const tempServer = new ProxyServer(db, shortTTLConfig);
146
-
147
- // Add item to cache
148
- await tempServer['cacheManager'].setDocument('workflow', 'test', 'cached content');
149
-
150
- // Wait for cache to expire
151
- await new Promise(resolve => setTimeout(resolve, 100));
116
+ it('should serve cached data when disconnected', async () => {
117
+ // LRU cache always serves cached data (no staleness - always fresh)
118
+ await server['cacheManager'].setDocument('workflow', 'test', 'cached content');
152
119
 
153
120
  // Simulate disconnected state
154
- tempServer['connectionManager']['state'] = 'disconnected';
121
+ server['connectionManager']['state'] = 'disconnected';
155
122
 
156
- const result = await tempServer['handleGetDocument']('workflow', 'test');
123
+ const result = await server['handleGetDocument']('workflow', 'test');
157
124
 
158
125
  expect((result.data as any).content).toBe('cached content');
159
- expect(result._metadata.source).toBe('stale_cache');
160
-
161
- await tempServer.stop();
126
+ expect(result._metadata.source).toBe('cache'); // Fresh cache, not stale
162
127
  });
163
128
 
164
129
  it('should return error when no cache available and disconnected', async () => {
@@ -244,7 +244,9 @@ describe('QwickBrainClient', () => {
244
244
  const isHealthy = await client.healthCheck();
245
245
 
246
246
  expect(isHealthy).toBe(true);
247
- expect(global.fetch).toHaveBeenCalledWith('http://api.test.com/health');
247
+ expect(global.fetch).toHaveBeenCalledWith('http://api.test.com/health', expect.objectContaining({
248
+ signal: expect.any(AbortSignal),
249
+ }));
248
250
  });
249
251
 
250
252
  it('should return false on health check failure', async () => {