@ekodb/ekodb-client 0.13.0 → 0.15.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.
@@ -1105,6 +1105,80 @@ describe("Convenience methods", () => {
1105
1105
  });
1106
1106
  });
1107
1107
 
1108
+ // ============================================================================
1109
+ // executeTool Tests
1110
+ // ============================================================================
1111
+
1112
+ describe("EkoDBClient executeTool", () => {
1113
+ it("executes tool successfully", async () => {
1114
+ const client = createTestClient();
1115
+
1116
+ mockTokenResponse();
1117
+ mockJsonResponse({
1118
+ success: true,
1119
+ result: { count: 42 },
1120
+ });
1121
+
1122
+ const result = await client.executeTool("count_records", {
1123
+ collection: "users",
1124
+ });
1125
+
1126
+ expect(result).toEqual({ count: 42 });
1127
+ });
1128
+
1129
+ it("passes chat_id when provided", async () => {
1130
+ const client = createTestClient();
1131
+
1132
+ mockTokenResponse();
1133
+ mockJsonResponse({
1134
+ success: true,
1135
+ result: { value: "hello" },
1136
+ });
1137
+
1138
+ const result = await client.executeTool(
1139
+ "kv_get",
1140
+ { key: "greeting" },
1141
+ "chat_456",
1142
+ );
1143
+
1144
+ expect(result).toEqual({ value: "hello" });
1145
+
1146
+ // Verify the request body included chat_id
1147
+ const lastCall = mockFetch.mock.calls[1];
1148
+ const body = JSON.parse(lastCall[1].body);
1149
+ expect(body.chat_id).toBe("chat_456");
1150
+ expect(body.tool).toBe("kv_get");
1151
+ expect(body.params).toEqual({ key: "greeting" });
1152
+ });
1153
+
1154
+ it("throws on tool execution failure", async () => {
1155
+ const client = createTestClient();
1156
+
1157
+ mockTokenResponse();
1158
+ mockJsonResponse({
1159
+ success: false,
1160
+ error: "permission denied",
1161
+ });
1162
+
1163
+ await expect(
1164
+ client.executeTool("delete_collection", { collection: "system" }),
1165
+ ).rejects.toThrow("permission denied");
1166
+ });
1167
+
1168
+ it("returns null when server does not support endpoint", async () => {
1169
+ const client = createTestClient();
1170
+
1171
+ mockTokenResponse();
1172
+ mockErrorResponse(404, "Not Found");
1173
+
1174
+ const result = await client.executeTool("count_records", {
1175
+ collection: "users",
1176
+ });
1177
+
1178
+ expect(result).toBeNull();
1179
+ });
1180
+ });
1181
+
1108
1182
  // ============================================================================
1109
1183
  // Chat Models Tests
1110
1184
  // ============================================================================
@@ -1480,6 +1554,126 @@ describe("EkoDBClient rawCompletion", () => {
1480
1554
  });
1481
1555
  });
1482
1556
 
1557
+ // ============================================================================
1558
+ // rawCompletionStream (SSE) Tests
1559
+ // ============================================================================
1560
+
1561
+ describe("EkoDBClient rawCompletionStream", () => {
1562
+ it("parses SSE done event and returns content", async () => {
1563
+ const client = createTestClient();
1564
+
1565
+ mockTokenResponse();
1566
+ // SSE response with token chunks and a done event
1567
+ const sseBody = [
1568
+ 'data: {"token":"Hello"}',
1569
+ 'data: {"token":" world"}',
1570
+ 'data: {"content":"Hello world","done":true}',
1571
+ "",
1572
+ ].join("\n");
1573
+
1574
+ mockFetch.mockResolvedValueOnce({
1575
+ ok: true,
1576
+ status: 200,
1577
+ text: async () => sseBody,
1578
+ headers: new Headers(),
1579
+ });
1580
+
1581
+ const result = await client.rawCompletionStream({
1582
+ system_prompt: "System.",
1583
+ message: "User.",
1584
+ });
1585
+
1586
+ expect(result.content).toBe("Hello world");
1587
+ });
1588
+
1589
+ it("accumulates tokens when no done event", async () => {
1590
+ const client = createTestClient();
1591
+
1592
+ mockTokenResponse();
1593
+ const sseBody = [
1594
+ 'data: {"token":"chunk1"}',
1595
+ 'data: {"token":"chunk2"}',
1596
+ "",
1597
+ ].join("\n");
1598
+
1599
+ mockFetch.mockResolvedValueOnce({
1600
+ ok: true,
1601
+ status: 200,
1602
+ text: async () => sseBody,
1603
+ headers: new Headers(),
1604
+ });
1605
+
1606
+ const result = await client.rawCompletionStream({
1607
+ system_prompt: "System.",
1608
+ message: "User.",
1609
+ });
1610
+
1611
+ expect(result.content).toBe("chunk1chunk2");
1612
+ });
1613
+
1614
+ it("throws on SSE error event", async () => {
1615
+ const client = createTestClient();
1616
+
1617
+ mockTokenResponse();
1618
+ const sseBody = 'data: {"error":"LLM timeout"}\n';
1619
+
1620
+ mockFetch.mockResolvedValueOnce({
1621
+ ok: true,
1622
+ status: 200,
1623
+ text: async () => sseBody,
1624
+ headers: new Headers(),
1625
+ });
1626
+
1627
+ await expect(
1628
+ client.rawCompletionStream({
1629
+ system_prompt: "System.",
1630
+ message: "User.",
1631
+ }),
1632
+ ).rejects.toThrow("LLM timeout");
1633
+ });
1634
+
1635
+ it("throws on non-200 HTTP response", async () => {
1636
+ const client = createTestClient();
1637
+
1638
+ mockTokenResponse();
1639
+ mockFetch.mockResolvedValueOnce({
1640
+ ok: false,
1641
+ status: 401,
1642
+ text: async () => "Unauthorized",
1643
+ headers: new Headers(),
1644
+ });
1645
+
1646
+ await expect(
1647
+ client.rawCompletionStream({
1648
+ system_prompt: "System.",
1649
+ message: "User.",
1650
+ }),
1651
+ ).rejects.toThrow("401");
1652
+ });
1653
+
1654
+ it("calls the /stream endpoint", async () => {
1655
+ const client = createTestClient();
1656
+
1657
+ mockTokenResponse();
1658
+ mockFetch.mockResolvedValueOnce({
1659
+ ok: true,
1660
+ status: 200,
1661
+ text: async () => 'data: {"content":"ok"}\n',
1662
+ headers: new Headers(),
1663
+ });
1664
+
1665
+ await client.rawCompletionStream({
1666
+ system_prompt: "System.",
1667
+ message: "User.",
1668
+ });
1669
+
1670
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
1671
+ const dataCall = calls[1];
1672
+ expect(dataCall[0]).toContain("/api/chat/complete/stream");
1673
+ expect(dataCall[1]?.headers?.Accept).toBe("text/event-stream");
1674
+ });
1675
+ });
1676
+
1483
1677
  // ============================================================================
