@ekodb/ekodb-client 0.12.0 → 0.14.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.
@@ -85,7 +85,6 @@ describe("EkoDBClient configuration", () => {
85
85
  const client = new EkoDBClient({
86
86
  baseURL: "http://localhost:8080",
87
87
  apiKey: "test-key",
88
- timeout: 60000,
89
88
  maxRetries: 5,
90
89
  shouldRetry: true,
91
90
  format: SerializationFormat.Json,
@@ -190,6 +189,80 @@ describe("EkoDBClient update", () => {
190
189
  });
191
190
  });
192
191
 
192
+ // ============================================================================
193
+ // Atomic Field Action Tests
194
+ // ============================================================================
195
+
196
+ describe("EkoDBClient updateWithAction", () => {
197
+ it("increments a field", async () => {
198
+ const client = createTestClient();
199
+
200
+ mockTokenResponse();
201
+ mockJsonResponse({ id: "rec_1", views: 42 });
202
+
203
+ const result = await client.updateWithAction(
204
+ "counters",
205
+ "rec_1",
206
+ "increment",
207
+ "views",
208
+ 1,
209
+ );
210
+
211
+ expect(result).toHaveProperty("views", 42);
212
+ });
213
+
214
+ it("pushes to an array field", async () => {
215
+ const client = createTestClient();
216
+
217
+ mockTokenResponse();
218
+ mockJsonResponse({ id: "rec_2", tags: ["rust", "new-tag"] });
219
+
220
+ const result = await client.updateWithAction(
221
+ "lists",
222
+ "rec_2",
223
+ "push",
224
+ "tags",
225
+ "new-tag",
226
+ );
227
+
228
+ expect(result.tags).toContain("new-tag");
229
+ });
230
+
231
+ it("clears a field without value", async () => {
232
+ const client = createTestClient();
233
+
234
+ mockTokenResponse();
235
+ mockJsonResponse({ id: "rec_3", temp: 0 });
236
+
237
+ const result = await client.updateWithAction(
238
+ "data",
239
+ "rec_3",
240
+ "clear",
241
+ "temp",
242
+ );
243
+
244
+ expect(result).toHaveProperty("temp", 0);
245
+ });
246
+ });
247
+
248
+ describe("EkoDBClient updateWithActionSequence", () => {
249
+ it("applies multiple actions atomically", async () => {
250
+ const client = createTestClient();
251
+
252
+ mockTokenResponse();
253
+ mockJsonResponse({ id: "player_1", score: 110, lives: 2 });
254
+
255
+ const result = await client.updateWithActionSequence("game", "player_1", [
256
+ ["increment", "score", 10],
257
+ ["decrement", "lives", 1],
258
+ ["push", "log", "hit"],
259
+ ]);
260
+
261
+ expect(result).toHaveProperty("score", 110);
262
+ expect(result).toHaveProperty("lives", 2);
263
+ });
264
+ });
265
+
193
266
  // ============================================================================
194
267
  // Delete Tests
195
268
  // ============================================================================
@@ -1243,3 +1316,1286 @@ describe("EkoDBClient collection utilities", () => {
1243
1316
  expect(result).toBe(0);
1244
1317
  });
1245
1318
  });
