@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.
- package/README.md +70 -0
- package/dist/client.d.ts +157 -4
- package/dist/client.js +523 -7
- package/dist/client.test.js +861 -0
- package/dist/websocket.test.js +89 -0
- package/package.json +1 -1
- package/src/client.test.ts +1090 -0
- package/src/client.ts +872 -7
- package/src/websocket.test.ts +137 -0
package/src/client.test.ts
CHANGED
|
@@ -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
|
+
});
|