1484
1678
  // Token Management Tests
1485
1679
  // ============================================================================
@@ -1583,3 +1777,899 @@ describe("findByIdWithProjection", () => {
1583
1777
  expect(dataCall[0]).toBe("http://localhost:8080/api/find/users/123");
1584
1778
  });
1585
1779
  });
1780
+
1781
+ // ============================================================================
1782
+ // Goal CRUD Tests
1783
+ // ============================================================================
1784
+
1785
+ describe("EkoDBClient goals", () => {
1786
+ it("creates a goal", async () => {
1787
+ const client = createTestClient();
1788
+ mockTokenResponse();
1789
+ mockJsonResponse({ id: "goal_1", title: "Test Goal", status: "active" });
1790
+
1791
+ const result = await client.goalCreate({ title: "Test Goal" });
1792
+ expect(result).toHaveProperty("id", "goal_1");
1793
+ expect(result).toHaveProperty("status", "active");
1794
+ });
1795
+
1796
+ it("lists goals", async () => {
1797
+ const client = createTestClient();
1798
+ mockTokenResponse();
1799
+ mockJsonResponse({ goals: [{ id: "goal_1" }, { id: "goal_2" }] });
1800
+
1801
+ const result = await client.goalList();
1802
+ expect(result).toHaveProperty("goals");
1803
+ });
1804
+
1805
+ it("gets a goal by ID", async () => {
1806
+ const client = createTestClient();
1807
+ mockTokenResponse();
1808
+ mockJsonResponse({ id: "goal_1", title: "Test Goal" });
1809
+
1810
+ const result = await client.goalGet("goal_1");
1811
+ expect(result).toHaveProperty("id", "goal_1");
1812
+ });
1813
+
1814
+ it("updates a goal", async () => {
1815
+ const client = createTestClient();
1816
+ mockTokenResponse();
1817
+ mockJsonResponse({ id: "goal_1", title: "Updated" });
1818
+
1819
+ const result = await client.goalUpdate("goal_1", { title: "Updated" });
1820
+ expect(result).toHaveProperty("title", "Updated");
1821
+ });
1822
+
1823
+ it("deletes a goal", async () => {
1824
+ const client = createTestClient();
1825
+ mockTokenResponse();
1826
+ mockJsonResponse({});
1827
+
1828
+ await expect(client.goalDelete("goal_1")).resolves.not.toThrow();
1829
+ });
1830
+
1831
+ it("searches goals", async () => {
1832
+ const client = createTestClient();
1833
+ mockTokenResponse();
1834
+ mockJsonResponse({ goals: [{ id: "goal_1" }] });
1835
+
1836
+ const result = await client.goalSearch("test query");
1837
+ expect(result).toHaveProperty("goals");
1838
+ });
1839
+
1840
+ it("completes a goal", async () => {
1841
+ const client = createTestClient();
1842
+ mockTokenResponse();
1843
+ mockJsonResponse({ id: "goal_1", status: "pending_review" });
1844
+
1845
+ const result = await client.goalComplete("goal_1", { summary: "Done" });
1846
+ expect(result).toHaveProperty("status", "pending_review");
1847
+ });
1848
+
1849
+ it("approves a goal", async () => {
1850
+ const client = createTestClient();
1851
+ mockTokenResponse();
1852
+ mockJsonResponse({ id: "goal_1", status: "in_progress" });
1853
+
1854
+ const result = await client.goalApprove("goal_1");
1855
+ expect(result).toHaveProperty("status", "in_progress");
1856
+ });
1857
+
1858
+ it("rejects a goal", async () => {
1859
+ const client = createTestClient();
1860
+ mockTokenResponse();
1861
+ mockJsonResponse({ id: "goal_1", status: "failed" });
1862
+
1863
+ const result = await client.goalReject("goal_1", { reason: "Bad plan" });
1864
+ expect(result).toHaveProperty("status", "failed");
1865
+ });
1866
+
1867
+ it("starts a goal step", async () => {
1868
+ const client = createTestClient();
1869
+ mockTokenResponse();
1870
+ mockJsonResponse({ id: "goal_1" });
1871
+
1872
+ const result = await client.goalStepStart("goal_1", 0);
1873
+ expect(result).toHaveProperty("id", "goal_1");
1874
+ });
1875
+
1876
+ it("completes a goal step", async () => {
1877
+ const client = createTestClient();
1878
+ mockTokenResponse();
1879
+ mockJsonResponse({ id: "goal_1" });
1880
+
1881
+ const result = await client.goalStepComplete("goal_1", 0, {
1882
+ result: "Step done",
1883
+ });
1884
+ expect(result).toHaveProperty("id", "goal_1");
1885
+ });
1886
+
1887
+ it("fails a goal step", async () => {
1888
+ const client = createTestClient();
1889
+ mockTokenResponse();
1890
+ mockJsonResponse({ id: "goal_1" });
1891
+
1892
+ const result = await client.goalStepFail("goal_1", 0, {
1893
+ error: "Step failed",
1894
+ });
1895
+ expect(result).toHaveProperty("id", "goal_1");
1896
+ });
1897
+
1898
+ it("returns error for non-existent goal", async () => {
1899
+ const client = createTestClient();
1900
+ mockTokenResponse();
1901
+ mockErrorResponse(404, "Not Found");
1902
+
1903
+ await expect(client.goalGet("nonexistent")).rejects.toThrow();
1904
+ });
1905
+ });
1906
+
1907
+ // ============================================================================
1908
+ // Task CRUD Tests
1909
+ // ============================================================================
1910
+
1911
+ describe("EkoDBClient tasks", () => {
1912
+ it("creates a task", async () => {
1913
+ const client = createTestClient();
1914
+ mockTokenResponse();
1915
+ mockJsonResponse({ id: "task_1", name: "Test Task", status: "active" });
1916
+
1917
+ const result = await client.taskCreate({
1918
+ name: "Test Task",
1919
+ cron: "0 * * * *",
1920
+ });
1921
+ expect(result).toHaveProperty("id", "task_1");
1922
+ });
1923
+
1924
+ it("lists tasks", async () => {
1925
+ const client = createTestClient();
1926
+ mockTokenResponse();
1927
+ mockJsonResponse({ tasks: [] });
1928
+
1929
+ const result = await client.taskList();
1930
+ expect(result).toHaveProperty("tasks");
1931
+ });
1932
+
1933
+ it("gets a task by ID", async () => {
1934
+ const client = createTestClient();
1935
+ mockTokenResponse();
1936
+ mockJsonResponse({ id: "task_1", name: "Test Task" });
1937
+
1938
+ const result = await client.taskGet("task_1");
1939
+ expect(result).toHaveProperty("id", "task_1");
1940
+ });
1941
+
1942
+ it("updates a task", async () => {
1943
+ const client = createTestClient();
1944
+ mockTokenResponse();
1945
+ mockJsonResponse({ id: "task_1", name: "Updated" });
1946
+
1947
+ const result = await client.taskUpdate("task_1", { name: "Updated" });
1948
+ expect(result).toHaveProperty("name", "Updated");
1949
+ });
1950
+
1951
+ it("deletes a task", async () => {
1952
+ const client = createTestClient();
1953
+ mockTokenResponse();
1954
+ mockJsonResponse({});
1955
+
1956
+ await expect(client.taskDelete("task_1")).resolves.not.toThrow();
1957
+ });
1958
+
1959
+ it("gets due tasks", async () => {
1960
+ const client = createTestClient();
1961
+ mockTokenResponse();
1962
+ mockJsonResponse({ tasks: [] });
1963
+
1964
+ const result = await client.taskDue("2026-03-20T00:00:00Z");
1965
+ expect(result).toHaveProperty("tasks");
1966
+ });
1967
+
1968
+ it("starts a task", async () => {
1969
+ const client = createTestClient();
1970
+ mockTokenResponse();
1971
+ mockJsonResponse({ id: "task_1", status: "running" });
1972
+
1973
+ const result = await client.taskStart("task_1");
1974
+ expect(result).toHaveProperty("status", "running");
1975
+ });
1976
+
1977
+ it("succeeds a task", async () => {
1978
+ const client = createTestClient();
1979
+ mockTokenResponse();
1980
+ mockJsonResponse({ id: "task_1", status: "active" });
1981
+
1982
+ const result = await client.taskSucceed("task_1", { output: "Success" });
1983
+ expect(result).toHaveProperty("status", "active");
1984
+ });
1985
+
1986
+ it("fails a task", async () => {
1987
+ const client = createTestClient();
1988
+ mockTokenResponse();
1989
+ mockJsonResponse({ id: "task_1", status: "active" });
1990
+
1991
+ const result = await client.taskFail("task_1", { error: "Timeout" });
1992
+ expect(result).toHaveProperty("status", "active");
1993
+ });
1994
+
1995
+ it("pauses a task", async () => {
1996
+ const client = createTestClient();
1997
+ mockTokenResponse();
1998
+ mockJsonResponse({ id: "task_1", status: "paused" });
1999
+
2000
+ const result = await client.taskPause("task_1");
2001
+ expect(result).toHaveProperty("status", "paused");
2002
+ });
2003
+
2004
+ it("resumes a task", async () => {
2005
+ const client = createTestClient();
2006
+ mockTokenResponse();
2007
+ mockJsonResponse({ id: "task_1", status: "active" });
2008
+
2009
+ const result = await client.taskResume("task_1", {});
2010
+ expect(result).toHaveProperty("status", "active");
2011
+ });
2012
+ });
2013
+
2014
+ // ============================================================================
2015
+ // Agent CRUD Tests
2016
+ // ============================================================================
2017
+
2018
+ describe("EkoDBClient agents", () => {
2019
+ it("creates an agent", async () => {
2020
+ const client = createTestClient();
2021
+ mockTokenResponse();
2022
+ mockJsonResponse({ id: "agent_1", name: "TestAgent" });
2023
+
2024
+ const result = await client.agentCreate({
2025
+ name: "TestAgent",
2026
+ system_prompt: "You help.",
2027
+ });
2028
+ expect(result).toHaveProperty("name", "TestAgent");
2029
+ });
2030
+
2031
+ it("lists agents", async () => {
2032
+ const client = createTestClient();
2033
+ mockTokenResponse();
2034
+ mockJsonResponse({ agents: [] });
2035
+
2036
+ const result = await client.agentList();
2037
+ expect(result).toHaveProperty("agents");
2038
+ });
2039
+
2040
+ it("gets an agent by ID", async () => {
2041
+ const client = createTestClient();
2042
+ mockTokenResponse();
2043
+ mockJsonResponse({ id: "agent_1", name: "TestAgent" });
2044
+
2045
+ const result = await client.agentGet("agent_1");
2046
+ expect(result).toHaveProperty("id", "agent_1");
2047
+ });
2048
+
2049
+ it("gets an agent by name", async () => {
2050
+ const client = createTestClient();
2051
+ mockTokenResponse();
2052
+ mockJsonResponse({ id: "agent_1", name: "TestAgent" });
2053
+
2054
+ const result = await client.agentGetByName("TestAgent");
2055
+ expect(result).toHaveProperty("name", "TestAgent");
2056
+ });
2057
+
2058
+ it("updates an agent", async () => {
2059
+ const client = createTestClient();
2060
+ mockTokenResponse();
2061
+ mockJsonResponse({ id: "agent_1", name: "Updated" });
2062
+
2063
+ const result = await client.agentUpdate("agent_1", { name: "Updated" });
2064
+ expect(result).toHaveProperty("name", "Updated");
2065
+ });
2066
+
2067
+ it("deletes an agent", async () => {
2068
+ const client = createTestClient();
2069
+ mockTokenResponse();
2070
+ mockJsonResponse({});
2071
+
2072
+ await expect(client.agentDelete("agent_1")).resolves.not.toThrow();
2073
+ });
2074
+
2075
+ it("gets agents by deployment", async () => {
2076
+ const client = createTestClient();
2077
+ mockTokenResponse();
2078
+ mockJsonResponse({ agents: [{ id: "agent_1" }] });
2079
+
2080
+ const result = await client.agentsByDeployment("deploy_1");
2081
+ expect(result).toHaveProperty("agents");
2082
+ });
2083
+
2084
+ it("returns error for non-existent agent", async () => {
2085
+ const client = createTestClient();
2086
+ mockTokenResponse();
2087
+ mockErrorResponse(404, "Not Found");
2088
+
2089
+ await expect(client.agentGet("nonexistent")).rejects.toThrow();
2090
+ });
2091
+ });
2092
+
2093
+ // ============================================================================
2094
+ // rawCompletionStreamWithProgress Tests
2095
+ // ============================================================================
2096
+
2097
+ describe("EkoDBClient rawCompletionStreamWithProgress", () => {
2098
+ it("calls onToken for each token", async () => {
2099
+ const client = createTestClient();
2100
+ mockTokenResponse();
2101
+
2102
+ // Mock SSE response
2103
+ const sseBody = [
2104
+ 'data: {"token": "Hello"}',
2105
+ 'data: {"token": " World"}',
2106
+ 'data: {"content": "Hello World"}',
2107
+ ].join("\n");
2108
+
2109
+ mockFetch.mockResolvedValueOnce({
2110
+ ok: true,
2111
+ status: 200,
2112
+ text: async () => sseBody,
2113
+ headers: new Headers({ "content-type": "text/event-stream" }),
2114
+ });
2115
+
2116
+ const tokens: string[] = [];
2117
+ const result = await client.rawCompletionStreamWithProgress(
2118
+ { system_prompt: "test", message: "test" },
2119
+ (token) => tokens.push(token),
2120
+ );
2121
+
2122
+ expect(tokens).toEqual(["Hello", " World"]);
2123
+ expect(result.content).toBe("Hello World");
2124
+ });
2125
+
2126
+ it("throws on SSE error event", async () => {
2127
+ const client = createTestClient();
2128
+ mockTokenResponse();
2129
+
2130
+ const sseBody = 'data: {"error": "Model overloaded"}';
2131
+ mockFetch.mockResolvedValueOnce({
2132
+ ok: true,
2133
+ status: 200,
2134
+ text: async () => sseBody,
2135
+ headers: new Headers({ "content-type": "text/event-stream" }),
2136
+ });
2137
+
2138
+ const tokens: string[] = [];
2139
+ await expect(
2140
+ client.rawCompletionStreamWithProgress(
2141
+ { system_prompt: "test", message: "test" },
2142
+ (token) => tokens.push(token),
2143
+ ),
2144
+ ).rejects.toThrow("Model overloaded");
2145
+ });
2146
+ });
2147
+
2148
+ // ============================================================================
2149
+ // Goal Template CRUD Tests
2150
+ // ============================================================================
2151
+
2152
+ describe("EkoDBClient goal templates", () => {
2153
+ it("creates a goal template", async () => {
2154
+ const client = createTestClient();
2155
+ mockTokenResponse();
2156
+ mockJsonResponse({ id: "gt_1", title: "Deploy Checklist" });
2157
+
2158
+ const result = await client.goalTemplateCreate({
2159
+ title: "Deploy Checklist",
2160
+ steps: [{ title: "Run tests" }],
2161
+ });
2162
+ expect(result).toHaveProperty("id", "gt_1");
2163
+ expect(result).toHaveProperty("title", "Deploy Checklist");
2164
+ });
2165
+
2166
+ it("creates a goal template and verifies POST method", async () => {
2167
+ const client = createTestClient();
2168
+ mockTokenResponse();
2169
+ mockJsonResponse({ id: "gt_2", title: "Onboarding" });
2170
+
2171
+ await client.goalTemplateCreate({ title: "Onboarding" });
2172
+
2173
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2174
+ const dataCall = calls[1]; // calls[0] is token
2175
+ expect(dataCall[0]).toContain("/api/chat/goal-templates");
2176
+ expect(dataCall[1]?.method).toBe("POST");
2177
+ });
2178
+
2179
+ it("lists goal templates", async () => {
2180
+ const client = createTestClient();
2181
+ mockTokenResponse();
2182
+ mockJsonResponse({
2183
+ templates: [{ id: "gt_1" }, { id: "gt_2" }],
2184
+ });
2185
+
2186
+ const result = await client.goalTemplateList();
2187
+ expect(result).toHaveProperty("templates");
2188
+ });
2189
+
2190
+ it("lists goal templates and verifies GET method", async () => {
2191
+ const client = createTestClient();
2192
+ mockTokenResponse();
2193
+ mockJsonResponse({ templates: [] });
2194
+
2195
+ await client.goalTemplateList();
2196
+
2197
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2198
+ const dataCall = calls[1];
2199
+ expect(dataCall[0]).toContain("/api/chat/goal-templates");
2200
+ expect(dataCall[1]?.method).toBe("GET");
2201
+ });
2202
+
2203
+ it("gets a goal template by ID", async () => {
2204
+ const client = createTestClient();
2205
+ mockTokenResponse();
2206
+ mockJsonResponse({ id: "gt_1", title: "Deploy Checklist" });
2207
+
2208
+ const result = await client.goalTemplateGet("gt_1");
2209
+ expect(result).toHaveProperty("id", "gt_1");
2210
+ expect(result).toHaveProperty("title", "Deploy Checklist");
2211
+ });
2212
+
2213
+ it("gets a goal template and verifies endpoint", async () => {
2214
+ const client = createTestClient();
2215
+ mockTokenResponse();
2216
+ mockJsonResponse({ id: "gt_1", title: "Deploy Checklist" });
2217
+
2218
+ await client.goalTemplateGet("gt_1");
2219
+
2220
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2221
+ const dataCall = calls[1];
2222
+ expect(dataCall[0]).toContain("/api/chat/goal-templates/gt_1");
2223
+ expect(dataCall[1]?.method).toBe("GET");
2224
+ });
2225
+
2226
+ it("updates a goal template", async () => {
2227
+ const client = createTestClient();
2228
+ mockTokenResponse();
2229
+ mockJsonResponse({ id: "gt_1", title: "Updated Checklist" });
2230
+
2231
+ const result = await client.goalTemplateUpdate("gt_1", {
2232
+ title: "Updated Checklist",
2233
+ });
2234
+ expect(result).toHaveProperty("title", "Updated Checklist");
2235
+ });
2236
+
2237
+ it("updates a goal template and verifies PUT method", async () => {
2238
+ const client = createTestClient();
2239
+ mockTokenResponse();
2240
+ mockJsonResponse({ id: "gt_1", title: "Updated" });
2241
+
2242
+ await client.goalTemplateUpdate("gt_1", { title: "Updated" });
2243
+
2244
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2245
+ const dataCall = calls[1];
2246
+ expect(dataCall[0]).toContain("/api/chat/goal-templates/gt_1");
2247
+ expect(dataCall[1]?.method).toBe("PUT");
2248
+ });
2249
+
2250
+ it("deletes a goal template", async () => {
2251
+ const client = createTestClient();
2252
+ mockTokenResponse();
2253
+ mockJsonResponse({});
2254
+
2255
+ await expect(client.goalTemplateDelete("gt_1")).resolves.not.toThrow();
2256
+ });
2257
+
2258
+ it("deletes a goal template and verifies DELETE method", async () => {
2259
+ const client = createTestClient();
2260
+ mockTokenResponse();
2261
+ mockJsonResponse({});
2262
+
2263
+ await client.goalTemplateDelete("gt_1");
2264
+
2265
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2266
+ const dataCall = calls[1];
2267
+ expect(dataCall[0]).toContain("/api/chat/goal-templates/gt_1");
2268
+ expect(dataCall[1]?.method).toBe("DELETE");
2269
+ });
2270
+
2271
+ it("returns error for non-existent goal template", async () => {
2272
+ const client = createTestClient();
2273
+ mockTokenResponse();
2274
+ mockErrorResponse(404, "Not Found");
2275
+
2276
+ await expect(client.goalTemplateGet("nonexistent")).rejects.toThrow();
2277
+ });
2278
+ });
2279
+
2280
+ // ============================================================================
2281
+ // chatMessageStream (SSE) Tests
2282
+ // ============================================================================
2283
+
2284
+ describe("EkoDBClient chatMessageStream", () => {
2285
+ it("emits chunk and end events from SSE stream", async () => {
2286
+ const client = createTestClient();
2287
+ mockTokenResponse();
2288
+
2289
+ const sseBody = [
2290
+ 'data: {"token":"Hello"}',
2291
+ 'data: {"token":" world"}',
2292
+ 'data: {"content":"Hello world","message_id":"msg_1","execution_time_ms":42}',
2293
+ "",
2294
+ ].join("\n");
2295
+
2296
+ mockFetch.mockResolvedValueOnce({
2297
+ ok: true,
2298
+ status: 200,
2299
+ text: async () => sseBody,
2300
+ headers: new Headers({ "content-type": "text/event-stream" }),
2301
+ });
2302
+
2303
+ const events: any[] = [];
2304
+ const stream = client.chatMessageStream("chat_123", {
2305
+ message: "Hello",
2306
+ });
2307
+ stream.on("event", (evt: any) => events.push(evt));
2308
+
2309
+ // Wait for the async SSE processing to complete
2310
+ await new Promise((resolve) => setTimeout(resolve, 50));
2311
+
2312
+ expect(events).toHaveLength(3);
2313
+ expect(events[0]).toEqual({ type: "chunk", content: "Hello" });
2314
+ expect(events[1]).toEqual({ type: "chunk", content: " world" });
2315
+ expect(events[2].type).toBe("end");
2316
+ expect(events[2].messageId).toBe("msg_1");
2317
+ expect(events[2].executionTimeMs).toBe(42);
2318
+ });
2319
+
2320
+ it("emits error event on SSE error", async () => {
2321
+ const client = createTestClient();
2322
+ mockTokenResponse();
2323
+
2324
+ const sseBody = 'data: {"error":"LLM timeout"}\n';
2325
+
2326
+ mockFetch.mockResolvedValueOnce({
2327
+ ok: true,
2328
+ status: 200,
2329
+ text: async () => sseBody,
2330
+ headers: new Headers({ "content-type": "text/event-stream" }),
2331
+ });
2332
+
2333
+ const events: any[] = [];
2334
+ const stream = client.chatMessageStream("chat_123", {
2335
+ message: "Hello",
2336
+ });
2337
+ stream.on("event", (evt: any) => events.push(evt));
2338
+
2339
+ await new Promise((resolve) => setTimeout(resolve, 50));
2340
+
2341
+ expect(events).toHaveLength(1);
2342
+ expect(events[0]).toEqual({ type: "error", error: "LLM timeout" });
2343
+ });
2344
+
2345
+ it("emits error event on non-200 HTTP response", async () => {
2346
+ const client = createTestClient();
2347
+ mockTokenResponse();
2348
+
2349
+ mockFetch.mockResolvedValueOnce({
2350
+ ok: false,
2351
+ status: 401,
2352
+ text: async () => "Unauthorized",
2353
+ headers: new Headers(),
2354
+ });
2355
+
2356
+ const events: any[] = [];
2357
+ const stream = client.chatMessageStream("chat_123", {
2358
+ message: "Hello",
2359
+ });
2360
+ stream.on("event", (evt: any) => events.push(evt));
2361
+
2362
+ await new Promise((resolve) => setTimeout(resolve, 50));
2363
+
2364
+ expect(events).toHaveLength(1);
2365
+ expect(events[0].type).toBe("error");
2366
+ expect(events[0].error).toContain("401");
2367
+ });
2368
+
2369
+ it("calls the correct stream endpoint", async () => {
2370
+ const client = createTestClient();
2371
+ mockTokenResponse();
2372
+
2373
+ mockFetch.mockResolvedValueOnce({
2374
+ ok: true,
2375
+ status: 200,
2376
+ text: async () => 'data: {"token":"ok"}\n',
2377
+ headers: new Headers({ "content-type": "text/event-stream" }),
2378
+ });
2379
+
2380
+ client.chatMessageStream("chat_456", { message: "Test" });
2381
+
2382
+ await new Promise((resolve) => setTimeout(resolve, 50));
2383
+
2384
+ const calls = (global.fetch as ReturnType<typeof vi.fn>).mock.calls;
2385
+ const dataCall = calls[1];
2386
+ expect(dataCall[0]).toContain("/api/chat/chat_456/messages/stream");
2387
+ expect(dataCall[1]?.method).toBe("POST");
2388
+ expect(dataCall[1]?.headers?.Accept).toBe("text/event-stream");
2389
+ });
2390
+ });
2391
+
2392
+ // ============================================================================
2393
+ // Schedule CRUD Tests
2394
+ // ============================================================================
2395
+
2396
+ describe("EkoDBClient schedules", () => {
2397
+ it("creates a schedule", async () => {
2398
+ const client = createTestClient();
2399
+ mockTokenResponse();
2400
+ mockJsonResponse({
2401
+ id: "sched_1",
2402
+ name: "Nightly Backup",
2403
+ cron: "0 2 * * *",
2404
+ status: "active",
2405
+ });
2406
+
2407
+ const result = await client.createSchedule({
2408
+ name: "Nightly Backup",
2409
+ cron: "0 2 * * *",
2410
+ action: "backup",
2411
+ });
2412
+ expect(result).toHaveProperty("id", "sched_1");
2413
+ expect(result).toHaveProperty("status", "active");
2414
+ });
2415
+
2416
+ it("lists schedules", async () => {
2417
+ const client = createTestClient();
2418
+ mockTokenResponse();
2419
+ mockJsonResponse({
2420
+ schedules: [
2421
+ { id: "sched_1", name: "Nightly Backup" },
2422
+ { id: "sched_2", name: "Hourly Sync" },
2423
+ ],
2424
+ });
2425
+
2426
+ const result = await client.listSchedules();
2427
+ expect(result).toHaveProperty("schedules");
2428
+ });
2429
+
2430
+ it("gets a schedule by ID", async () => {
2431
+ const client = createTestClient();
2432
+ mockTokenResponse();
2433
+ mockJsonResponse({
2434
+ id: "sched_1",
2435
+ name: "Nightly Backup",
2436
+ cron: "0 2 * * *",
2437
+ });
2438
+
2439
+ const result = await client.getSchedule("sched_1");
2440
+ expect(result).toHaveProperty("id", "sched_1");
2441
+ expect(result).toHaveProperty("name", "Nightly Backup");
2442
+ });
2443
+
2444
+ it("updates a schedule", async () => {
2445
+ const client = createTestClient();
2446
+ mockTokenResponse();
2447
+ mockJsonResponse({
2448
+ id: "sched_1",
2449
+ name: "Updated Backup",
2450
+ cron: "0 3 * * *",
2451
+ });
2452
+
2453
+ const result = await client.updateSchedule("sched_1", {
2454
+ name: "Updated Backup",
2455
+ cron: "0 3 * * *",
2456
+ });
2457
+ expect(result).toHaveProperty("name", "Updated Backup");
2458
+ expect(result).toHaveProperty("cron", "0 3 * * *");
2459
+ });
2460
+
2461
+ it("deletes a schedule", async () => {
2462
+ const client = createTestClient();
2463
+ mockTokenResponse();
2464
+ mockJsonResponse({});
2465
+
2466
+ await expect(client.deleteSchedule("sched_1")).resolves.not.toThrow();
2467
+ });
2468
+
2469
+ it("pauses a schedule", async () => {
2470
+ const client = createTestClient();
2471
+ mockTokenResponse();
2472
+ mockJsonResponse({ id: "sched_1", status: "paused" });
2473
+
2474
+ const result = await client.pauseSchedule("sched_1");
2475
+ expect(result).toHaveProperty("status", "paused");
2476
+ });
2477
+
2478
+ it("resumes a schedule", async () => {
2479
+ const client = createTestClient();
2480
+ mockTokenResponse();
2481
+ mockJsonResponse({ id: "sched_1", status: "active" });
2482
+
2483
+ const result = await client.resumeSchedule("sched_1");
2484
+ expect(result).toHaveProperty("status", "active");
2485
+ });
2486
+ });
2487
+
2488
+ // ============================================================================
2489
+ // KV Links Tests
2490
+ // ============================================================================
2491
+
2492
+ describe("EkoDBClient kv links", () => {
2493
+ it("gets links for a KV key", async () => {
2494
+ const client = createTestClient();
2495
+ mockTokenResponse();
2496
+ mockJsonResponse({
2497
+ links: [
2498
+ { collection: "users", document_id: "user_1" },
2499
+ { collection: "orders", document_id: "order_1" },
2500
+ ],
2501
+ });
2502
+
2503
+ const result = await client.kvGetLinks("session:user123");
2504
+ expect(result).toHaveProperty("links");
2505
+ });
2506
+
2507
+ it("links a document to a KV key", async () => {
2508
+ const client = createTestClient();
2509
+ mockTokenResponse();
2510
+ mockJsonResponse({ status: "linked" });
2511
+
2512
+ const result = await client.kvLink("session:user123", "users", "user_1");
2513
+ expect(result).toHaveProperty("status", "linked");
2514
+ });
2515
+
2516
+ it("unlinks a document from a KV key", async () => {
2517
+ const client = createTestClient();
2518
+ mockTokenResponse();
2519
+ mockJsonResponse({ status: "unlinked" });
2520
+
2521
+ const result = await client.kvUnlink("session:user123", "users", "user_1");
2522
+ expect(result).toHaveProperty("status", "unlinked");
2523
+ });
2524
+ });
2525
+
2526
+ // ============================================================================
2527
+ // Text Search and Hybrid Search Tests
2528
+ // ============================================================================
2529
+
2530
+ describe("EkoDBClient text and hybrid search", () => {
2531
+ it("performs text search", async () => {
2532
+ const client = createTestClient();
2533
+ mockTokenResponse();
2534
+ mockJsonResponse({
2535
+ results: [
2536
+ {
2537
+ record: { id: "doc_1", title: "Ownership Guide" },
2538
+ score: 0.92,
2539
+ matched_fields: ["title"],
2540
+ },
2541
+ {
2542
+ record: { id: "doc_2", title: "Property Ownership" },
2543
+ score: 0.85,
2544
+ matched_fields: ["title"],
2545
+ },
2546
+ ],
2547
+ total: 2,
2548
+ took_ms: 12,
2549
+ });
2550
+
2551
+ const result = await client.textSearch("documents", "ownership", {
2552
+ limit: 10,
2553
+ select_fields: ["title", "content"],
2554
+ });
2555
+
2556
+ expect(result.results).toHaveLength(2);
2557
+ expect(result.total).toBe(2);
2558
+ expect(result.results[0].score).toBe(0.92);
2559
+ });
2560
+
2561
+ it("performs hybrid search", async () => {
2562
+ const client = createTestClient();
2563
+ mockTokenResponse();
2564
+ mockJsonResponse({
2565
+ results: [
2566
+ {
2567
+ record: { id: "doc_1", title: "ML Guide", content: "Learn ML" },
2568
+ score: 0.95,
2569
+ matched_fields: ["title", "content"],
2570
+ },
2571
+ ],
2572
+ total: 1,
2573
+ took_ms: 25,
2574
+ });
2575
+
2576
+ const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
2577
+ const result = await client.hybridSearch(
2578
+ "documents",
2579
+ "machine learning",
2580
+ queryVector,
2581
+ 5,
2582
+ );
2583
+
2584
+ expect(result).toHaveLength(1);
2585
+ expect(result[0]).toHaveProperty("id", "doc_1");
2586
+ expect(result[0]).toHaveProperty("title", "ML Guide");
2587
+ });
2588
+ });
2589
+
2590
+ // ============================================================================
2591
+ // Auth Token Management
2592
+ // ============================================================================
2593
+
2594
+ describe("EkoDBClient auth token management", () => {
2595
+ beforeEach(() => {
2596
+ mockFetch.mockReset();
2597
+ });
2598
+
2599
+ it("getToken auto-refreshes when token is about to expire", async () => {
2600
+ const client = createTestClient();
2601
+
2602
+ // First init — get a token with a short expiry (30s from now)
2603
+ const shortExp = Math.floor(Date.now() / 1000) + 30;
2604
+ const shortPayload = btoa(JSON.stringify({ exp: shortExp }))
2605
+ .replace(/\+/g, "-")
2606
+ .replace(/\//g, "_")
2607
+ .replace(/=/g, "");
2608
+ const shortJwt = `eyJhbGciOiJIUzI1NiJ9.${shortPayload}.fakesig`;
2609
+ mockFetch.mockResolvedValueOnce({
2610
+ ok: true,
2611
+ status: 200,
2612
+ json: async () => ({ token: shortJwt }),
2613
+ headers: new Headers(),
2614
+ });
2615
+ await client.init();
2616
+
2617
+ // Now getToken should see it's about to expire and refresh
2618
+ const newExp = Math.floor(Date.now() / 1000) + 3600;
2619
+ const newPayload = btoa(JSON.stringify({ exp: newExp }))
2620
+ .replace(/\+/g, "-")
2621
+ .replace(/\//g, "_")
2622
+ .replace(/=/g, "");
2623
+ const newJwt = `eyJhbGciOiJIUzI1NiJ9.${newPayload}.fakesig`;
2624
+ mockFetch.mockResolvedValueOnce({
2625
+ ok: true,
2626
+ status: 200,
2627
+ json: async () => ({ token: newJwt }),
2628
+ headers: new Headers(),
2629
+ });
2630
+
2631
+ const token = await client.getToken();
2632
+ expect(token).toBe(newJwt);
2633
+ // 2 fetch calls: init + proactive refresh
2634
+ expect(mockFetch).toHaveBeenCalledTimes(2);
2635
+ });
2636
+
2637
+ it("getToken returns cached token when not expired", async () => {
2638
+ const client = createTestClient();
2639
+
2640
+ // Token with expiry far in the future
2641
+ const farExp = Math.floor(Date.now() / 1000) + 7200;
2642
+ const payload = btoa(JSON.stringify({ exp: farExp }))
2643
+ .replace(/\+/g, "-")
2644
+ .replace(/\//g, "_")
2645
+ .replace(/=/g, "");
2646
+ const jwt = `eyJhbGciOiJIUzI1NiJ9.${payload}.fakesig`;
2647
+ mockFetch.mockResolvedValueOnce({
2648
+ ok: true,
2649
+ status: 200,
2650
+ json: async () => ({ token: jwt }),
2651
+ headers: new Headers(),
2652
+ });
2653
+ await client.init();
2654
+
2655
+ // getToken should return cached — no extra fetch
2656
+ const token = await client.getToken();
2657
+ expect(token).toBe(jwt);
2658
+ expect(mockFetch).toHaveBeenCalledTimes(1); // only init
2659
+ });
2660
+
2661
+ it("clearTokenCache resets token and expiry", async () => {
2662
+ const client = createTestClient();
2663
+ mockTokenResponse();
2664
+ await client.init();
2665
+ expect(await client.getToken()).toBeTruthy();
2666
+
2667
+ client.clearTokenCache();
2668
+ // After clear, getToken will auto-refresh (fetch a new token)
2669
+ mockTokenResponse();
2670
+ const token = await client.getToken();
2671
+ expect(token).toBe("test-jwt-token");
2672
+ // 2 fetch calls: init + post-clear getToken refresh
2673
+ expect(mockFetch).toHaveBeenCalledTimes(2);
2674
+ });
2675
+ });