@lobu/worker 6.1.1 → 7.1.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/core/error-handler.d.ts +0 -4
- package/dist/core/error-handler.d.ts.map +1 -1
- package/dist/core/error-handler.js +4 -15
- package/dist/core/error-handler.js.map +1 -1
- package/dist/core/types.d.ts +1 -19
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/workspace.d.ts +2 -11
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +14 -36
- package/dist/core/workspace.js.map +1 -1
- package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
- package/dist/embedded/just-bash-bootstrap.js +60 -6
- package/dist/embedded/just-bash-bootstrap.js.map +1 -1
- package/dist/embedded/mcp-cli-commands.d.ts.map +1 -1
- package/dist/embedded/mcp-cli-commands.js +3 -38
- package/dist/embedded/mcp-cli-commands.js.map +1 -1
- package/dist/gateway/gateway-integration.js +4 -4
- package/dist/gateway/gateway-integration.js.map +1 -1
- package/dist/gateway/message-batcher.d.ts.map +1 -1
- package/dist/gateway/message-batcher.js +3 -5
- package/dist/gateway/message-batcher.js.map +1 -1
- package/dist/gateway/sse-client.d.ts +1 -0
- package/dist/gateway/sse-client.d.ts.map +1 -1
- package/dist/gateway/sse-client.js +52 -8
- package/dist/gateway/sse-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -24
- package/dist/index.js.map +1 -1
- package/dist/instructions/builder.d.ts.map +1 -1
- package/dist/instructions/builder.js +2 -1
- package/dist/instructions/builder.js.map +1 -1
- package/dist/openclaw/plugin-loader.d.ts.map +1 -1
- package/dist/openclaw/plugin-loader.js +8 -19
- package/dist/openclaw/plugin-loader.js.map +1 -1
- package/dist/openclaw/processor.d.ts.map +1 -1
- package/dist/openclaw/processor.js +2 -0
- package/dist/openclaw/processor.js.map +1 -1
- package/dist/openclaw/sandbox-leak.d.ts.map +1 -1
- package/dist/openclaw/sandbox-leak.js +1 -6
- package/dist/openclaw/sandbox-leak.js.map +1 -1
- package/dist/openclaw/session-context.d.ts.map +1 -1
- package/dist/openclaw/session-context.js +3 -0
- package/dist/openclaw/session-context.js.map +1 -1
- package/dist/openclaw/tool-policy.d.ts.map +1 -1
- package/dist/openclaw/tool-policy.js +5 -11
- package/dist/openclaw/tool-policy.js.map +1 -1
- package/dist/openclaw/worker.d.ts +0 -1
- package/dist/openclaw/worker.d.ts.map +1 -1
- package/dist/openclaw/worker.js +19 -85
- package/dist/openclaw/worker.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -40
- package/dist/server.js.map +1 -1
- package/dist/shared/audio-provider-suggestions.d.ts.map +1 -1
- package/dist/shared/audio-provider-suggestions.js +4 -6
- package/dist/shared/audio-provider-suggestions.js.map +1 -1
- package/dist/shared/tool-implementations.d.ts.map +1 -1
- package/dist/shared/tool-implementations.js +99 -37
- package/dist/shared/tool-implementations.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
- package/src/__tests__/custom-tools.test.ts +92 -0
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
- package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
- package/src/__tests__/embedded-tools.test.ts +744 -0
- package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
- package/src/__tests__/exec-sandbox.test.ts +550 -0
- package/src/__tests__/generated-media.test.ts +142 -0
- package/src/__tests__/instructions.test.ts +60 -0
- package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
- package/src/__tests__/mcp-cli-commands.test.ts +383 -0
- package/src/__tests__/mcp-tool-call.test.ts +423 -0
- package/src/__tests__/memory-flush-harden.test.ts +367 -0
- package/src/__tests__/memory-flush-runtime.test.ts +138 -0
- package/src/__tests__/memory-flush.test.ts +64 -0
- package/src/__tests__/message-batcher.test.ts +247 -0
- package/src/__tests__/model-resolver-harden.test.ts +197 -0
- package/src/__tests__/model-resolver.test.ts +156 -0
- package/src/__tests__/processor-harden.test.ts +259 -0
- package/src/__tests__/processor.test.ts +225 -0
- package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
- package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
- package/src/__tests__/sandbox-leak.test.ts +167 -0
- package/src/__tests__/setup.ts +102 -0
- package/src/__tests__/sse-client-harden.test.ts +588 -0
- package/src/__tests__/sse-client.test.ts +90 -0
- package/src/__tests__/tool-implementations.test.ts +196 -0
- package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
- package/src/__tests__/tool-policy.test.ts +269 -0
- package/src/__tests__/worker.test.ts +89 -0
- package/src/core/error-handler.ts +47 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +94 -0
- package/src/core/workspace.ts +66 -0
- package/src/embedded/exec-sandbox.ts +372 -0
- package/src/embedded/just-bash-bootstrap.ts +575 -0
- package/src/embedded/mcp-cli-commands.ts +405 -0
- package/src/gateway/gateway-integration.ts +298 -0
- package/src/gateway/message-batcher.ts +123 -0
- package/src/gateway/sse-client.ts +988 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +123 -0
- package/src/instructions/builder.ts +44 -0
- package/src/instructions/providers.ts +27 -0
- package/src/modules/lifecycle.ts +92 -0
- package/src/openclaw/custom-tools.ts +315 -0
- package/src/openclaw/instructions.ts +36 -0
- package/src/openclaw/model-resolver.ts +150 -0
- package/src/openclaw/plugin-loader.ts +423 -0
- package/src/openclaw/processor.ts +199 -0
- package/src/openclaw/sandbox-leak.ts +100 -0
- package/src/openclaw/session-context.ts +323 -0
- package/src/openclaw/tool-policy.ts +241 -0
- package/src/openclaw/tools.ts +277 -0
- package/src/openclaw/worker.ts +1836 -0
- package/src/server.ts +330 -0
- package/src/shared/audio-provider-suggestions.ts +130 -0
- package/src/shared/processor-utils.ts +33 -0
- package/src/shared/provider-auth-hints.ts +68 -0
- package/src/shared/tool-display-config.ts +75 -0
- package/src/shared/tool-implementations.ts +981 -0
- package/src/shared/worker-env-keys.ts +8 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side MCP tool call tests (callMcpTool)
|
|
3
|
+
*
|
|
4
|
+
* callMcpTool is the function the agent calls to invoke an MCP tool via the
|
|
5
|
+
* gateway proxy. It routes to POST /mcp/<mcpId>/tools/<toolName> using the
|
|
6
|
+
* worker's JWT, then normalises the JSON response into the shared TextResult
|
|
7
|
+
* shape. These tests cover:
|
|
8
|
+
*
|
|
9
|
+
* - Happy path: content text forwarded
|
|
10
|
+
* - isError=true response: wrapped in "Error: ..." prefix
|
|
11
|
+
* - Non-200 HTTP: error text extracted
|
|
12
|
+
* - Empty content array: falls back to "<toolName> completed."
|
|
13
|
+
* - Correct Authorization header forwarding
|
|
14
|
+
* - Unknown tool name (404 from proxy): error wrapped
|
|
15
|
+
* - Tool approval required (403): error message surfaced to model
|
|
16
|
+
* - fetch throws: caught and returned as error text
|
|
17
|
+
* - AskUser / UploadFile: gateway error propagation
|
|
18
|
+
* - getChannelHistory: empty-messages branch
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
22
|
+
import {
|
|
23
|
+
askUserQuestion,
|
|
24
|
+
callMcpTool,
|
|
25
|
+
getChannelHistory,
|
|
26
|
+
uploadUserFile,
|
|
27
|
+
} from "../shared/tool-implementations";
|
|
28
|
+
import type { GatewayParams } from "../shared/tool-implementations";
|
|
29
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { rm } from "node:fs/promises";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
|
|
34
|
+
const originalFetch = globalThis.fetch;
|
|
35
|
+
|
|
36
|
+
const gw: GatewayParams = {
|
|
37
|
+
gatewayUrl: "http://gateway",
|
|
38
|
+
workerToken: "tok-abc",
|
|
39
|
+
channelId: "ch-1",
|
|
40
|
+
conversationId: "conv-1",
|
|
41
|
+
platform: "telegram",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function extractText(result: {
|
|
45
|
+
content: Array<{ type: "text"; text: string }>;
|
|
46
|
+
}): string {
|
|
47
|
+
return result.content.map((c) => c.text).join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
globalThis.fetch = originalFetch;
|
|
52
|
+
mock.restore();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// callMcpTool happy path
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
describe("callMcpTool", () => {
|
|
60
|
+
test("happy path: forwards content text from proxy response", async () => {
|
|
61
|
+
let capturedUrl = "";
|
|
62
|
+
let capturedAuth = "";
|
|
63
|
+
|
|
64
|
+
globalThis.fetch = mock(
|
|
65
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
66
|
+
capturedUrl =
|
|
67
|
+
typeof input === "string" ? input : (input as Request).url;
|
|
68
|
+
capturedAuth = new Headers(init?.headers).get("Authorization") ?? "";
|
|
69
|
+
return Response.json({
|
|
70
|
+
content: [{ type: "text", text: "the answer" }],
|
|
71
|
+
isError: false,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
) as unknown as typeof fetch;
|
|
75
|
+
|
|
76
|
+
const result = await callMcpTool(gw, "lobu", "search_memory", {
|
|
77
|
+
query: "test",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(extractText(result)).toBe("the answer");
|
|
81
|
+
expect(capturedUrl).toBe("http://gateway/mcp/lobu/tools/search_memory");
|
|
82
|
+
expect(capturedAuth).toBe("Bearer tok-abc");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("isError=true from proxy: wrapped in Error prefix", async () => {
|
|
86
|
+
globalThis.fetch = mock(async () =>
|
|
87
|
+
Response.json({
|
|
88
|
+
content: [{ type: "text", text: "not allowed" }],
|
|
89
|
+
isError: true,
|
|
90
|
+
})
|
|
91
|
+
) as unknown as typeof fetch;
|
|
92
|
+
|
|
93
|
+
const result = await callMcpTool(gw, "gh", "delete_repo", {
|
|
94
|
+
name: "myrepo",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(extractText(result)).toContain("Error:");
|
|
98
|
+
expect(extractText(result)).toContain("not allowed");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("non-200 HTTP status: error message surfaced", async () => {
|
|
102
|
+
globalThis.fetch = mock(async () =>
|
|
103
|
+
Response.json(
|
|
104
|
+
{ error: "Tool approval required", content: [], isError: true },
|
|
105
|
+
{ status: 403 }
|
|
106
|
+
)
|
|
107
|
+
) as unknown as typeof fetch;
|
|
108
|
+
|
|
109
|
+
const result = await callMcpTool(gw, "gh", "create_issue", {});
|
|
110
|
+
|
|
111
|
+
expect(extractText(result)).toContain("Error:");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("empty content array falls back to '<toolName> completed.'", async () => {
|
|
115
|
+
globalThis.fetch = mock(async () =>
|
|
116
|
+
Response.json({
|
|
117
|
+
content: [],
|
|
118
|
+
isError: false,
|
|
119
|
+
})
|
|
120
|
+
) as unknown as typeof fetch;
|
|
121
|
+
|
|
122
|
+
const result = await callMcpTool(gw, "lobu", "noop_tool", {});
|
|
123
|
+
|
|
124
|
+
expect(extractText(result)).toBe("noop_tool completed.");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("error field on response used as error message", async () => {
|
|
128
|
+
globalThis.fetch = mock(async () =>
|
|
129
|
+
Response.json(
|
|
130
|
+
{ content: [], isError: true, error: "Upstream timed out" },
|
|
131
|
+
{ status: 502 }
|
|
132
|
+
)
|
|
133
|
+
) as unknown as typeof fetch;
|
|
134
|
+
|
|
135
|
+
const result = await callMcpTool(gw, "sentry", "resolve_issue", {});
|
|
136
|
+
|
|
137
|
+
expect(extractText(result)).toContain("Upstream timed out");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("fetch throws (network down): error returned as text", async () => {
|
|
141
|
+
globalThis.fetch = mock(async () => {
|
|
142
|
+
throw new Error("network unreachable");
|
|
143
|
+
}) as unknown as typeof fetch;
|
|
144
|
+
|
|
145
|
+
const result = await callMcpTool(gw, "lobu", "search_memory", {});
|
|
146
|
+
|
|
147
|
+
expect(extractText(result)).toContain("network unreachable");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("unknown tool name (404): error wrapped", async () => {
|
|
151
|
+
globalThis.fetch = mock(async () =>
|
|
152
|
+
Response.json(
|
|
153
|
+
{ error: "MCP server 'ghost' not found", content: [], isError: true },
|
|
154
|
+
{ status: 404 }
|
|
155
|
+
)
|
|
156
|
+
) as unknown as typeof fetch;
|
|
157
|
+
|
|
158
|
+
const result = await callMcpTool(gw, "ghost", "missing_tool", {});
|
|
159
|
+
|
|
160
|
+
expect(extractText(result)).toContain("Error:");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("multiple content items concatenated", async () => {
|
|
164
|
+
globalThis.fetch = mock(async () =>
|
|
165
|
+
Response.json({
|
|
166
|
+
content: [
|
|
167
|
+
{ type: "text", text: "line one" },
|
|
168
|
+
{ type: "text", text: "line two" },
|
|
169
|
+
],
|
|
170
|
+
isError: false,
|
|
171
|
+
})
|
|
172
|
+
) as unknown as typeof fetch;
|
|
173
|
+
|
|
174
|
+
const result = await callMcpTool(gw, "lobu", "multi_content", {});
|
|
175
|
+
|
|
176
|
+
const text = extractText(result);
|
|
177
|
+
expect(text).toContain("line one");
|
|
178
|
+
expect(text).toContain("line two");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("non-text content items are filtered from output", async () => {
|
|
182
|
+
globalThis.fetch = mock(async () =>
|
|
183
|
+
Response.json({
|
|
184
|
+
content: [
|
|
185
|
+
{ type: "image", data: "base64..." },
|
|
186
|
+
{ type: "text", text: "the text" },
|
|
187
|
+
],
|
|
188
|
+
isError: false,
|
|
189
|
+
})
|
|
190
|
+
) as unknown as typeof fetch;
|
|
191
|
+
|
|
192
|
+
const result = await callMcpTool(gw, "lobu", "mixed_content", {});
|
|
193
|
+
|
|
194
|
+
// Only the text item should appear
|
|
195
|
+
expect(extractText(result)).toBe("the text");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("sends correct Content-Type header", async () => {
|
|
199
|
+
let capturedContentType = "";
|
|
200
|
+
globalThis.fetch = mock(
|
|
201
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
202
|
+
capturedContentType =
|
|
203
|
+
new Headers(init?.headers).get("Content-Type") ?? "";
|
|
204
|
+
return Response.json({
|
|
205
|
+
content: [{ type: "text", text: "ok" }],
|
|
206
|
+
isError: false,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
) as unknown as typeof fetch;
|
|
210
|
+
|
|
211
|
+
await callMcpTool(gw, "lobu", "search_memory", { q: "hello" });
|
|
212
|
+
|
|
213
|
+
expect(capturedContentType).toBe("application/json");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("sends POST method", async () => {
|
|
217
|
+
let capturedMethod = "";
|
|
218
|
+
globalThis.fetch = mock(
|
|
219
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
220
|
+
capturedMethod = init?.method ?? "";
|
|
221
|
+
return Response.json({
|
|
222
|
+
content: [{ type: "text", text: "ok" }],
|
|
223
|
+
isError: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
) as unknown as typeof fetch;
|
|
227
|
+
|
|
228
|
+
await callMcpTool(gw, "lobu", "search_memory", {});
|
|
229
|
+
|
|
230
|
+
expect(capturedMethod).toBe("POST");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// AskUserQuestion edge cases
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
describe("askUserQuestion edge cases", () => {
|
|
239
|
+
test("gateway HTTP error surfaced as error text", async () => {
|
|
240
|
+
globalThis.fetch = mock(async () =>
|
|
241
|
+
Response.json({ error: "channel not found" }, { status: 404 })
|
|
242
|
+
) as unknown as typeof fetch;
|
|
243
|
+
|
|
244
|
+
const result = await askUserQuestion(gw, {
|
|
245
|
+
question: "Pick?",
|
|
246
|
+
options: ["A"],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(extractText(result)).toContain("Error:");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("sends correct interaction type", async () => {
|
|
253
|
+
let body: Record<string, unknown> | null = null;
|
|
254
|
+
globalThis.fetch = mock(
|
|
255
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
256
|
+
body = JSON.parse(String(init?.body ?? "{}"));
|
|
257
|
+
return Response.json({ id: "q-1" });
|
|
258
|
+
}
|
|
259
|
+
) as unknown as typeof fetch;
|
|
260
|
+
|
|
261
|
+
await askUserQuestion(gw, {
|
|
262
|
+
question: "Yes or No?",
|
|
263
|
+
options: ["Yes", "No"],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(body?.interactionType).toBe("question");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// UploadUserFile edge cases
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
describe("uploadUserFile edge cases", () => {
|
|
275
|
+
test("upload HTTP error surfaced as error text", async () => {
|
|
276
|
+
const tempDir = mkdtempSync(join(tmpdir(), "lobu-up-err-"));
|
|
277
|
+
const filePath = join(tempDir, "f.txt");
|
|
278
|
+
writeFileSync(filePath, "content");
|
|
279
|
+
|
|
280
|
+
globalThis.fetch = mock(
|
|
281
|
+
async () => new Response("Forbidden", { status: 403 })
|
|
282
|
+
) as unknown as typeof fetch;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await uploadUserFile(gw, { file_path: filePath });
|
|
286
|
+
expect(extractText(result as any)).toContain("Error:");
|
|
287
|
+
} finally {
|
|
288
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("relative path without workspaceDir returns error", async () => {
|
|
293
|
+
const result = await uploadUserFile(gw, { file_path: "relative/path.txt" });
|
|
294
|
+
expect(extractText(result as any)).toContain("workspaceDir not set");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("directory path (not a file) returns error", async () => {
|
|
298
|
+
const tempDir = mkdtempSync(join(tmpdir(), "lobu-dir-test-"));
|
|
299
|
+
try {
|
|
300
|
+
const result = await uploadUserFile(gw, { file_path: tempDir });
|
|
301
|
+
expect(extractText(result as any)).toContain(
|
|
302
|
+
"not found or is not a file"
|
|
303
|
+
);
|
|
304
|
+
} finally {
|
|
305
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("empty file returns error", async () => {
|
|
310
|
+
const tempDir = mkdtempSync(join(tmpdir(), "lobu-empty-"));
|
|
311
|
+
const filePath = join(tempDir, "empty.txt");
|
|
312
|
+
writeFileSync(filePath, "");
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const result = await uploadUserFile(gw, { file_path: filePath });
|
|
316
|
+
expect(extractText(result as any)).toContain("Cannot show empty file");
|
|
317
|
+
} finally {
|
|
318
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// GetChannelHistory edge cases
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
describe("getChannelHistory edge cases", () => {
|
|
328
|
+
test("empty messages array returns 'No messages found'", async () => {
|
|
329
|
+
globalThis.fetch = mock(async () =>
|
|
330
|
+
Response.json({
|
|
331
|
+
messages: [],
|
|
332
|
+
nextCursor: null,
|
|
333
|
+
hasMore: false,
|
|
334
|
+
})
|
|
335
|
+
) as unknown as typeof fetch;
|
|
336
|
+
|
|
337
|
+
const result = await getChannelHistory(gw, { limit: 10 });
|
|
338
|
+
expect(extractText(result as any)).toContain("No messages found");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("hasMore=false with nextCursor null: no pagination hint appended", async () => {
|
|
342
|
+
globalThis.fetch = mock(async () =>
|
|
343
|
+
Response.json({
|
|
344
|
+
messages: [
|
|
345
|
+
{
|
|
346
|
+
timestamp: "2026-05-13T10:00:00.000Z",
|
|
347
|
+
user: "Alice",
|
|
348
|
+
text: "Hi",
|
|
349
|
+
isBot: false,
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
nextCursor: null,
|
|
353
|
+
hasMore: false,
|
|
354
|
+
})
|
|
355
|
+
) as unknown as typeof fetch;
|
|
356
|
+
|
|
357
|
+
const result = await getChannelHistory(gw, {});
|
|
358
|
+
const text = extractText(result as any);
|
|
359
|
+
expect(text).toContain("Alice: Hi");
|
|
360
|
+
expect(text).not.toContain("before=");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("bot messages prefixed with [Bot]", async () => {
|
|
364
|
+
globalThis.fetch = mock(async () =>
|
|
365
|
+
Response.json({
|
|
366
|
+
messages: [
|
|
367
|
+
{
|
|
368
|
+
timestamp: "2026-05-13T10:00:00.000Z",
|
|
369
|
+
user: "Lobu",
|
|
370
|
+
text: "I can help",
|
|
371
|
+
isBot: true,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
nextCursor: null,
|
|
375
|
+
hasMore: false,
|
|
376
|
+
})
|
|
377
|
+
) as unknown as typeof fetch;
|
|
378
|
+
|
|
379
|
+
const result = await getChannelHistory(gw, { limit: 1 });
|
|
380
|
+
const text = extractText(result as any);
|
|
381
|
+
expect(text).toContain("[Bot] Lobu");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("gateway HTTP error surfaced as error text", async () => {
|
|
385
|
+
globalThis.fetch = mock(async () =>
|
|
386
|
+
Response.json({ error: "channel forbidden" }, { status: 403 })
|
|
387
|
+
) as unknown as typeof fetch;
|
|
388
|
+
|
|
389
|
+
const result = await getChannelHistory(gw, { limit: 5 });
|
|
390
|
+
expect(extractText(result as any)).toContain("Error:");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Tool-policy integration: callMcpTool approval-blocked response
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
describe("callMcpTool: approval-blocked response from gateway", () => {
|
|
399
|
+
test("403 with requires-approval text surfaces as Error prefix", async () => {
|
|
400
|
+
globalThis.fetch = mock(async () =>
|
|
401
|
+
Response.json(
|
|
402
|
+
{
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "text",
|
|
406
|
+
text: "Tool call requires approval. The user has been asked to approve.",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
isError: true,
|
|
410
|
+
},
|
|
411
|
+
{ status: 403 }
|
|
412
|
+
)
|
|
413
|
+
) as unknown as typeof fetch;
|
|
414
|
+
|
|
415
|
+
const result = await callMcpTool(gw, "gh", "delete_branch", {
|
|
416
|
+
branch: "main",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const text = extractText(result);
|
|
420
|
+
expect(text).toContain("Error:");
|
|
421
|
+
expect(text).toContain("requires approval");
|
|
422
|
+
});
|
|
423
|
+
});
|