1319
+
1320
+ // ============================================================================
1321
+ // Distinct Values Tests
1322
+ // ============================================================================
1323
+
1324
+ describe("EkoDBClient distinctValues", () => {
1325
+ it("returns distinct values for a field", async () => {
1326
+ const client = createTestClient();
1327
+
1328
+ mockTokenResponse();
1329
+ mockJsonResponse({
1330
+ collection: "products",
1331
+ field: "category",
1332
+ values: ["books", "electronics", "food"],
1333
+ count: 3,
1334
+ });
1335
+
1336
+ const result = await client.distinctValues("products", "category");
1337
+
1338
+ expect(result.collection).toBe("products");
1339
+ expect(result.field).toBe("category");
1340
+ expect(result.count).toBe(3);
1341
+ expect(result.values).toHaveLength(3);
1342
+ expect(result.values).toContain("books");
1343
+ });
1344
+
1345
+ it("sends filter in request body", async () => {
1346
+ const client = createTestClient();
1347
+
1348
+ mockTokenResponse();
1349
+ mockJsonResponse({
1350
+ collection: "orders",
1351
+ field: "status",
1352
+ values: ["active", "pending"],
1353
+ count: 2,
1354
+ });
1355
+
1356
+ const filter = {
1357
+ type: "Condition",
1358
+ content: { field: "region", operator: "Eq", value: "us" },
1359
+ };
1360
+ const result = await client.distinctValues("orders", "status", { filter });
1361
+
1362
+ expect(result.count).toBe(2);
1363
+ expect(result.values).toContain("active");
1364
+ });
1365
+
1366
+ it("returns empty values for collection with no matching records", async () => {
1367
+ const client = createTestClient();
1368
+
1369
+ mockTokenResponse();
1370
+ mockJsonResponse({
1371
+ collection: "empty",
1372
+ field: "tag",
1373
+ values: [],
1374
+ count: 0,
1375
+ });
1376
+
1377
+ const result = await client.distinctValues("empty", "tag");
1378
+
1379
+ expect(result.count).toBe(0);
1380
+ expect(result.values).toHaveLength(0);
1381
+ });
1382
+
1383
+ it("calls correct endpoint", async () => {
1384
+ const client = createTestClient();
1385
+
1386
+ mockTokenResponse();
1387
+ mockJsonResponse({
1388
+ collection: "users",
1389
+ field: "role",
1390
+ values: ["admin", "user"],
1391
+ count: 2,
1392
+ });
1393
+
1394
+ await client.distinctValues("users", "role");
1395
+
1396
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1397
+ const dataCall = calls[1]; // calls[0] is token
1398
+ expect(dataCall[0]).toContain("/api/distinct/users/role");
1399
+ expect(dataCall[1]?.method).toBe("POST");
1400
+ });
1401
+ });
1402
+
1403
+ // ============================================================================
1404
+ // Raw Completion Tests
1405
+ // ============================================================================
1406
+
1407
+ describe("EkoDBClient rawCompletion", () => {
1408
+ it("returns content from LLM response", async () => {
1409
+ const client = createTestClient();
1410
+
1411
+ mockTokenResponse();
1412
+ mockJsonResponse({ content: "The answer is 42." });
1413
+
1414
+ const result = await client.rawCompletion({
1415
+ system_prompt: "You are a helpful assistant.",
1416
+ message: "What is the answer?",
1417
+ });
1418
+
1419
+ expect(result.content).toBe("The answer is 42.");
1420
+ });
1421
+
1422
+ it("sends all fields in request body", async () => {
1423
+ const client = createTestClient();
1424
+
1425
+ mockTokenResponse();
1426
+ mockJsonResponse({ content: "Response text." });
1427
+
1428
+ await client.rawCompletion({
1429
+ system_prompt: "System.",
1430
+ message: "User.",
1431
+ provider: "openai",
1432
+ model: "gpt-4o",
1433
+ max_tokens: 512,
1434
+ });
1435
+
1436
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1437
+ const dataCall = calls[1]; // calls[0] is token
1438
+ const body = JSON.parse(dataCall[1]?.body as string);
1439
+ expect(body.system_prompt).toBe("System.");
1440
+ expect(body.message).toBe("User.");
1441
+ expect(body.provider).toBe("openai");
1442
+ expect(body.model).toBe("gpt-4o");
1443
+ expect(body.max_tokens).toBe(512);
1444
+ });
1445
+
1446
+ it("omits optional fields when not provided", async () => {
1447
+ const client = createTestClient();
1448
+
1449
+ mockTokenResponse();
1450
+ mockJsonResponse({ content: "Response." });
1451
+
1452
+ await client.rawCompletion({
1453
+ system_prompt: "System.",
1454
+ message: "User.",
1455
+ });
1456
+
1457
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1458
+ const dataCall = calls[1];
1459
+ const body = JSON.parse(dataCall[1]?.body as string);
1460
+ expect(body.provider).toBeUndefined();
1461
+ expect(body.model).toBeUndefined();
1462
+ expect(body.max_tokens).toBeUndefined();
1463
+ });
1464
+
1465
+ it("calls correct endpoint with POST method", async () => {
1466
+ const client = createTestClient();
1467
+
1468
+ mockTokenResponse();
1469
+ mockJsonResponse({ content: "Response." });
1470
+
1471
+ await client.rawCompletion({
1472
+ system_prompt: "System.",
1473
+ message: "User.",
1474
+ });
1475
+
1476
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1477
+ const dataCall = calls[1];
1478
+ expect(dataCall[0]).toContain("/api/chat/complete");
1479
+ expect(dataCall[1]?.method).toBe("POST");
1480
+ });
1481
+ });
1482
+
1483
+ // ============================================================================
1484
+ // rawCompletionStream (SSE) Tests
1485
+ // ============================================================================
1486
+
1487
+ describe("EkoDBClient rawCompletionStream", () => {
1488
+ it("parses SSE done event and returns content", async () => {
1489
+ const client = createTestClient();
1490
+
1491
+ mockTokenResponse();
1492
+ // SSE response with token chunks and a done event
1493
+ const sseBody = [
1494
+ 'data: {"token":"Hello"}',
1495
+ 'data: {"token":" world"}',
1496
+ 'data: {"content":"Hello world","done":true}',
1497
+ "",
1498
+ ].join("\n");
1499
+
1500
+ mockFetch.mockResolvedValueOnce({
1501
+ ok: true,
1502
+ status: 200,
1503
+ text: async () => sseBody,
1504
+ headers: new Headers(),
1505
+ });
1506
+
1507
+ const result = await client.rawCompletionStream({
1508
+ system_prompt: "System.",
1509
+ message: "User.",
1510
+ });
1511
+
1512
+ expect(result.content).toBe("Hello world");
1513
+ });
1514
+
1515
+ it("accumulates tokens when no done event", async () => {
1516
+ const client = createTestClient();
1517
+
1518
+ mockTokenResponse();
1519
+ const sseBody = [
1520
+ 'data: {"token":"chunk1"}',
1521
+ 'data: {"token":"chunk2"}',
1522
+ "",
1523
+ ].join("\n");
1524
+
1525
+ mockFetch.mockResolvedValueOnce({
1526
+ ok: true,
1527
+ status: 200,
1528
+ text: async () => sseBody,
1529
+ headers: new Headers(),
1530
+ });
1531
+
1532
+ const result = await client.rawCompletionStream({
1533
+ system_prompt: "System.",
1534
+ message: "User.",
1535
+ });
1536
+
1537
+ expect(result.content).toBe("chunk1chunk2");
1538
+ });
1539
+
1540
+ it("throws on SSE error event", async () => {
1541
+ const client = createTestClient();
1542
+
1543
+ mockTokenResponse();
1544
+ const sseBody = 'data: {"error":"LLM timeout"}\n';
1545
+
1546
+ mockFetch.mockResolvedValueOnce({
1547
+ ok: true,
1548
+ status: 200,
1549
+ text: async () => sseBody,
1550
+ headers: new Headers(),
1551
+ });
1552
+
1553
+ await expect(
1554
+ client.rawCompletionStream({
1555
+ system_prompt: "System.",
1556
+ message: "User.",
1557
+ }),
1558
+ ).rejects.toThrow("LLM timeout");
1559
+ });
1560
+
1561
+ it("throws on non-200 HTTP response", async () => {
1562
+ const client = createTestClient();
1563
+
1564
+ mockTokenResponse();
1565
+ mockFetch.mockResolvedValueOnce({
1566
+ ok: false,
1567
+ status: 401,
1568
+ text: async () => "Unauthorized",
1569
+ headers: new Headers(),
1570
+ });
1571
+
1572
+ await expect(
1573
+ client.rawCompletionStream({
1574
+ system_prompt: "System.",
1575
+ message: "User.",
1576
+ }),
1577
+ ).rejects.toThrow("401");
1578
+ });
1579
+
1580
+ it("calls the /stream endpoint", async () => {
1581
+ const client = createTestClient();
1582
+
1583
+ mockTokenResponse();
1584
+ mockFetch.mockResolvedValueOnce({
1585
+ ok: true,
1586
+ status: 200,
1587
+ text: async () => 'data: {"content":"ok"}\n',
1588
+ headers: new Headers(),
1589
+ });
1590
+
1591
+ await client.rawCompletionStream({
1592
+ system_prompt: "System.",
1593
+ message: "User.",
1594
+ });
1595
+
1596
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1597
+ const dataCall = calls[1];
1598
+ expect(dataCall[0]).toContain("/api/chat/complete/stream");
1599
+ expect(dataCall[1]?.headers?.Accept).toBe("text/event-stream");
1600
+ });
1601
+ });
1602
+
1603
+ // ============================================================================
1604
+ // Token Management Tests
1605
+ // ============================================================================
1606
+
1607
+ describe("refreshToken", () => {
1608
+ it("fetches a new token", async () => {
1609
+ const client = createTestClient();
1610
+
1611
+ // First token fetch (init)
1612
+ mockTokenResponse();
1613
+ mockJsonResponse({ status: "ok" });
1614
+ await client.health();
1615
+
1616
+ // Refresh token
1617
+ mockFetch.mockResolvedValueOnce({
1618
+ ok: true,
1619
+ status: 200,
1620
+ json: async () => ({ token: "new-jwt-token" }),
1621
+ headers: new Headers(),
1622
+ });
1623
+ await client.refreshToken();
1624
+
1625
+ // Verify it called the token endpoint again
1626
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1627
+ const tokenCalls = calls.filter((c: unknown[]) =>
1628
+ (c[0] as string).includes("/api/auth/token"),
1629
+ );
1630
+ expect(tokenCalls.length).toBe(2);
1631
+ });
1632
+ });
1633
+
1634
+ describe("clearTokenCache", () => {
1635
+ it("clears the cached token", async () => {
1636
+ const client = createTestClient();
1637
+
1638
+ // Init with token
1639
+ mockTokenResponse();
1640
+ mockJsonResponse({ status: "ok" });
1641
+ await client.health();
1642
+
1643
+ // Clear cache
1644
+ client.clearTokenCache();
1645
+
1646
+ // Next request should fetch a new token
1647
+ mockTokenResponse();
1648
+ mockJsonResponse({ status: "ok" });
1649
+ await client.health();
1650
+
1651
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1652
+ const tokenCalls = calls.filter((c: unknown[]) =>
1653
+ (c[0] as string).includes("/api/auth/token"),
1654
+ );
1655
+ expect(tokenCalls.length).toBe(2);
1656
+ });
1657
+ });
1658
+
1659
+ // ============================================================================
1660
+ // findByIdWithProjection Tests
1661
+ // ============================================================================
1662
+
1663
+ describe("findByIdWithProjection", () => {
1664
+ it("calls correct endpoint with select_fields", async () => {
1665
+ const client = createTestClient();
1666
+
1667
+ mockTokenResponse();
1668
+ mockJsonResponse({ id: "123", name: "Alice" });
1669
+
1670
+ await client.findByIdWithProjection("users", "123", ["name", "email"]);
1671
+
1672
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1673
+ const dataCall = calls[1];
1674
+ expect(dataCall[0]).toContain("/api/find/users/123");
1675
+ expect(dataCall[0]).toContain("select_fields=name%2Cemail");
1676
+ });
1677
+
1678
+ it("calls correct endpoint with exclude_fields", async () => {
1679
+ const client = createTestClient();
1680
+
1681
+ mockTokenResponse();
1682
+ mockJsonResponse({ id: "123", name: "Alice" });
1683
+
1684
+ await client.findByIdWithProjection("users", "123", undefined, [
1685
+ "password",
1686
+ ]);
1687
+
1688
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1689
+ const dataCall = calls[1];
1690
+ expect(dataCall[0]).toContain("exclude_fields=password");
1691
+ });
1692
+
1693
+ it("calls without params when no projection", async () => {
1694
+ const client = createTestClient();
1695
+
1696
+ mockTokenResponse();
1697
+ mockJsonResponse({ id: "123", name: "Alice" });
1698
+
1699
+ await client.findByIdWithProjection("users", "123");
1700
+
1701
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1702
+ const dataCall = calls[1];
1703
+ expect(dataCall[0]).toBe("http://localhost:8080/api/find/users/123");
1704
+ });
1705
+ });
1706
+
1707
+ // ============================================================================
1708
+ // Goal CRUD Tests
1709
+ // ============================================================================
1710
+
1711
+ describe("EkoDBClient goals", () => {
1712
+ it("creates a goal", async () => {
1713
+ const client = createTestClient();
1714
+ mockTokenResponse();
1715
+ mockJsonResponse({ id: "goal_1", title: "Test Goal", status: "active" });
1716
+
1717
+ const result = await client.goalCreate({ title: "Test Goal" });
1718
+ expect(result).toHaveProperty("id", "goal_1");
1719
+ expect(result).toHaveProperty("status", "active");
1720
+ });
1721
+
1722
+ it("lists goals", async () => {
1723
+ const client = createTestClient();
1724
+ mockTokenResponse();
1725
+ mockJsonResponse({ goals: [{ id: "goal_1" }, { id: "goal_2" }] });
1726
+
1727
+ const result = await client.goalList();
1728
+ expect(result).toHaveProperty("goals");
1729
+ });
1730
+
1731
+ it("gets a goal by ID", async () => {
1732
+ const client = createTestClient();
1733
+ mockTokenResponse();
1734
+ mockJsonResponse({ id: "goal_1", title: "Test Goal" });
1735
+
1736
+ const result = await client.goalGet("goal_1");
1737
+ expect(result).toHaveProperty("id", "goal_1");
1738
+ });
1739
+
1740
+ it("updates a goal", async () => {
1741
+ const client = createTestClient();
1742
+ mockTokenResponse();
1743
+ mockJsonResponse({ id: "goal_1", title: "Updated" });
1744
+
1745
+ const result = await client.goalUpdate("goal_1", { title: "Updated" });
1746
+ expect(result).toHaveProperty("title", "Updated");
1747
+ });
1748
+
1749
+ it("deletes a goal", async () => {
1750
+ const client = createTestClient();
1751
+ mockTokenResponse();
1752
+ mockJsonResponse({});
1753
+
1754
+ await expect(client.goalDelete("goal_1")).resolves.not.toThrow();
1755
+ });
1756
+
1757
+ it("searches goals", async () => {
1758
+ const client = createTestClient();
1759
+ mockTokenResponse();
1760
+ mockJsonResponse({ goals: [{ id: "goal_1" }] });
1761
+
1762
+ const result = await client.goalSearch("test query");
1763
+ expect(result).toHaveProperty("goals");
1764
+ });
1765
+
1766
+ it("completes a goal", async () => {
1767
+ const client = createTestClient();
1768
+ mockTokenResponse();
1769
+ mockJsonResponse({ id: "goal_1", status: "pending_review" });
1770
+
1771
+ const result = await client.goalComplete("goal_1", { summary: "Done" });
1772
+ expect(result).toHaveProperty("status", "pending_review");
1773
+ });
1774
+
1775
+ it("approves a goal", async () => {
1776
+ const client = createTestClient();
1777
+ mockTokenResponse();
1778
+ mockJsonResponse({ id: "goal_1", status: "in_progress" });
1779
+
1780
+ const result = await client.goalApprove("goal_1");
1781
+ expect(result).toHaveProperty("status", "in_progress");
1782
+ });
1783
+
1784
+ it("rejects a goal", async () => {
1785
+ const client = createTestClient();
1786
+ mockTokenResponse();
1787
+ mockJsonResponse({ id: "goal_1", status: "failed" });
1788
+
1789
+ const result = await client.goalReject("goal_1", { reason: "Bad plan" });
1790
+ expect(result).toHaveProperty("status", "failed");
1791
+ });
1792
+
1793
+ it("starts a goal step", async () => {
1794
+ const client = createTestClient();
1795
+ mockTokenResponse();
1796
+ mockJsonResponse({ id: "goal_1" });
1797
+
1798
+ const result = await client.goalStepStart("goal_1", 0);
1799
+ expect(result).toHaveProperty("id", "goal_1");
1800
+ });
1801
+
1802
+ it("completes a goal step", async () => {
1803
+ const client = createTestClient();
1804
+ mockTokenResponse();
1805
+ mockJsonResponse({ id: "goal_1" });
1806
+
1807
+ const result = await client.goalStepComplete("goal_1", 0, {
1808
+ result: "Step done",
1809
+ });
1810
+ expect(result).toHaveProperty("id", "goal_1");
1811
+ });
1812
+
1813
+ it("fails a goal step", async () => {
1814
+ const client = createTestClient();
1815
+ mockTokenResponse();
1816
+ mockJsonResponse({ id: "goal_1" });
1817
+
1818
+ const result = await client.goalStepFail("goal_1", 0, {
1819
+ error: "Step failed",
1820
+ });
1821
+ expect(result).toHaveProperty("id", "goal_1");
1822
+ });
1823
+
1824
+ it("returns error for non-existent goal", async () => {
1825
+ const client = createTestClient();
1826
+ mockTokenResponse();
1827
+ mockErrorResponse(404, "Not Found");
1828
+
1829
+ await expect(client.goalGet("nonexistent")).rejects.toThrow();
1830
+ });
1831
+ });
1832
+
1833
+ // ============================================================================
1834
+ // Task CRUD Tests
1835
+ // ============================================================================
1836
+
1837
+ describe("EkoDBClient tasks", () => {
1838
+ it("creates a task", async () => {
1839
+ const client = createTestClient();
1840
+ mockTokenResponse();
1841
+ mockJsonResponse({ id: "task_1", name: "Test Task", status: "active" });
1842
+
1843
+ const result = await client.taskCreate({
1844
+ name: "Test Task",
1845
+ cron: "0 * * * *",
1846
+ });
1847
+ expect(result).toHaveProperty("id", "task_1");
1848
+ });
1849
+
1850
+ it("lists tasks", async () => {
1851
+ const client = createTestClient();
1852
+ mockTokenResponse();
1853
+ mockJsonResponse({ tasks: [] });
1854
+
1855
+ const result = await client.taskList();
1856
+ expect(result).toHaveProperty("tasks");
1857
+ });
1858
+
1859
+ it("gets a task by ID", async () => {
1860
+ const client = createTestClient();
1861
+ mockTokenResponse();
1862
+ mockJsonResponse({ id: "task_1", name: "Test Task" });
1863
+
1864
+ const result = await client.taskGet("task_1");
1865
+ expect(result).toHaveProperty("id", "task_1");
1866
+ });
1867
+
1868
+ it("updates a task", async () => {
1869
+ const client = createTestClient();
1870
+ mockTokenResponse();
1871
+ mockJsonResponse({ id: "task_1", name: "Updated" });
1872
+
1873
+ const result = await client.taskUpdate("task_1", { name: "Updated" });
1874
+ expect(result).toHaveProperty("name", "Updated");
1875
+ });
1876
+
1877
+ it("deletes a task", async () => {
1878
+ const client = createTestClient();
1879
+ mockTokenResponse();
1880
+ mockJsonResponse({});
1881
+
1882
+ await expect(client.taskDelete("task_1")).resolves.not.toThrow();
1883
+ });
1884
+
1885
+ it("gets due tasks", async () => {
1886
+ const client = createTestClient();
1887
+ mockTokenResponse();
1888
+ mockJsonResponse({ tasks: [] });
1889
+
1890
+ const result = await client.taskDue("2026-03-20T00:00:00Z");
1891
+ expect(result).toHaveProperty("tasks");
1892
+ });
1893
+
1894
+ it("starts a task", async () => {
1895
+ const client = createTestClient();
1896
+ mockTokenResponse();
1897
+ mockJsonResponse({ id: "task_1", status: "running" });
1898
+
1899
+ const result = await client.taskStart("task_1");
1900
+ expect(result).toHaveProperty("status", "running");
1901
+ });
1902
+
1903
+ it("succeeds a task", async () => {
1904
+ const client = createTestClient();
1905
+ mockTokenResponse();
1906
+ mockJsonResponse({ id: "task_1", status: "active" });
1907
+
1908
+ const result = await client.taskSucceed("task_1", { output: "Success" });
1909
+ expect(result).toHaveProperty("status", "active");
1910
+ });
1911
+
1912
+ it("fails a task", async () => {
1913
+ const client = createTestClient();
1914
+ mockTokenResponse();
1915
+ mockJsonResponse({ id: "task_1", status: "active" });
1916
+
1917
+ const result = await client.taskFail("task_1", { error: "Timeout" });
1918
+ expect(result).toHaveProperty("status", "active");
1919
+ });
1920
+
1921
+ it("pauses a task", async () => {
1922
+ const client = createTestClient();
1923
+ mockTokenResponse();
1924
+ mockJsonResponse({ id: "task_1", status: "paused" });
1925
+
1926
+ const result = await client.taskPause("task_1");
1927
+ expect(result).toHaveProperty("status", "paused");
1928
+ });
1929
+
1930
+ it("resumes a task", async () => {
1931
+ const client = createTestClient();
1932
+ mockTokenResponse();
1933
+ mockJsonResponse({ id: "task_1", status: "active" });
1934
+
1935
+ const result = await client.taskResume("task_1", {});
1936
+ expect(result).toHaveProperty("status", "active");
1937
+ });
1938
+ });
1939
+
1940
+ // ============================================================================
1941
+ // Agent CRUD Tests
1942
+ // ============================================================================
1943
+
1944
+ describe("EkoDBClient agents", () => {
1945
+ it("creates an agent", async () => {
1946
+ const client = createTestClient();
1947
+ mockTokenResponse();
1948
+ mockJsonResponse({ id: "agent_1", name: "TestAgent" });
1949
+
1950
+ const result = await client.agentCreate({
1951
+ name: "TestAgent",
1952
+ system_prompt: "You help.",
1953
+ });
1954
+ expect(result).toHaveProperty("name", "TestAgent");
1955
+ });
1956
+
1957
+ it("lists agents", async () => {
1958
+ const client = createTestClient();
1959
+ mockTokenResponse();
1960
+ mockJsonResponse({ agents: [] });
1961
+
1962
+ const result = await client.agentList();
1963
+ expect(result).toHaveProperty("agents");
1964
+ });
1965
+
1966
+ it("gets an agent by ID", async () => {
1967
+ const client = createTestClient();
1968
+ mockTokenResponse();
1969
+ mockJsonResponse({ id: "agent_1", name: "TestAgent" });
1970
+
1971
+ const result = await client.agentGet("agent_1");
1972
+ expect(result).toHaveProperty("id", "agent_1");
1973
+ });
1974
+
1975
+ it("gets an agent by name", async () => {
1976
+ const client = createTestClient();
1977
+ mockTokenResponse();
1978
+ mockJsonResponse({ id: "agent_1", name: "TestAgent" });
1979
+
1980
+ const result = await client.agentGetByName("TestAgent");
1981
+ expect(result).toHaveProperty("name", "TestAgent");
1982
+ });
1983
+
1984
+ it("updates an agent", async () => {
1985
+ const client = createTestClient();
1986
+ mockTokenResponse();
1987
+ mockJsonResponse({ id: "agent_1", name: "Updated" });
1988
+
1989
+ const result = await client.agentUpdate("agent_1", { name: "Updated" });
1990
+ expect(result).toHaveProperty("name", "Updated");
1991
+ });
1992
+
1993
+ it("deletes an agent", async () => {
1994
+ const client = createTestClient();
1995
+ mockTokenResponse();
1996
+ mockJsonResponse({});
1997
+
1998
+ await expect(client.agentDelete("agent_1")).resolves.not.toThrow();
1999
+ });
2000
+
2001
+ it("gets agents by deployment", async () => {
2002
+ const client = createTestClient();
2003
+ mockTokenResponse();
2004
+ mockJsonResponse({ agents: [{ id: "agent_1" }] });
2005
+
2006
+ const result = await client.agentsByDeployment("deploy_1");
2007
+ expect(result).toHaveProperty("agents");
2008
+ });
2009
+
2010
+ it("returns error for non-existent agent", async () => {
2011
+ const client = createTestClient();
2012
+ mockTokenResponse();
2013
+ mockErrorResponse(404, "Not Found");
2014
+
2015
+ await expect(client.agentGet("nonexistent")).rejects.toThrow();
2016
+ });
2017
+ });
2018
+
2019
+ // ============================================================================
2020
+ // rawCompletionStreamWithProgress Tests
2021
+ // ============================================================================
2022
+
2023
+ describe("EkoDBClient rawCompletionStreamWithProgress", () => {
2024
+ it("calls onToken for each token", async () => {
2025
+ const client = createTestClient();
2026
+ mockTokenResponse();
2027
+
2028
+ // Mock SSE response
2029
+ const sseBody = [
2030
+ 'data: {"token": "Hello"}',
2031
+ 'data: {"token": " World"}',
2032
+ 'data: {"content": "Hello World"}',
2033
+ ].join("\n");
2034
+
2035
+ mockFetch.mockResolvedValueOnce({
2036
+ ok: true,
2037
+ status: 200,
2038
+ text: async () => sseBody,
2039
+ headers: new Headers({ "content-type": "text/event-stream" }),
2040
+ });
2041
+
2042
+ const tokens: string[] = [];
2043
+ const result = await client.rawCompletionStreamWithProgress(
2044
+ { system_prompt: "test", message: "test" },
2045
+ (token) => tokens.push(token),
2046
+ );
2047
+
2048
+ expect(tokens).toEqual(["Hello", " World"]);
2049
+ expect(result.content).toBe("Hello World");
2050
+ });
2051
+
2052
+ it("throws on SSE error event", async () => {
2053
+ const client = createTestClient();
2054
+ mockTokenResponse();
2055
+
2056
+ const sseBody = 'data: {"error": "Model overloaded"}';
2057
+ mockFetch.mockResolvedValueOnce({
2058
+ ok: true,
2059
+ status: 200,
2060
+ text: async () => sseBody,
2061
+ headers: new Headers({ "content-type": "text/event-stream" }),
2062
+ });
2063
+
2064
+ const tokens: string[] = [];
2065
+ await expect(
2066
+ client.rawCompletionStreamWithProgress(
2067
+ { system_prompt: "test", message: "test" },
2068
+ (token) => tokens.push(token),
2069
+ ),
2070
+ ).rejects.toThrow("Model overloaded");
2071
+ });
2072
+ });
2073
+
2074
+ // ============================================================================
2075
+ // Goal Template CRUD Tests
2076
+ // ============================================================================
2077
+
2078
+ describe("EkoDBClient goal templates", () => {
2079
+ it("creates a goal template", async () => {
2080
+ const client = createTestClient();
2081
+ mockTokenResponse();
2082
+ mockJsonResponse({ id: "gt_1", title: "Deploy Checklist" });
2083
+
2084
+ const result = await client.goalTemplateCreate({
2085
+ title: "Deploy Checklist",
2086
+ steps: [{ title: "Run tests" }],
2087
+ });
2088
+ expect(result).toHaveProperty("id", "gt_1");
2089
+ expect(result).toHaveProperty("title", "Deploy Checklist");
2090
+ });
2091
+
2092
+ it("creates a goal template and verifies POST method", async () => {
2093
+ const client = createTestClient();
2094
+ mockTokenResponse();
2095
+ mockJsonResponse({ id: "gt_2", title: "Onboarding" });
2096
+
2097
+ await client.goalTemplateCreate({ title: "Onboarding" });
2098
+
2099
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2100
+ const dataCall = calls[1]; // calls[0] is token
2101
+ expect(dataCall[0]).toContain("/api/chat/goal-templates");
2102
+ expect(dataCall[1]?.method).toBe("POST");
2103
+ });
2104
+
2105
+ it("lists goal templates", async () => {
2106
+ const client = createTestClient();
2107
+ mockTokenResponse();
2108
+ mockJsonResponse({
2109
+ templates: [{ id: "gt_1" }, { id: "gt_2" }],
2110
+ });
2111
+
2112
+ const result = await client.goalTemplateList();
2113
+ expect(result).toHaveProperty("templates");
2114
+ });
2115
+
2116
+ it("lists goal templates and verifies GET method", async () => {
2117
+ const client = createTestClient();
2118
+ mockTokenResponse();
2119
+ mockJsonResponse({ templates: [] });
2120
+
2121
+ await client.goalTemplateList();
2122
+
2123
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2124
+ const dataCall = calls[1];
2125
+ expect(dataCall[0]).toContain("/api/chat/goal-templates");
2126
+ expect(dataCall[1]?.method).toBe("GET");
2127
+ });
2128
+
2129
+ it("gets a goal template by ID", async () => {
2130
+ const client = createTestClient();
2131
+ mockTokenResponse();
2132
+ mockJsonResponse({ id: "gt_1", title: "Deploy Checklist" });
2133
+
2134
+ const result = await client.goalTemplateGet("gt_1");
2135
+ expect(result).toHaveProperty("id", "gt_1");
2136
+ expect(result).toHaveProperty("title", "Deploy Checklist");
2137
+ });
2138
+
2139
+ it("gets a goal template and verifies endpoint", async () => {
2140
+ const client = createTestClient();
2141
+ mockTokenResponse();
2142
+ mockJsonResponse({ id: "gt_1", title: "Deploy Checklist" });
2143
+
2144
+ await client.goalTemplateGet("gt_1");
2145
+
2146
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2147
+ const dataCall = calls[1];
2148
+ expect(dataCall[0]).toContain("/api/chat/goal-templates/gt_1");
2149
+ expect(dataCall[1]?.method).toBe("GET");
2150
+ });
2151
+
2152
+ it("updates a goal template", async () => {
2153
+ const client = createTestClient();
2154
+ mockTokenResponse();
2155
+ mockJsonResponse({ id: "gt_1", title: "Updated Checklist" });
2156
+
2157
+ const result = await client.goalTemplateUpdate("gt_1", {
2158
+ title: "Updated Checklist",
2159
+ });
2160
+ expect(result).toHaveProperty("title", "Updated Checklist");
2161
+ });
2162
+
2163
+ it("updates a goal template and verifies PUT method", async () => {
2164
+ const client = createTestClient();
2165
+ mockTokenResponse();
2166
+ mockJsonResponse({ id: "gt_1", title: "Updated" });
2167
+
2168
+ await client.goalTemplateUpdate("gt_1", { title: "Updated" });
2169
+
2170
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2171
+ const dataCall = calls[1];
2172
+ expect(dataCall[0]).toContain("/api/chat/goal-templates/gt_1");
2173
+ expect(dataCall[1]?.method).toBe("PUT");
2174
+ });
2175
+
2176
+ it("deletes a goal template", async () => {
2177
+ const client = createTestClient();
2178
+ mockTokenResponse();
2179
+ mockJsonResponse({});
2180
+
2181
+ await expect(client.goalTemplateDelete("gt_1")).resolves.not.toThrow();
2182
+ });
2183
+
2184
+ it("deletes a goal template and verifies DELETE method", async () => {
2185
+ const client = createTestClient();
2186
+ mockTokenResponse();
2187
+ mockJsonResponse({});
2188
+
2189
+ await client.goalTemplateDelete("gt_1");
2190
+
2191
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2192
+ const dataCall = calls[1];
2193
+ expect(dataCall[0]).toContain("/api/chat/goal-templates/gt_1");
2194
+ expect(dataCall[1]?.method).toBe("DELETE");
2195
+ });
2196
+
2197
+ it("returns error for non-existent goal template", async () => {
2198
+ const client = createTestClient();
2199
+ mockTokenResponse();
2200
+ mockErrorResponse(404, "Not Found");
2201
+
2202
+ await expect(client.goalTemplateGet("nonexistent")).rejects.toThrow();
2203
+ });
2204
+ });
2205
+
2206
+ // ============================================================================
2207
+ // chatMessageStream (SSE) Tests
2208
+ // ============================================================================
2209
+
2210
+ describe("EkoDBClient chatMessageStream", () => {
2211
+ it("emits chunk and end events from SSE stream", async () => {
2212
+ const client = createTestClient();
2213
+ mockTokenResponse();
2214
+
2215
+ const sseBody = [
2216
+ 'data: {"token":"Hello"}',
2217
+ 'data: {"token":" world"}',
2218
+ 'data: {"content":"Hello world","message_id":"msg_1","execution_time_ms":42}',
2219
+ "",
2220
+ ].join("\n");
2221
+
2222
+ mockFetch.mockResolvedValueOnce({
2223
+ ok: true,
2224
+ status: 200,
2225
+ text: async () => sseBody,
2226
+ headers: new Headers({ "content-type": "text/event-stream" }),
2227
+ });
2228
+
2229
+ const events: any[] = [];
2230
+ const stream = client.chatMessageStream("chat_123", {
2231
+ message: "Hello",
2232
+ });
2233
+ stream.on("event", (evt: any) => events.push(evt));
2234
+
2235
+ // Wait for the async SSE processing to complete
2236
+ await new Promise((resolve) => setTimeout(resolve, 50));
2237
+
2238
+ expect(events).toHaveLength(3);
2239
+ expect(events[0]).toEqual({ type: "chunk", content: "Hello" });
2240
+ expect(events[1]).toEqual({ type: "chunk", content: " world" });
2241
+ expect(events[2].type).toBe("end");
2242
+ expect(events[2].messageId).toBe("msg_1");
2243
+ expect(events[2].executionTimeMs).toBe(42);
2244
+ });
2245
+
2246
+ it("emits error event on SSE error", async () => {
2247
+ const client = createTestClient();
2248
+ mockTokenResponse();
2249
+
2250
+ const sseBody = 'data: {"error":"LLM timeout"}\n';
2251
+
2252
+ mockFetch.mockResolvedValueOnce({
2253
+ ok: true,
2254
+ status: 200,
2255
+ text: async () => sseBody,
2256
+ headers: new Headers({ "content-type": "text/event-stream" }),
2257
+ });
2258
+
2259
+ const events: any[] = [];
2260
+ const stream = client.chatMessageStream("chat_123", {
2261
+ message: "Hello",
2262
+ });
2263
+ stream.on("event", (evt: any) => events.push(evt));
2264
+
2265
+ await new Promise((resolve) => setTimeout(resolve, 50));
2266
+
2267
+ expect(events).toHaveLength(1);
2268
+ expect(events[0]).toEqual({ type: "error", error: "LLM timeout" });
2269
+ });
2270
+
2271
+ it("emits error event on non-200 HTTP response", async () => {
2272
+ const client = createTestClient();
2273
+ mockTokenResponse();
2274
+
2275
+ mockFetch.mockResolvedValueOnce({
2276
+ ok: false,
2277
+ status: 401,
2278
+ text: async () => "Unauthorized",
2279
+ headers: new Headers(),
2280
+ });
2281
+
2282
+ const events: any[] = [];
2283
+ const stream = client.chatMessageStream("chat_123", {
2284
+ message: "Hello",
2285
+ });
2286
+ stream.on("event", (evt: any) => events.push(evt));
2287
+
2288
+ await new Promise((resolve) => setTimeout(resolve, 50));
2289
+
2290
+ expect(events).toHaveLength(1);
2291
+ expect(events[0].type).toBe("error");
2292
+ expect(events[0].error).toContain("401");
2293
+ });
2294
+
2295
+ it("calls the correct stream endpoint", async () => {
2296
+ const client = createTestClient();
2297
+ mockTokenResponse();
2298
+
2299
+ mockFetch.mockResolvedValueOnce({
2300
+ ok: true,
2301
+ status: 200,
2302
+ text: async () => 'data: {"token":"ok"}\n',
2303
+ headers: new Headers({ "content-type": "text/event-stream" }),
2304
+ });
2305
+
2306
+ client.chatMessageStream("chat_456", { message: "Test" });
2307
+
2308
+ await new Promise((resolve) => setTimeout(resolve, 50));
2309
+
2310
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2311
+ const dataCall = calls[1];
2312
+ expect(dataCall[0]).toContain("/api/chat/chat_456/messages/stream");
2313
+ expect(dataCall[1]?.method).toBe("POST");
2314
+ expect(dataCall[1]?.headers?.Accept).toBe("text/event-stream");
2315
+ });
2316
+ });
2317
+
2318
+ // ============================================================================
2319
+ // Schedule CRUD Tests
2320
+ // ============================================================================
2321
+
2322
+ describe("EkoDBClient schedules", () => {
2323
+ it("creates a schedule", async () => {
2324
+ const client = createTestClient();
2325
+ mockTokenResponse();
2326
+ mockJsonResponse({
2327
+ id: "sched_1",
2328
+ name: "Nightly Backup",
2329
+ cron: "0 2 * * *",
2330
+ status: "active",
2331
+ });
2332
+
2333
+ const result = await client.createSchedule({
2334
+ name: "Nightly Backup",
2335
+ cron: "0 2 * * *",
2336
+ action: "backup",
2337
+ });
2338
+ expect(result).toHaveProperty("id", "sched_1");
2339
+ expect(result).toHaveProperty("status", "active");
2340
+ });
2341
+
2342
+ it("lists schedules", async () => {
2343
+ const client = createTestClient();
2344
+ mockTokenResponse();
2345
+ mockJsonResponse({
2346
+ schedules: [
2347
+ { id: "sched_1", name: "Nightly Backup" },
2348
+ { id: "sched_2", name: "Hourly Sync" },
2349
+ ],
2350
+ });
2351
+
2352
+ const result = await client.listSchedules();
2353
+ expect(result).toHaveProperty("schedules");
2354
+ });
2355
+
2356
+ it("gets a schedule by ID", async () => {
2357
+ const client = createTestClient();
2358
+ mockTokenResponse();
2359
+ mockJsonResponse({
2360
+ id: "sched_1",
2361
+ name: "Nightly Backup",
2362
+ cron: "0 2 * * *",
2363
+ });
2364
+
2365
+ const result = await client.getSchedule("sched_1");
2366
+ expect(result).toHaveProperty("id", "sched_1");
2367
+ expect(result).toHaveProperty("name", "Nightly Backup");
2368
+ });
2369
+
2370
+ it("updates a schedule", async () => {
2371
+ const client = createTestClient();
2372
+ mockTokenResponse();
2373
+ mockJsonResponse({
2374
+ id: "sched_1",
2375
+ name: "Updated Backup",
2376
+ cron: "0 3 * * *",
2377
+ });
2378
+
2379
+ const result = await client.updateSchedule("sched_1", {
2380
+ name: "Updated Backup",
2381
+ cron: "0 3 * * *",
2382
+ });
2383
+ expect(result).toHaveProperty("name", "Updated Backup");
2384
+ expect(result).toHaveProperty("cron", "0 3 * * *");
2385
+ });
2386
+
2387
+ it("deletes a schedule", async () => {
2388
+ const client = createTestClient();
2389
+ mockTokenResponse();
2390
+ mockJsonResponse({});
2391
+
2392
+ await expect(client.deleteSchedule("sched_1")).resolves.not.toThrow();
2393
+ });
2394
+
2395
+ it("pauses a schedule", async () => {
2396
+ const client = createTestClient();
2397
+ mockTokenResponse();
2398
+ mockJsonResponse({ id: "sched_1", status: "paused" });
2399
+
2400
+ const result = await client.pauseSchedule("sched_1");
2401
+ expect(result).toHaveProperty("status", "paused");
2402
+ });
2403
+
2404
+ it("resumes a schedule", async () => {
2405
+ const client = createTestClient();
2406
+ mockTokenResponse();
2407
+ mockJsonResponse({ id: "sched_1", status: "active" });
2408
+
2409
+ const result = await client.resumeSchedule("sched_1");
2410
+ expect(result).toHaveProperty("status", "active");
2411
+ });
2412
+ });
2413
+
2414
+ // ============================================================================
2415
+ // KV Links Tests
2416
+ // ============================================================================
2417
+
2418
+ describe("EkoDBClient kv links", () => {
2419
+ it("gets links for a KV key", async () => {
2420
+ const client = createTestClient();
2421
+ mockTokenResponse();
2422
+ mockJsonResponse({
2423
+ links: [
2424
+ { collection: "users", document_id: "user_1" },
2425
+ { collection: "orders", document_id: "order_1" },
2426
+ ],
2427
+ });
2428
+
2429
+ const result = await client.kvGetLinks("session:user123");
2430
+ expect(result).toHaveProperty("links");
2431
+ });
2432
+
2433
+ it("links a document to a KV key", async () => {
2434
+ const client = createTestClient();
2435
+ mockTokenResponse();
2436
+ mockJsonResponse({ status: "linked" });
2437
+
2438
+ const result = await client.kvLink("session:user123", "users", "user_1");
2439
+ expect(result).toHaveProperty("status", "linked");
2440
+ });
2441
+
2442
+ it("unlinks a document from a KV key", async () => {
2443
+ const client = createTestClient();
2444
+ mockTokenResponse();
2445
+ mockJsonResponse({ status: "unlinked" });
2446
+
2447
+ const result = await client.kvUnlink("session:user123", "users", "user_1");
2448
+ expect(result).toHaveProperty("status", "unlinked");
2449
+ });
2450
+ });
2451
+
2452
+ // ============================================================================
2453
+ // Text Search and Hybrid Search Tests
2454
+ // ============================================================================
2455
+
2456
+ describe("EkoDBClient text and hybrid search", () => {
2457
+ it("performs text search", async () => {
2458
+ const client = createTestClient();
2459
+ mockTokenResponse();
2460
+ mockJsonResponse({
2461
+ results: [
2462
+ {
2463
+ record: { id: "doc_1", title: "Ownership Guide" },
2464
+ score: 0.92,
2465
+ matched_fields: ["title"],
2466
+ },
2467
+ {
2468
+ record: { id: "doc_2", title: "Property Ownership" },
2469
+ score: 0.85,
2470
+ matched_fields: ["title"],
2471
+ },
2472
+ ],
2473
+ total: 2,
2474
+ took_ms: 12,
2475
+ });
2476
+
2477
+ const result = await client.textSearch("documents", "ownership", {
2478
+ limit: 10,
2479
+ select_fields: ["title", "content"],
2480
+ });
2481
+
2482
+ expect(result.results).toHaveLength(2);
2483
+ expect(result.total).toBe(2);
2484
+ expect(result.results[0].score).toBe(0.92);
2485
+ });
2486
+
2487
+ it("performs hybrid search", async () => {
2488
+ const client = createTestClient();
2489
+ mockTokenResponse();
2490
+ mockJsonResponse({
2491
+ results: [
2492
+ {
2493
+ record: { id: "doc_1", title: "ML Guide", content: "Learn ML" },
2494
+ score: 0.95,
2495
+ matched_fields: ["title", "content"],
2496
+ },
2497
+ ],
2498
+ total: 1,
2499
+ took_ms: 25,
2500
+ });
2501
+
2502
+ const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
2503
+ const result = await client.hybridSearch(
2504
+ "documents",
2505
+ "machine learning",
2506
+ queryVector,
2507
+ 5,
2508
+ );
2509
+
2510
+ expect(result).toHaveLength(1);
2511
+ expect(result[0]).toHaveProperty("id", "doc_1");
2512
+ expect(result[0]).toHaveProperty("title", "ML Guide");
2513
+ });
2514
+ });
2515
+
2516
+ // ============================================================================
2517
+ // Auth Token Management
2518
+ // ============================================================================
2519
+
2520
+ describe("EkoDBClient auth token management", () => {
2521
+ beforeEach(() => {
2522
+ mockFetch.mockReset();
2523
+ });
2524
+
2525
+ it("getToken auto-refreshes when token is about to expire", async () => {
2526
+ const client = createTestClient();
2527
+
2528
+ // First init — get a token with a short expiry (30s from now)
2529
+ const shortExp = Math.floor(Date.now() / 1000) + 30;
2530
+ const shortPayload = btoa(JSON.stringify({ exp: shortExp }))
2531
+ .replace(/\+/g, "-")
2532
+ .replace(/\//g, "_")
2533
+ .replace(/=/g, "");
2534
+ const shortJwt = `eyJhbGciOiJIUzI1NiJ9.${shortPayload}.fakesig`;
2535
+ mockFetch.mockResolvedValueOnce({
2536
+ ok: true,
2537
+ status: 200,
2538
+ json: async () => ({ token: shortJwt }),
2539
+ headers: new Headers(),
2540
+ });
2541
+ await client.init();
2542
+
2543
+ // Now getToken should see it's about to expire and refresh
2544
+ const newExp = Math.floor(Date.now() / 1000) + 3600;
2545
+ const newPayload = btoa(JSON.stringify({ exp: newExp }))
2546
+ .replace(/\+/g, "-")
2547
+ .replace(/\//g, "_")
2548
+ .replace(/=/g, "");
2549
+ const newJwt = `eyJhbGciOiJIUzI1NiJ9.${newPayload}.fakesig`;
2550
+ mockFetch.mockResolvedValueOnce({
2551
+ ok: true,
2552
+ status: 200,
2553
+ json: async () => ({ token: newJwt }),
2554
+ headers: new Headers(),
2555
+ });
2556
+
2557
+ const token = await client.getToken();
2558
+ expect(token).toBe(newJwt);
2559
+ // 2 fetch calls: init + proactive refresh
2560
+ expect(mockFetch).toHaveBeenCalledTimes(2);
2561
+ });
2562
+
2563
+ it("getToken returns cached token when not expired", async () => {
2564
+ const client = createTestClient();
2565
+
2566
+ // Token with expiry far in the future
2567
+ const farExp = Math.floor(Date.now() / 1000) + 7200;
2568
+ const payload = btoa(JSON.stringify({ exp: farExp }))
2569
+ .replace(/\+/g, "-")
2570
+ .replace(/\//g, "_")
2571
+ .replace(/=/g, "");
2572
+ const jwt = `eyJhbGciOiJIUzI1NiJ9.${payload}.fakesig`;
2573
+ mockFetch.mockResolvedValueOnce({
2574
+ ok: true,
2575
+ status: 200,
2576
+ json: async () => ({ token: jwt }),
2577
+ headers: new Headers(),
2578
+ });
2579
+ await client.init();
2580
+
2581
+ // getToken should return cached — no extra fetch
2582
+ const token = await client.getToken();
2583
+ expect(token).toBe(jwt);
2584
+ expect(mockFetch).toHaveBeenCalledTimes(1); // only init
2585
+ });
2586
+
2587
+ it("clearTokenCache resets token and expiry", async () => {
2588
+ const client = createTestClient();
2589
+ mockTokenResponse();
2590
+ await client.init();
2591
+ expect(await client.getToken()).toBeTruthy();
2592
+
2593
+ client.clearTokenCache();
2594
+ // After clear, getToken will auto-refresh (fetch a new token)
2595
+ mockTokenResponse();
2596
+ const token = await client.getToken();
2597
+ expect(token).toBe("test-jwt-token");
2598
+ // 2 fetch calls: init + post-clear getToken refresh
2599
+ expect(mockFetch).toHaveBeenCalledTimes(2);
2600
+ });
2601
+ });