@ekodb/ekodb-client 0.7.1 → 0.8.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.
@@ -0,0 +1,961 @@
1
+ /**
2
+ * Unit tests for ekoDB TypeScript client
3
+ *
4
+ * These tests use vitest and mock fetch to test client methods
5
+ * without requiring a running ekoDB server.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
9
+ import { EkoDBClient, SerializationFormat } from "./client";
10
+
11
+ // Mock fetch globally
12
+ const mockFetch = vi.fn();
13
+ global.fetch = mockFetch;
14
+
15
+ // ============================================================================
16
+ // Test Helpers
17
+ // ============================================================================
18
+
19
+ function createTestClient(): EkoDBClient {
20
+ return new EkoDBClient({
21
+ baseURL: "http://localhost:8080",
22
+ apiKey: "test-api-key",
23
+ format: SerializationFormat.Json,
24
+ shouldRetry: false,
25
+ });
26
+ }
27
+
28
+ function mockTokenResponse(): void {
29
+ mockFetch.mockResolvedValueOnce({
30
+ ok: true,
31
+ status: 200,
32
+ json: async () => ({ token: "test-jwt-token" }),
33
+ headers: new Headers(),
34
+ });
35
+ }
36
+
37
+ function mockJsonResponse(data: unknown, status = 200): void {
38
+ mockFetch.mockResolvedValueOnce({
39
+ ok: status >= 200 && status < 300,
40
+ status,
41
+ json: async () => data,
42
+ text: async () => JSON.stringify(data),
43
+ headers: new Headers({
44
+ "content-type": "application/json",
45
+ }),
46
+ });
47
+ }
48
+
49
+ function mockErrorResponse(status: number, message: string): void {
50
+ mockFetch.mockResolvedValueOnce({
51
+ ok: false,
52
+ status,
53
+ json: async () => ({ error: message }),
54
+ text: async () => message,
55
+ headers: new Headers(),
56
+ });
57
+ }
58
+
59
+ // ============================================================================
60
+ // Setup/Teardown
61
+ // ============================================================================
62
+
63
+ beforeEach(() => {
64
+ mockFetch.mockReset();
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.clearAllMocks();
69
+ });
70
+
71
+ // ============================================================================
72
+ // Client Configuration Tests
73
+ // ============================================================================
74
+
75
+ describe("EkoDBClient configuration", () => {
76
+ it("creates client with required config", () => {
77
+ const client = new EkoDBClient({
78
+ baseURL: "http://localhost:8080",
79
+ apiKey: "test-key",
80
+ });
81
+ expect(client).toBeInstanceOf(EkoDBClient);
82
+ });
83
+
84
+ it("creates client with all options", () => {
85
+ const client = new EkoDBClient({
86
+ baseURL: "http://localhost:8080",
87
+ apiKey: "test-key",
88
+ timeout: 60000,
89
+ maxRetries: 5,
90
+ shouldRetry: true,
91
+ format: SerializationFormat.Json,
92
+ });
93
+ expect(client).toBeInstanceOf(EkoDBClient);
94
+ });
95
+ });
96
+
97
+ // ============================================================================
98
+ // Health Check Tests
99
+ // ============================================================================
100
+
101
+ // Note: Health check tests require specific response format matching
102
+ // Covered by integration tests
103
+
104
+ // ============================================================================
105
+ // Insert Tests
106
+ // ============================================================================
107
+
108
+ describe("EkoDBClient insert", () => {
109
+ it("inserts record successfully", async () => {
110
+ const client = createTestClient();
111
+
112
+ mockTokenResponse();
113
+ mockJsonResponse({ id: "record_123", name: "Test" });
114
+
115
+ const result = await client.insert("users", { name: "Test" });
116
+
117
+ expect(result).toHaveProperty("id", "record_123");
118
+ expect(mockFetch).toHaveBeenCalledTimes(2);
119
+ });
120
+
121
+ it("inserts record with TTL", async () => {
122
+ const client = createTestClient();
123
+
124
+ mockTokenResponse();
125
+ mockJsonResponse({ id: "record_123", name: "Test", ttl: "30m" });
126
+
127
+ const result = await client.insert("users", { name: "Test", ttl: "30m" });
128
+
129
+ expect(result).toHaveProperty("id");
130
+ });
131
+ });
132
+
133
+ // ============================================================================
134
+ // Find Tests
135
+ // ============================================================================
136
+
137
+ describe("EkoDBClient find", () => {
138
+ it("finds records with query", async () => {
139
+ const client = createTestClient();
140
+
141
+ mockTokenResponse();
142
+ mockJsonResponse([
143
+ { id: "user_1", name: "Alice" },
144
+ { id: "user_2", name: "Bob" },
145
+ ]);
146
+
147
+ const result = await client.find("users", { limit: 10 });
148
+
149
+ expect(result).toHaveLength(2);
150
+ expect(result[0]).toHaveProperty("name", "Alice");
151
+ });
152
+
153
+ it("finds record by ID", async () => {
154
+ const client = createTestClient();
155
+
156
+ mockTokenResponse();
157
+ mockJsonResponse({ id: "user_123", name: "Alice" });
158
+
159
+ const result = await client.findById("users", "user_123");
160
+
161
+ expect(result).toHaveProperty("id", "user_123");
162
+ });
163
+
164
+ it("throws error for not found record", async () => {
165
+ const client = createTestClient();
166
+
167
+ mockTokenResponse();
168
+ mockErrorResponse(404, "Not found");
169
+
170
+ await expect(client.findById("users", "nonexistent")).rejects.toThrow();
171
+ });
172
+ });
173
+
174
+ // ============================================================================
175
+ // Update Tests
176
+ // ============================================================================
177
+
178
+ describe("EkoDBClient update", () => {
179
+ it("updates record successfully", async () => {
180
+ const client = createTestClient();
181
+
182
+ mockTokenResponse();
183
+ mockJsonResponse({ id: "user_123", name: "Updated" });
184
+
185
+ const result = await client.update("users", "user_123", {
186
+ name: "Updated",
187
+ });
188
+
189
+ expect(result).toHaveProperty("name", "Updated");
190
+ });
191
+ });
192
+
193
+ // ============================================================================
194
+ // Delete Tests
195
+ // ============================================================================
196
+
197
+ describe("EkoDBClient delete", () => {
198
+ it("deletes record successfully", async () => {
199
+ const client = createTestClient();
200
+
201
+ mockTokenResponse();
202
+ mockJsonResponse({ id: "user_123", deleted: true });
203
+
204
+ await expect(client.delete("users", "user_123")).resolves.not.toThrow();
205
+ });
206
+ });
207
+
208
+ // ============================================================================
209
+ // Batch Operations Tests
210
+ // ============================================================================
211
+
212
+ describe("EkoDBClient batch operations", () => {
213
+ it("batch inserts records", async () => {
214
+ const client = createTestClient();
215
+
216
+ mockTokenResponse();
217
+ mockJsonResponse({
218
+ successful: ["id_1", "id_2", "id_3"],
219
+ failed: [],
220
+ });
221
+
222
+ const records = [{ name: "A" }, { name: "B" }, { name: "C" }];
223
+ const result = await client.batchInsert("users", records);
224
+
225
+ expect(result.successful).toHaveLength(3);
226
+ expect(result.failed).toHaveLength(0);
227
+ });
228
+
229
+ it("batch deletes records", async () => {
230
+ const client = createTestClient();
231
+
232
+ mockTokenResponse();
233
+ mockJsonResponse({
234
+ successful: ["id_1", "id_2"],
235
+ failed: [],
236
+ });
237
+
238
+ const result = await client.batchDelete("users", ["id_1", "id_2"]);
239
+
240
+ expect(result.successful).toHaveLength(2);
241
+ });
242
+ });
243
+
244
+ // ============================================================================
245
+ // KV Store Tests
246
+ // ============================================================================
247
+
248
+ describe("EkoDBClient KV store", () => {
249
+ it("sets KV value", async () => {
250
+ const client = createTestClient();
251
+
252
+ mockTokenResponse();
253
+ mockJsonResponse({ success: true });
254
+
255
+ await expect(
256
+ client.kvSet("my_key", { data: "value" }),
257
+ ).resolves.not.toThrow();
258
+ });
259
+
260
+ it("gets KV value", async () => {
261
+ const client = createTestClient();
262
+
263
+ mockTokenResponse();
264
+ mockJsonResponse({ value: { data: "stored_value" } });
265
+
266
+ const result = await client.kvGet("my_key");
267
+
268
+ expect(result).toEqual({ data: "stored_value" });
269
+ });
270
+
271
+ it("deletes KV value", async () => {
272
+ const client = createTestClient();
273
+
274
+ mockTokenResponse();
275
+ mockJsonResponse({ deleted: true });
276
+
277
+ await expect(client.kvDelete("my_key")).resolves.not.toThrow();
278
+ });
279
+
280
+ it("checks KV exists", async () => {
281
+ const client = createTestClient();
282
+
283
+ mockTokenResponse();
284
+ mockJsonResponse({ value: "something" });
285
+
286
+ const result = await client.kvExists("my_key");
287
+
288
+ expect(result).toBe(true);
289
+ });
290
+ });
291
+
292
+ // ============================================================================
293
+ // Transaction Tests
294
+ // ============================================================================
295
+
296
+ describe("EkoDBClient transactions", () => {
297
+ it("begins transaction", async () => {
298
+ const client = createTestClient();
299
+
300
+ mockTokenResponse();
301
+ mockJsonResponse({ transaction_id: "tx_123456" });
302
+
303
+ const txId = await client.beginTransaction();
304
+
305
+ expect(txId).toBe("tx_123456");
306
+ });
307
+
308
+ it("commits transaction", async () => {
309
+ const client = createTestClient();
310
+
311
+ mockTokenResponse();
312
+ mockJsonResponse({ status: "committed" });
313
+
314
+ await expect(client.commitTransaction("tx_123")).resolves.not.toThrow();
315
+ });
316
+
317
+ it("rolls back transaction", async () => {
318
+ const client = createTestClient();
319
+
320
+ mockTokenResponse();
321
+ mockJsonResponse({ status: "rolled_back" });
322
+
323
+ await expect(client.rollbackTransaction("tx_123")).resolves.not.toThrow();
324
+ });
325
+ });
326
+
327
+ // ============================================================================
328
+ // Collection Management Tests
329
+ // ============================================================================
330
+
331
+ describe("EkoDBClient collections", () => {
332
+ it("lists collections", async () => {
333
+ const client = createTestClient();
334
+
335
+ mockTokenResponse();
336
+ mockJsonResponse({ collections: ["users", "posts", "comments"] });
337
+
338
+ const result = await client.listCollections();
339
+
340
+ expect(result).toContain("users");
341
+ expect(result).toHaveLength(3);
342
+ });
343
+
344
+ it("deletes collection", async () => {
345
+ const client = createTestClient();
346
+
347
+ mockTokenResponse();
348
+ mockJsonResponse({ status: "deleted" });
349
+
350
+ await expect(
351
+ client.deleteCollection("test_collection"),
352
+ ).resolves.not.toThrow();
353
+ });
354
+ });
355
+
356
+ // ============================================================================
357
+ // Restore Operations Tests
358
+ // ============================================================================
359
+
360
+ describe("EkoDBClient restore operations", () => {
361
+ it("restores deleted record", async () => {
362
+ const client = createTestClient();
363
+
364
+ mockTokenResponse();
365
+ mockJsonResponse({ status: "restored" });
366
+
367
+ await expect(
368
+ client.restoreRecord("users", "record_123"),
369
+ ).resolves.not.toThrow();
370
+ });
371
+
372
+ // Note: restoreCollection return type may vary - covered by integration tests
373
+ });
374
+
375
+ // ============================================================================
376
+ // Search Tests
377
+ // ============================================================================
378
+
379
+ describe("EkoDBClient search", () => {
380
+ it("performs search", async () => {
381
+ const client = createTestClient();
382
+
383
+ mockTokenResponse();
384
+ mockJsonResponse({
385
+ results: [
386
+ { id: "doc_1", score: 0.95 },
387
+ { id: "doc_2", score: 0.85 },
388
+ ],
389
+ total: 2,
390
+ });
391
+
392
+ const result = await client.search("documents", { query: "test" });
393
+
394
+ expect(result.results).toHaveLength(2);
395
+ expect(result.total).toBe(2);
396
+ });
397
+
398
+ // Note: textSearch and hybridSearch require specific mock setup - covered by integration tests
399
+ });
400
+
401
+ // ============================================================================
402
+ // Functions/Scripts Tests
403
+ // ============================================================================
404
+
405
+ describe("EkoDBClient functions", () => {
406
+ it("calls script", async () => {
407
+ const client = createTestClient();
408
+
409
+ mockTokenResponse();
410
+ mockJsonResponse({
411
+ results: [{ id: "user_1", name: "Alice" }],
412
+ });
413
+
414
+ const result = await client.callScript("my_function", { limit: 10 });
415
+
416
+ expect(result).toHaveProperty("results");
417
+ });
418
+
419
+ it("lists scripts", async () => {
420
+ const client = createTestClient();
421
+
422
+ mockTokenResponse();
423
+ mockJsonResponse([
424
+ { id: "func_1", label: "function_1" },
425
+ { id: "func_2", label: "function_2" },
426
+ ]);
427
+
428
+ const result = await client.listScripts();
429
+
430
+ expect(result).toHaveLength(2);
431
+ });
432
+
433
+ it("deletes script", async () => {
434
+ const client = createTestClient();
435
+
436
+ mockTokenResponse();
437
+ mockJsonResponse({ status: "deleted" });
438
+
439
+ await expect(client.deleteScript("func_123")).resolves.not.toThrow();
440
+ });
441
+ });
442
+
443
+ // ============================================================================
444
+ // Chat Tests
445
+ // ============================================================================
446
+
447
+ describe("EkoDBClient chat", () => {
448
+ it("creates chat session", async () => {
449
+ const client = createTestClient();
450
+
451
+ mockTokenResponse();
452
+ mockJsonResponse({
453
+ chat_id: "chat_123",
454
+ message_id: "msg_001",
455
+ responses: ["Hello!"],
456
+ });
457
+
458
+ const result = await client.createChatSession({
459
+ collections: [{ collection_name: "documents" }],
460
+ llm_provider: "openai",
461
+ llm_model: "gpt-4",
462
+ });
463
+
464
+ expect(result).toHaveProperty("chat_id", "chat_123");
465
+ });
466
+
467
+ it("sends chat message", async () => {
468
+ const client = createTestClient();
469
+
470
+ mockTokenResponse();
471
+ mockJsonResponse({
472
+ chat_id: "chat_123",
473
+ message_id: "msg_002",
474
+ responses: ["Here is my response."],
475
+ });
476
+
477
+ const request = { message: "What is the answer?" };
478
+ const result = await client.chatMessage("chat_123", request);
479
+
480
+ expect(result).toHaveProperty("message_id");
481
+ });
482
+
483
+ it("lists chat sessions", async () => {
484
+ const client = createTestClient();
485
+
486
+ mockTokenResponse();
487
+ mockJsonResponse({
488
+ sessions: [
489
+ { id: "chat_1", created_at: "2024-01-01T00:00:00Z" },
490
+ { id: "chat_2", created_at: "2024-01-02T00:00:00Z" },
491
+ ],
492
+ total: 2,
493
+ });
494
+
495
+ const result = await client.listChatSessions();
496
+
497
+ expect(result.sessions).toHaveLength(2);
498
+ });
499
+
500
+ it("gets chat session", async () => {
501
+ const client = createTestClient();
502
+
503
+ mockTokenResponse();
504
+ mockJsonResponse({
505
+ id: "chat_123",
506
+ created_at: "2024-01-01T00:00:00Z",
507
+ messages: [],
508
+ });
509
+
510
+ const result = await client.getChatSession("chat_123");
511
+
512
+ expect(result).toHaveProperty("id", "chat_123");
513
+ });
514
+
515
+ it("deletes chat session", async () => {
516
+ const client = createTestClient();
517
+
518
+ mockTokenResponse();
519
+ mockJsonResponse({ status: "deleted" });
520
+
521
+ await expect(client.deleteChatSession("chat_123")).resolves.not.toThrow();
522
+ });
523
+ });
524
+
525
+ // ============================================================================
526
+ // Error Handling Tests
527
+ // ============================================================================
528
+
529
+ describe("EkoDBClient error handling", () => {
530
+ it("handles server error", async () => {
531
+ const client = createTestClient();
532
+
533
+ mockTokenResponse();
534
+ mockErrorResponse(500, "Internal Server Error");
535
+
536
+ await expect(client.insert("users", { name: "Test" })).rejects.toThrow();
537
+ });
538
+
539
+ it("handles rate limit error", async () => {
540
+ const client = createTestClient();
541
+
542
+ mockTokenResponse();
543
+ mockFetch.mockResolvedValueOnce({
544
+ ok: false,
545
+ status: 429,
546
+ json: async () => ({ error: "Rate limit exceeded" }),
547
+ text: async () => "Rate limit exceeded",
548
+ headers: new Headers({
549
+ "Retry-After": "60",
550
+ }),
551
+ });
552
+
553
+ await expect(client.insert("users", { name: "Test" })).rejects.toThrow();
554
+ });
555
+
556
+ it("handles authentication error", async () => {
557
+ mockFetch.mockResolvedValueOnce({
558
+ ok: false,
559
+ status: 401,
560
+ json: async () => ({ error: "Invalid API key" }),
561
+ text: async () => "Invalid API key",
562
+ headers: new Headers(),
563
+ });
564
+
565
+ const client = createTestClient();
566
+
567
+ await expect(client.insert("users", { name: "Test" })).rejects.toThrow();
568
+ });
569
+ });
570
+
571
+ // Note: Rate limit info tests require actual HTTP response headers
572
+ // which are handled differently in the real client vs mocks
573
+ // Covered by integration tests
574
+
575
+ // ============================================================================
576
+ // Additional Missing Method Tests - Added Jan 4, 2026
577
+ // ============================================================================
578
+
579
+ // Note: findAll/findAllWithLimit use WebSocket, not HTTP - covered by integration tests
580
+
581
+ describe("EkoDBClient batchUpdate", () => {
582
+ it("batch updates records", async () => {
583
+ const client = createTestClient();
584
+
585
+ mockTokenResponse();
586
+ mockJsonResponse({
587
+ successful: ["id_1", "id_2"],
588
+ failed: [],
589
+ });
590
+
591
+ const updates = [
592
+ { id: "id_1", data: { name: "Updated 1" } },
593
+ { id: "id_2", data: { name: "Updated 2" } },
594
+ ];
595
+ const result = await client.batchUpdate("users", updates);
596
+
597
+ expect(result.successful).toHaveLength(2);
598
+ });
599
+ });
600
+
601
+ describe("EkoDBClient collection management", () => {
602
+ it("creates collection", async () => {
603
+ const client = createTestClient();
604
+
605
+ mockTokenResponse();
606
+ mockJsonResponse({ status: "created" });
607
+
608
+ await expect(
609
+ client.createCollection("new_collection", { fields: {} }),
610
+ ).resolves.not.toThrow();
611
+ });
612
+
613
+ it("gets collection metadata", async () => {
614
+ const client = createTestClient();
615
+
616
+ mockTokenResponse();
617
+ mockJsonResponse({
618
+ collection: { fields: { name: { field_type: "String" } } },
619
+ });
620
+
621
+ const result = await client.getCollection("users");
622
+
623
+ expect(result).toBeDefined();
624
+ });
625
+
626
+ // Note: getSchema requires specific response format - covered by integration tests
627
+ });
628
+
629
+ describe("EkoDBClient KV advanced", () => {
630
+ it("queries KV by pattern", async () => {
631
+ const client = createTestClient();
632
+
633
+ mockTokenResponse();
634
+ mockJsonResponse([
635
+ { key: "config:app", value: "value1" },
636
+ { key: "config:db", value: "value2" },
637
+ ]);
638
+
639
+ const result = await client.kvFind({ pattern: "config:*" });
640
+
641
+ expect(result).toHaveLength(2);
642
+ });
643
+ });
644
+
645
+ describe("EkoDBClient scripts advanced", () => {
646
+ it("saves script", async () => {
647
+ const client = createTestClient();
648
+
649
+ mockTokenResponse();
650
+ mockJsonResponse({ id: "func_123", label: "my_function" });
651
+
652
+ const script = {
653
+ label: "my_function",
654
+ name: "my_function",
655
+ parameters: {},
656
+ functions: [],
657
+ };
658
+ const result = await client.saveScript(script);
659
+
660
+ expect(result).toBeDefined();
661
+ });
662
+
663
+ it("gets script by ID", async () => {
664
+ const client = createTestClient();
665
+
666
+ mockTokenResponse();
667
+ mockJsonResponse({ id: "func_123", label: "my_function" });
668
+
669
+ const result = await client.getScript("func_123");
670
+
671
+ expect(result).toBeDefined();
672
+ });
673
+
674
+ it("updates script", async () => {
675
+ const client = createTestClient();
676
+
677
+ mockTokenResponse();
678
+ mockJsonResponse({ id: "func_123", label: "updated_function" });
679
+
680
+ const script = {
681
+ label: "updated_function",
682
+ name: "updated_function",
683
+ parameters: {},
684
+ functions: [],
685
+ };
686
+ await expect(
687
+ client.updateScript("func_123", script),
688
+ ).resolves.not.toThrow();
689
+ });
690
+ });
691
+
692
+ describe("EkoDBClient chat advanced", () => {
693
+ it("gets chat session messages", async () => {
694
+ const client = createTestClient();
695
+
696
+ mockTokenResponse();
697
+ mockJsonResponse({
698
+ messages: [
699
+ { id: "msg_1", role: "user", content: "Hello" },
700
+ { id: "msg_2", role: "assistant", content: "Hi" },
701
+ ],
702
+ });
703
+
704
+ const result = await client.getChatSessionMessages("chat_123");
705
+
706
+ expect(result).toBeDefined();
707
+ });
708
+
709
+ it("updates chat session", async () => {
710
+ const client = createTestClient();
711
+
712
+ mockTokenResponse();
713
+ mockJsonResponse({ id: "chat_123", updated: true });
714
+
715
+ await expect(
716
+ client.updateChatSession("chat_123", { system_prompt: "New prompt" }),
717
+ ).resolves.not.toThrow();
718
+ });
719
+
720
+ it("branches chat session", async () => {
721
+ const client = createTestClient();
722
+
723
+ mockTokenResponse();
724
+ mockJsonResponse({ chat_id: "chat_456", branched_from: "chat_123" });
725
+
726
+ const result = await client.branchChatSession({
727
+ collections: [{ collection_name: "documents" }],
728
+ llm_provider: "openai",
729
+ });
730
+
731
+ expect(result).toBeDefined();
732
+ });
733
+
734
+ it("merges chat sessions", async () => {
735
+ const client = createTestClient();
736
+
737
+ mockTokenResponse();
738
+ mockJsonResponse({ chat_id: "chat_123", merged: true });
739
+
740
+ const result = await client.mergeChatSessions({
741
+ source_chat_ids: ["chat_456"],
742
+ target_chat_id: "chat_123",
743
+ merge_strategy: "Interleaved" as any,
744
+ });
745
+
746
+ expect(result).toBeDefined();
747
+ });
748
+
749
+ it("deletes chat message", async () => {
750
+ const client = createTestClient();
751
+
752
+ mockTokenResponse();
753
+ mockJsonResponse({ deleted: true });
754
+
755
+ await expect(
756
+ client.deleteChatMessage("chat_123", "msg_001"),
757
+ ).resolves.not.toThrow();
758
+ });
759
+
760
+ it("updates chat message", async () => {
761
+ const client = createTestClient();
762
+
763
+ mockTokenResponse();
764
+ mockJsonResponse({ message_id: "msg_001", updated: true });
765
+
766
+ await expect(
767
+ client.updateChatMessage("chat_123", "msg_001", "Updated content"),
768
+ ).resolves.not.toThrow();
769
+ });
770
+
771
+ it("regenerates chat message", async () => {
772
+ const client = createTestClient();
773
+
774
+ mockTokenResponse();
775
+ mockJsonResponse({ message_id: "msg_002", content: "Regenerated" });
776
+
777
+ const result = await client.regenerateMessage("chat_123", "msg_001");
778
+
779
+ expect(result).toBeDefined();
780
+ });
781
+
782
+ it("toggles forgotten message", async () => {
783
+ const client = createTestClient();
784
+
785
+ mockTokenResponse();
786
+ mockJsonResponse({ message_id: "msg_001", forgotten: true });
787
+
788
+ await expect(
789
+ client.toggleForgottenMessage("chat_123", "msg_001", true),
790
+ ).resolves.not.toThrow();
791
+ });
792
+ });
793
+
794
+ describe("EkoDBClient transaction status", () => {
795
+ it("gets transaction status", async () => {
796
+ const client = createTestClient();
797
+
798
+ mockTokenResponse();
799
+ mockJsonResponse({ transaction_id: "tx_123", status: "active" });
800
+
801
+ const result = await client.getTransactionStatus("tx_123");
802
+
803
+ expect(result).toBeDefined();
804
+ });
805
+ });
806
+
807
+ // ============================================================================
808
+ // Convenience Methods Tests
809
+ // ============================================================================
810
+
811
+ describe("Convenience methods", () => {
812
+ describe("upsert", () => {
813
+ it("inserts when record not found", async () => {
814
+ mockTokenResponse();
815
+ // Mock update returning 404
816
+ mockErrorResponse(404, "Not found");
817
+ // Mock insert succeeding
818
+ mockJsonResponse({ id: "user123", name: "John Doe" });
819
+
820
+ const client = createTestClient();
821
+ await client.init();
822
+
823
+ const result = await client.upsert("users", "user123", {
824
+ name: "John Doe",
825
+ });
826
+ expect(result).toEqual({ id: "user123", name: "John Doe" });
827
+ });
828
+
829
+ it("updates when record exists", async () => {
830
+ mockTokenResponse();
831
+ // Mock update succeeding
832
+ mockJsonResponse({ id: "user123", name: "John Doe Updated" });
833
+
834
+ const client = createTestClient();
835
+ await client.init();
836
+
837
+ const result = await client.upsert("users", "user123", {
838
+ name: "John Doe Updated",
839
+ });
840
+ expect(result).toEqual({ id: "user123", name: "John Doe Updated" });
841
+ });
842
+
843
+ it("throws on non-404 errors", async () => {
844
+ mockTokenResponse();
845
+ // Mock update with server error
846
+ mockErrorResponse(500, "Internal server error");
847
+
848
+ const client = createTestClient();
849
+ await client.init();
850
+
851
+ await expect(
852
+ client.upsert("users", "user123", { name: "John Doe" }),
853
+ ).rejects.toThrow();
854
+ });
855
+ });
856
+
857
+ describe("findOne", () => {
858
+ it("returns record when found", async () => {
859
+ mockTokenResponse();
860
+ mockJsonResponse([
861
+ { id: "user123", email: "test@example.com", name: "John" },
862
+ ]);
863
+
864
+ const client = createTestClient();
865
+ await client.init();
866
+
867
+ const result = await client.findOne("users", "email", "test@example.com");
868
+ expect(result).toEqual({
869
+ id: "user123",
870
+ email: "test@example.com",
871
+ name: "John",
872
+ });
873
+ });
874
+
875
+ it("returns null when not found", async () => {
876
+ mockTokenResponse();
877
+ mockJsonResponse([]);
878
+
879
+ const client = createTestClient();
880
+ await client.init();
881
+
882
+ const result = await client.findOne(
883
+ "users",
884
+ "email",
885
+ "notfound@example.com",
886
+ );
887
+ expect(result).toBeNull();
888
+ });
889
+ });
890
+
891
+ describe("exists", () => {
892
+ it("returns true when record exists", async () => {
893
+ mockTokenResponse();
894
+ mockJsonResponse({ id: "user123", name: "John" });
895
+
896
+ const client = createTestClient();
897
+ await client.init();
898
+
899
+ const result = await client.exists("users", "user123");
900
+ expect(result).toBe(true);
901
+ });
902
+
903
+ it("returns false when record not found", async () => {
904
+ mockTokenResponse();
905
+ mockErrorResponse(404, "Not found");
906
+
907
+ const client = createTestClient();
908
+ await client.init();
909
+
910
+ const result = await client.exists("users", "user123");
911
+ expect(result).toBe(false);
912
+ });
913
+
914
+ it("throws on non-404 errors", async () => {
915
+ mockTokenResponse();
916
+ mockErrorResponse(500, "Internal server error");
917
+
918
+ const client = createTestClient();
919
+ await client.init();
920
+
921
+ await expect(client.exists("users", "user123")).rejects.toThrow();
922
+ });
923
+ });
924
+
925
+ describe("paginate", () => {
926
+ it("calculates skip correctly for page 2", async () => {
927
+ mockTokenResponse();
928
+ mockJsonResponse([{ id: "user11", name: "User 11" }]);
929
+
930
+ const client = createTestClient();
931
+ await client.init();
932
+
933
+ const result = await client.paginate("users", 2, 10);
934
+ expect(result).toHaveLength(1);
935
+ expect(result[0]).toEqual({ id: "user11", name: "User 11" });
936
+ });
937
+
938
+ it("skips zero records for page 1", async () => {
939
+ mockTokenResponse();
940
+ mockJsonResponse([{ id: "user1", name: "User 1" }]);
941
+
942
+ const client = createTestClient();
943
+ await client.init();
944
+
945
+ const result = await client.paginate("users", 1, 10);
946
+ expect(result).toHaveLength(1);
947
+ expect(result[0]).toEqual({ id: "user1", name: "User 1" });
948
+ });
949
+
950
+ it("returns empty array when no records", async () => {
951
+ mockTokenResponse();
952
+ mockJsonResponse([]);
953
+
954
+ const client = createTestClient();
955
+ await client.init();
956
+
957
+ const result = await client.paginate("users", 5, 10);
958
+ expect(result).toHaveLength(0);
959
+ });
960
+ });
961
+ });