@ekodb/ekodb-client 0.15.2 → 0.17.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/dist/client.d.ts +34 -30
- package/dist/client.js +117 -12
- package/dist/client.test.js +204 -3
- package/dist/functions.d.ts +192 -12
- package/dist/functions.js +118 -1
- package/dist/functions.test.d.ts +9 -0
- package/dist/functions.test.js +327 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/client.test.ts +243 -3
- package/src/client.ts +155 -30
- package/src/functions.test.ts +444 -0
- package/src/functions.ts +276 -13
- package/src/index.ts +3 -3
package/dist/client.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { QueryBuilder } from "./query-builder";
|
|
5
5
|
import { SearchQuery, SearchResponse } from "./search";
|
|
6
6
|
import { Schema, SchemaBuilder, CollectionMetadata } from "./schema";
|
|
7
|
-
import {
|
|
7
|
+
import { UserFunction, FunctionResult } from "./functions";
|
|
8
8
|
export interface Record {
|
|
9
9
|
[key: string]: any;
|
|
10
10
|
}
|
|
@@ -156,6 +156,7 @@ export interface CreateChatSessionRequest {
|
|
|
156
156
|
llm_provider: string;
|
|
157
157
|
llm_model?: string;
|
|
158
158
|
system_prompt?: string;
|
|
159
|
+
agent_id?: string;
|
|
159
160
|
bypass_ripple?: boolean;
|
|
160
161
|
parent_id?: string;
|
|
161
162
|
branch_point_idx?: number;
|
|
@@ -171,6 +172,9 @@ export interface ChatMessageRequest {
|
|
|
171
172
|
max_iterations?: number;
|
|
172
173
|
tool_config?: ToolConfig;
|
|
173
174
|
llm_model?: string;
|
|
175
|
+
client_tools?: ClientToolDefinition[];
|
|
176
|
+
confirm_tools?: string[];
|
|
177
|
+
exclude_tools?: string[];
|
|
174
178
|
}
|
|
175
179
|
export interface TokenUsage {
|
|
176
180
|
prompt_tokens: number;
|
|
@@ -193,6 +197,7 @@ export interface ChatSession {
|
|
|
193
197
|
llm_model: string;
|
|
194
198
|
collections: CollectionConfig[];
|
|
195
199
|
system_prompt?: string;
|
|
200
|
+
agent_id?: string;
|
|
196
201
|
title?: string;
|
|
197
202
|
message_count: number;
|
|
198
203
|
}
|
|
@@ -285,23 +290,6 @@ export interface RawCompletionRequest {
|
|
|
285
290
|
export interface RawCompletionResponse {
|
|
286
291
|
content: string;
|
|
287
292
|
}
|
|
288
|
-
/**
|
|
289
|
-
* User function definition - reusable sequence of Functions that can be called by Scripts
|
|
290
|
-
*/
|
|
291
|
-
export interface UserFunction {
|
|
292
|
-
label: string;
|
|
293
|
-
name: string;
|
|
294
|
-
description?: string;
|
|
295
|
-
version?: string;
|
|
296
|
-
parameters: {
|
|
297
|
-
[key: string]: ParameterDefinition;
|
|
298
|
-
};
|
|
299
|
-
functions: FunctionStageConfig[];
|
|
300
|
-
tags?: string[];
|
|
301
|
-
id?: string;
|
|
302
|
-
created_at?: string;
|
|
303
|
-
updated_at?: string;
|
|
304
|
-
}
|
|
305
293
|
/**
|
|
306
294
|
* Parameter definition for functions
|
|
307
295
|
*/
|
|
@@ -821,6 +809,11 @@ export declare class EkoDBClient {
|
|
|
821
809
|
* Send a message in an existing chat session
|
|
822
810
|
*/
|
|
823
811
|
chatMessage(sessionId: string, request: ChatMessageRequest): Promise<ChatResponse>;
|
|
812
|
+
/**
|
|
813
|
+
* Submit a client tool result for an in-flight SSE chat stream.
|
|
814
|
+
* Unblocks ekoDB's tool loop so it can feed the result to the LLM.
|
|
815
|
+
*/
|
|
816
|
+
submitChatToolResult(chatId: string, callId: string, success: boolean, result?: any, error?: string): Promise<void>;
|
|
824
817
|
/**
|
|
825
818
|
* Send a message in an existing chat session via SSE streaming.
|
|
826
819
|
*
|
|
@@ -902,29 +895,29 @@ export declare class EkoDBClient {
|
|
|
902
895
|
*/
|
|
903
896
|
getChatMessage(sessionId: string, messageId: string): Promise<Record>;
|
|
904
897
|
/**
|
|
905
|
-
* Save a new
|
|
898
|
+
* Save a new function definition
|
|
906
899
|
*/
|
|
907
|
-
|
|
900
|
+
saveFunction(script: UserFunction): Promise<string>;
|
|
908
901
|
/**
|
|
909
|
-
* Get a
|
|
902
|
+
* Get a function by ID
|
|
910
903
|
*/
|
|
911
|
-
|
|
904
|
+
getFunction(id: string): Promise<UserFunction>;
|
|
912
905
|
/**
|
|
913
|
-
* List all
|
|
906
|
+
* List all functions, optionally filtered by tags
|
|
914
907
|
*/
|
|
915
|
-
|
|
908
|
+
listFunctions(tags?: string[]): Promise<UserFunction[]>;
|
|
916
909
|
/**
|
|
917
|
-
* Update an existing
|
|
910
|
+
* Update an existing function by ID
|
|
918
911
|
*/
|
|
919
|
-
|
|
912
|
+
updateFunction(id: string, script: UserFunction): Promise<void>;
|
|
920
913
|
/**
|
|
921
|
-
* Delete a
|
|
914
|
+
* Delete a function by ID
|
|
922
915
|
*/
|
|
923
|
-
|
|
916
|
+
deleteFunction(id: string): Promise<void>;
|
|
924
917
|
/**
|
|
925
|
-
* Call a saved
|
|
918
|
+
* Call a saved function by ID or label
|
|
926
919
|
*/
|
|
927
|
-
|
|
920
|
+
callFunction(idOrLabel: string, params?: {
|
|
928
921
|
[key: string]: any;
|
|
929
922
|
}): Promise<FunctionResult>;
|
|
930
923
|
/**
|
|
@@ -1058,6 +1051,17 @@ export declare class EkoDBClient {
|
|
|
1058
1051
|
* @returns Number of documents in the collection
|
|
1059
1052
|
*/
|
|
1060
1053
|
countDocuments(collection: string): Promise<number>;
|
|
1054
|
+
/**
|
|
1055
|
+
* Subscribe to collection mutations via SSE (Server-Sent Events).
|
|
1056
|
+
*
|
|
1057
|
+
* Returns an EventStream that emits MutationNotification events.
|
|
1058
|
+
* Use this when WebSocket connections aren't available (e.g. behind
|
|
1059
|
+
* reverse proxies that block WS upgrades).
|
|
1060
|
+
*/
|
|
1061
|
+
subscribeSSE(collection: string, options?: {
|
|
1062
|
+
filterField?: string;
|
|
1063
|
+
filterValue?: string;
|
|
1064
|
+
}): EventStream<MutationNotification>;
|
|
1061
1065
|
/**
|
|
1062
1066
|
* Create a WebSocket client
|
|
1063
1067
|
*/
|
package/dist/client.js
CHANGED
|
@@ -1098,6 +1098,18 @@ class EkoDBClient {
|
|
|
1098
1098
|
async chatMessage(sessionId, request) {
|
|
1099
1099
|
return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
|
|
1100
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Submit a client tool result for an in-flight SSE chat stream.
|
|
1103
|
+
* Unblocks ekoDB's tool loop so it can feed the result to the LLM.
|
|
1104
|
+
*/
|
|
1105
|
+
async submitChatToolResult(chatId, callId, success, result, error) {
|
|
1106
|
+
await this.makeRequest("POST", `/api/chat/${chatId}/tool-result`, {
|
|
1107
|
+
call_id: callId,
|
|
1108
|
+
success,
|
|
1109
|
+
...(result !== undefined && { result }),
|
|
1110
|
+
...(error !== undefined && { error }),
|
|
1111
|
+
}, 0, true);
|
|
1112
|
+
}
|
|
1101
1113
|
/**
|
|
1102
1114
|
* Send a message in an existing chat session via SSE streaming.
|
|
1103
1115
|
*
|
|
@@ -1307,41 +1319,41 @@ class EkoDBClient {
|
|
|
1307
1319
|
// SCRIPTS API
|
|
1308
1320
|
// ========================================================================
|
|
1309
1321
|
/**
|
|
1310
|
-
* Save a new
|
|
1322
|
+
* Save a new function definition
|
|
1311
1323
|
*/
|
|
1312
|
-
async
|
|
1324
|
+
async saveFunction(script) {
|
|
1313
1325
|
const result = await this.makeRequest("POST", "/api/functions", script);
|
|
1314
1326
|
return result.id;
|
|
1315
1327
|
}
|
|
1316
1328
|
/**
|
|
1317
|
-
* Get a
|
|
1329
|
+
* Get a function by ID
|
|
1318
1330
|
*/
|
|
1319
|
-
async
|
|
1331
|
+
async getFunction(id) {
|
|
1320
1332
|
return this.makeRequest("GET", `/api/functions/${id}`);
|
|
1321
1333
|
}
|
|
1322
1334
|
/**
|
|
1323
|
-
* List all
|
|
1335
|
+
* List all functions, optionally filtered by tags
|
|
1324
1336
|
*/
|
|
1325
|
-
async
|
|
1337
|
+
async listFunctions(tags) {
|
|
1326
1338
|
const params = tags ? `?tags=${tags.join(",")}` : "";
|
|
1327
1339
|
return this.makeRequest("GET", `/api/functions${params}`);
|
|
1328
1340
|
}
|
|
1329
1341
|
/**
|
|
1330
|
-
* Update an existing
|
|
1342
|
+
* Update an existing function by ID
|
|
1331
1343
|
*/
|
|
1332
|
-
async
|
|
1344
|
+
async updateFunction(id, script) {
|
|
1333
1345
|
await this.makeRequest("PUT", `/api/functions/${id}`, script);
|
|
1334
1346
|
}
|
|
1335
1347
|
/**
|
|
1336
|
-
* Delete a
|
|
1348
|
+
* Delete a function by ID
|
|
1337
1349
|
*/
|
|
1338
|
-
async
|
|
1350
|
+
async deleteFunction(id) {
|
|
1339
1351
|
await this.makeRequest("DELETE", `/api/functions/${id}`);
|
|
1340
1352
|
}
|
|
1341
1353
|
/**
|
|
1342
|
-
* Call a saved
|
|
1354
|
+
* Call a saved function by ID or label
|
|
1343
1355
|
*/
|
|
1344
|
-
async
|
|
1356
|
+
async callFunction(idOrLabel, params) {
|
|
1345
1357
|
return this.makeRequest("POST", `/api/functions/${idOrLabel}`, params || {});
|
|
1346
1358
|
}
|
|
1347
1359
|
// ========================================================================
|
|
@@ -1613,6 +1625,99 @@ class EkoDBClient {
|
|
|
1613
1625
|
const records = await this.find(collection, query);
|
|
1614
1626
|
return records.length;
|
|
1615
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Subscribe to collection mutations via SSE (Server-Sent Events).
|
|
1630
|
+
*
|
|
1631
|
+
* Returns an EventStream that emits MutationNotification events.
|
|
1632
|
+
* Use this when WebSocket connections aren't available (e.g. behind
|
|
1633
|
+
* reverse proxies that block WS upgrades).
|
|
1634
|
+
*/
|
|
1635
|
+
subscribeSSE(collection, options) {
|
|
1636
|
+
const stream = new EventStream();
|
|
1637
|
+
(async () => {
|
|
1638
|
+
try {
|
|
1639
|
+
let token = await this.getToken();
|
|
1640
|
+
if (!token) {
|
|
1641
|
+
await this.refreshToken();
|
|
1642
|
+
token = await this.getToken();
|
|
1643
|
+
}
|
|
1644
|
+
let url = `${this.baseURL}/api/subscribe/${encodeURIComponent(collection)}`;
|
|
1645
|
+
const params = [];
|
|
1646
|
+
if (options?.filterField)
|
|
1647
|
+
params.push(`filter_field=${encodeURIComponent(options.filterField)}`);
|
|
1648
|
+
if (options?.filterValue)
|
|
1649
|
+
params.push(`filter_value=${encodeURIComponent(options.filterValue)}`);
|
|
1650
|
+
if (params.length > 0)
|
|
1651
|
+
url += `?${params.join("&")}`;
|
|
1652
|
+
const response = await fetch(url, {
|
|
1653
|
+
method: "GET",
|
|
1654
|
+
headers: {
|
|
1655
|
+
Accept: "text/event-stream",
|
|
1656
|
+
Authorization: `Bearer ${token}`,
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
if (!response.ok) {
|
|
1660
|
+
const body = await response.text();
|
|
1661
|
+
stream.emit("error", `SSE subscribe failed (${response.status}): ${body}`);
|
|
1662
|
+
stream.close();
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const reader = response.body?.getReader();
|
|
1666
|
+
if (!reader) {
|
|
1667
|
+
stream.emit("error", "SSE subscribe failed: streaming not supported");
|
|
1668
|
+
stream.close();
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const decoder = new TextDecoder("utf-8");
|
|
1672
|
+
let buffer = "";
|
|
1673
|
+
let eventType = "";
|
|
1674
|
+
let dataLines = [];
|
|
1675
|
+
while (!stream.closed) {
|
|
1676
|
+
const { value, done } = await reader.read();
|
|
1677
|
+
if (done)
|
|
1678
|
+
break;
|
|
1679
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1680
|
+
const lines = buffer.split("\n");
|
|
1681
|
+
buffer = lines.pop() ?? "";
|
|
1682
|
+
for (const line of lines) {
|
|
1683
|
+
if (line === "") {
|
|
1684
|
+
// End of event block
|
|
1685
|
+
if (eventType === "mutation" && dataLines.length > 0) {
|
|
1686
|
+
try {
|
|
1687
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
1688
|
+
stream.emit("event", {
|
|
1689
|
+
collection: payload.collection,
|
|
1690
|
+
event: payload.event,
|
|
1691
|
+
recordIds: payload.record_ids,
|
|
1692
|
+
records: payload.records,
|
|
1693
|
+
timestamp: payload.timestamp,
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
// skip malformed data
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
eventType = "";
|
|
1701
|
+
dataLines = [];
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (line.startsWith("event: ")) {
|
|
1705
|
+
eventType = line.slice(7).trim();
|
|
1706
|
+
}
|
|
1707
|
+
else if (line.startsWith("data: ")) {
|
|
1708
|
+
dataLines.push(line.slice(6).trim());
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
stream.close();
|
|
1713
|
+
}
|
|
1714
|
+
catch (err) {
|
|
1715
|
+
stream.emit("error", err.message ?? String(err));
|
|
1716
|
+
stream.close();
|
|
1717
|
+
}
|
|
1718
|
+
})();
|
|
1719
|
+
return stream;
|
|
1720
|
+
}
|
|
1616
1721
|
/**
|
|
1617
1722
|
* Create a WebSocket client
|
|
1618
1723
|
*/
|
package/dist/client.test.js
CHANGED
|
@@ -590,14 +590,14 @@ function mockErrorResponse(status, message) {
|
|
|
590
590
|
parameters: {},
|
|
591
591
|
functions: [],
|
|
592
592
|
};
|
|
593
|
-
const result = await client.
|
|
593
|
+
const result = await client.saveFunction(script);
|
|
594
594
|
(0, vitest_1.expect)(result).toBeDefined();
|
|
595
595
|
});
|
|
596
596
|
(0, vitest_1.it)("gets script by ID", async () => {
|
|
597
597
|
const client = createTestClient();
|
|
598
598
|
mockTokenResponse();
|
|
599
599
|
mockJsonResponse({ id: "func_123", label: "my_function" });
|
|
600
|
-
const result = await client.
|
|
600
|
+
const result = await client.getFunction("func_123");
|
|
601
601
|
(0, vitest_1.expect)(result).toBeDefined();
|
|
602
602
|
});
|
|
603
603
|
(0, vitest_1.it)("updates script", async () => {
|
|
@@ -610,7 +610,7 @@ function mockErrorResponse(status, message) {
|
|
|
610
610
|
parameters: {},
|
|
611
611
|
functions: [],
|
|
612
612
|
};
|
|
613
|
-
await (0, vitest_1.expect)(client.
|
|
613
|
+
await (0, vitest_1.expect)(client.updateFunction("func_123", script)).resolves.not.toThrow();
|
|
614
614
|
});
|
|
615
615
|
});
|
|
616
616
|
(0, vitest_1.describe)("EkoDBClient chat advanced", () => {
|
|
@@ -2023,3 +2023,204 @@ function mockErrorResponse(status, message) {
|
|
|
2023
2023
|
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
|
|
2024
2024
|
});
|
|
2025
2025
|
});
|
|
2026
|
+
// ============================================================================
|
|
2027
|
+
// agent_id Tests
|
|
2028
|
+
// ============================================================================
|
|
2029
|
+
(0, vitest_1.describe)("agent_id on chat types", () => {
|
|
2030
|
+
(0, vitest_1.it)("CreateChatSessionRequest includes agent_id", () => {
|
|
2031
|
+
const req = {
|
|
2032
|
+
collections: [{ collection_name: "docs" }],
|
|
2033
|
+
llm_provider: "openai",
|
|
2034
|
+
agent_id: "my-agent",
|
|
2035
|
+
};
|
|
2036
|
+
(0, vitest_1.expect)(req.agent_id).toBe("my-agent");
|
|
2037
|
+
});
|
|
2038
|
+
(0, vitest_1.it)("CreateChatSessionRequest omits agent_id when undefined", () => {
|
|
2039
|
+
const req = {
|
|
2040
|
+
collections: [],
|
|
2041
|
+
llm_provider: "openai",
|
|
2042
|
+
};
|
|
2043
|
+
(0, vitest_1.expect)(req.agent_id).toBeUndefined();
|
|
2044
|
+
});
|
|
2045
|
+
(0, vitest_1.it)("ChatSession includes agent_id", () => {
|
|
2046
|
+
const session = {
|
|
2047
|
+
chat_id: "c1",
|
|
2048
|
+
created_at: "2026-01-01",
|
|
2049
|
+
updated_at: "2026-01-01",
|
|
2050
|
+
llm_provider: "openai",
|
|
2051
|
+
llm_model: "gpt-4",
|
|
2052
|
+
collections: [],
|
|
2053
|
+
agent_id: "bot-1",
|
|
2054
|
+
message_count: 0,
|
|
2055
|
+
};
|
|
2056
|
+
(0, vitest_1.expect)(session.agent_id).toBe("bot-1");
|
|
2057
|
+
});
|
|
2058
|
+
(0, vitest_1.it)("ChatSession allows missing agent_id", () => {
|
|
2059
|
+
const session = {
|
|
2060
|
+
chat_id: "c1",
|
|
2061
|
+
created_at: "2026-01-01",
|
|
2062
|
+
updated_at: "2026-01-01",
|
|
2063
|
+
llm_provider: "openai",
|
|
2064
|
+
llm_model: "gpt-4",
|
|
2065
|
+
collections: [],
|
|
2066
|
+
message_count: 0,
|
|
2067
|
+
};
|
|
2068
|
+
(0, vitest_1.expect)(session.agent_id).toBeUndefined();
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
// ============================================================================
|
|
2072
|
+
// client_tools / confirm_tools / exclude_tools Tests
|
|
2073
|
+
// ============================================================================
|
|
2074
|
+
(0, vitest_1.describe)("ChatMessageRequest tool fields", () => {
|
|
2075
|
+
(0, vitest_1.it)("includes client_tools, confirm_tools, exclude_tools", () => {
|
|
2076
|
+
const req = {
|
|
2077
|
+
message: "hello",
|
|
2078
|
+
client_tools: [
|
|
2079
|
+
{
|
|
2080
|
+
name: "weather",
|
|
2081
|
+
description: "Get weather",
|
|
2082
|
+
parameters: { type: "object" },
|
|
2083
|
+
},
|
|
2084
|
+
],
|
|
2085
|
+
confirm_tools: ["shell_exec"],
|
|
2086
|
+
exclude_tools: ["file_delete"],
|
|
2087
|
+
};
|
|
2088
|
+
(0, vitest_1.expect)(req.client_tools).toHaveLength(1);
|
|
2089
|
+
(0, vitest_1.expect)(req.client_tools[0].name).toBe("weather");
|
|
2090
|
+
(0, vitest_1.expect)(req.confirm_tools).toEqual(["shell_exec"]);
|
|
2091
|
+
(0, vitest_1.expect)(req.exclude_tools).toEqual(["file_delete"]);
|
|
2092
|
+
});
|
|
2093
|
+
(0, vitest_1.it)("tool fields are optional", () => {
|
|
2094
|
+
const req = { message: "hi" };
|
|
2095
|
+
(0, vitest_1.expect)(req.client_tools).toBeUndefined();
|
|
2096
|
+
(0, vitest_1.expect)(req.confirm_tools).toBeUndefined();
|
|
2097
|
+
(0, vitest_1.expect)(req.exclude_tools).toBeUndefined();
|
|
2098
|
+
});
|
|
2099
|
+
(0, vitest_1.it)("ClientToolDefinition has correct shape", () => {
|
|
2100
|
+
const tool = {
|
|
2101
|
+
name: "calc",
|
|
2102
|
+
description: "Calculator",
|
|
2103
|
+
parameters: { type: "object", properties: {} },
|
|
2104
|
+
};
|
|
2105
|
+
(0, vitest_1.expect)(tool.name).toBe("calc");
|
|
2106
|
+
(0, vitest_1.expect)(tool.description).toBe("Calculator");
|
|
2107
|
+
(0, vitest_1.expect)(tool.parameters.type).toBe("object");
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
// ============================================================================
|
|
2111
|
+
// submitChatToolResult Tests
|
|
2112
|
+
// ============================================================================
|
|
2113
|
+
(0, vitest_1.describe)("submitChatToolResult", () => {
|
|
2114
|
+
(0, vitest_1.it)("sends tool result to correct endpoint", async () => {
|
|
2115
|
+
const client = createTestClient();
|
|
2116
|
+
mockTokenResponse();
|
|
2117
|
+
mockJsonResponse({});
|
|
2118
|
+
await client.submitChatToolResult("chat-123", "call-456", true, {
|
|
2119
|
+
temp: "72F",
|
|
2120
|
+
});
|
|
2121
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
|
|
2122
|
+
const call = mockFetch.mock.calls[1];
|
|
2123
|
+
(0, vitest_1.expect)(call[0]).toContain("/api/chat/chat-123/tool-result");
|
|
2124
|
+
const body = JSON.parse(call[1].body);
|
|
2125
|
+
(0, vitest_1.expect)(body.call_id).toBe("call-456");
|
|
2126
|
+
(0, vitest_1.expect)(body.success).toBe(true);
|
|
2127
|
+
(0, vitest_1.expect)(body.result.temp).toBe("72F");
|
|
2128
|
+
});
|
|
2129
|
+
(0, vitest_1.it)("sends error result", async () => {
|
|
2130
|
+
const client = createTestClient();
|
|
2131
|
+
mockTokenResponse();
|
|
2132
|
+
mockJsonResponse({});
|
|
2133
|
+
await client.submitChatToolResult("chat-123", "call-456", false, undefined, "tool crashed");
|
|
2134
|
+
const call = mockFetch.mock.calls[1];
|
|
2135
|
+
const body = JSON.parse(call[1].body);
|
|
2136
|
+
(0, vitest_1.expect)(body.success).toBe(false);
|
|
2137
|
+
(0, vitest_1.expect)(body.error).toBe("tool crashed");
|
|
2138
|
+
(0, vitest_1.expect)(body.result).toBeUndefined();
|
|
2139
|
+
});
|
|
2140
|
+
});
|
|
2141
|
+
// ============================================================================
|
|
2142
|
+
// subscribeSSE Tests
|
|
2143
|
+
// ============================================================================
|
|
2144
|
+
(0, vitest_1.describe)("subscribeSSE", () => {
|
|
2145
|
+
/** Create a mock ReadableStream from a string */
|
|
2146
|
+
function mockReadableStream(data) {
|
|
2147
|
+
const encoder = new TextEncoder();
|
|
2148
|
+
const bytes = encoder.encode(data);
|
|
2149
|
+
let sent = false;
|
|
2150
|
+
return {
|
|
2151
|
+
getReader: () => ({
|
|
2152
|
+
read: async () => {
|
|
2153
|
+
if (!sent) {
|
|
2154
|
+
sent = true;
|
|
2155
|
+
return { value: bytes, done: false };
|
|
2156
|
+
}
|
|
2157
|
+
return { value: undefined, done: true };
|
|
2158
|
+
},
|
|
2159
|
+
}),
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
(0, vitest_1.it)("parses mutation events from SSE stream", async () => {
|
|
2163
|
+
const client = createTestClient();
|
|
2164
|
+
mockTokenResponse();
|
|
2165
|
+
const sseBody = "event: subscribed\ndata: {}\n\n" +
|
|
2166
|
+
'event: mutation\ndata: {"collection":"orders","event":"insert","record_ids":["r1"],"timestamp":"t1"}\n\n' +
|
|
2167
|
+
'event: mutation\ndata: {"collection":"orders","event":"update","record_ids":["r2"],"timestamp":"t2"}\n\n';
|
|
2168
|
+
mockFetch.mockResolvedValueOnce({
|
|
2169
|
+
ok: true,
|
|
2170
|
+
status: 200,
|
|
2171
|
+
body: mockReadableStream(sseBody),
|
|
2172
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2173
|
+
});
|
|
2174
|
+
const stream = client.subscribeSSE("orders");
|
|
2175
|
+
const events = [];
|
|
2176
|
+
await new Promise((resolve) => {
|
|
2177
|
+
stream.on("event", (e) => events.push(e));
|
|
2178
|
+
stream.on("close", resolve);
|
|
2179
|
+
setTimeout(resolve, 100);
|
|
2180
|
+
});
|
|
2181
|
+
(0, vitest_1.expect)(events).toHaveLength(2);
|
|
2182
|
+
(0, vitest_1.expect)(events[0].event).toBe("insert");
|
|
2183
|
+
(0, vitest_1.expect)(events[0].recordIds).toEqual(["r1"]);
|
|
2184
|
+
(0, vitest_1.expect)(events[1].event).toBe("update");
|
|
2185
|
+
(0, vitest_1.expect)(events[1].recordIds).toEqual(["r2"]);
|
|
2186
|
+
});
|
|
2187
|
+
(0, vitest_1.it)("passes filter params in URL", async () => {
|
|
2188
|
+
const client = createTestClient();
|
|
2189
|
+
mockTokenResponse();
|
|
2190
|
+
mockFetch.mockResolvedValueOnce({
|
|
2191
|
+
ok: true,
|
|
2192
|
+
status: 200,
|
|
2193
|
+
body: mockReadableStream(""),
|
|
2194
|
+
headers: new Headers({ "content-type": "text/event-stream" }),
|
|
2195
|
+
});
|
|
2196
|
+
client.subscribeSSE("orders", {
|
|
2197
|
+
filterField: "status",
|
|
2198
|
+
filterValue: "active",
|
|
2199
|
+
});
|
|
2200
|
+
// Wait for async fetch
|
|
2201
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2202
|
+
const call = mockFetch.mock.calls[1];
|
|
2203
|
+
(0, vitest_1.expect)(call[0]).toContain("/api/subscribe/orders");
|
|
2204
|
+
(0, vitest_1.expect)(call[0]).toContain("filter_field=status");
|
|
2205
|
+
(0, vitest_1.expect)(call[0]).toContain("filter_value=active");
|
|
2206
|
+
});
|
|
2207
|
+
(0, vitest_1.it)("emits error on HTTP failure", async () => {
|
|
2208
|
+
const client = createTestClient();
|
|
2209
|
+
mockTokenResponse();
|
|
2210
|
+
mockFetch.mockResolvedValueOnce({
|
|
2211
|
+
ok: false,
|
|
2212
|
+
status: 401,
|
|
2213
|
+
text: async () => "Unauthorized",
|
|
2214
|
+
headers: new Headers(),
|
|
2215
|
+
});
|
|
2216
|
+
const stream = client.subscribeSSE("orders");
|
|
2217
|
+
const errors = [];
|
|
2218
|
+
await new Promise((resolve) => {
|
|
2219
|
+
stream.on("error", (e) => errors.push(e));
|
|
2220
|
+
stream.on("close", resolve);
|
|
2221
|
+
setTimeout(resolve, 100);
|
|
2222
|
+
});
|
|
2223
|
+
(0, vitest_1.expect)(errors).toHaveLength(1);
|
|
2224
|
+
(0, vitest_1.expect)(errors[0]).toContain("401");
|
|
2225
|
+
});
|
|
2226
|
+
});
|