@lobu/worker 6.0.1 → 7.0.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/embedded/exec-sandbox.d.ts +2 -2
- package/dist/embedded/exec-sandbox.js +7 -7
- package/dist/embedded/exec-sandbox.js.map +1 -1
- package/dist/embedded/just-bash-bootstrap.d.ts +2 -2
- package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
- package/dist/embedded/just-bash-bootstrap.js +30 -6
- package/dist/embedded/just-bash-bootstrap.js.map +1 -1
- package/dist/embedded/mcp-cli-commands.d.ts +5 -5
- 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 +13 -8
- package/dist/gateway/sse-client.js.map +1 -1
- package/dist/gateway/types.d.ts +1 -1
- package/dist/gateway/types.d.ts.map +1 -1
- package/dist/instructions/builder.d.ts +4 -0
- package/dist/instructions/builder.d.ts.map +1 -1
- package/dist/instructions/builder.js +8 -11
- package/dist/instructions/builder.js.map +1 -1
- package/dist/instructions/providers.d.ts +5 -5
- package/dist/instructions/providers.d.ts.map +1 -1
- package/dist/instructions/providers.js +3 -2
- package/dist/instructions/providers.js.map +1 -1
- package/dist/openclaw/custom-tools.d.ts +1 -1
- package/dist/openclaw/custom-tools.js +1 -1
- package/dist/openclaw/instructions.d.ts +9 -9
- package/dist/openclaw/instructions.d.ts.map +1 -1
- package/dist/openclaw/instructions.js +4 -4
- package/dist/openclaw/instructions.js.map +1 -1
- package/dist/openclaw/tools.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 +18 -75
- package/dist/openclaw/worker.js.map +1 -1
- package/dist/shared/tool-implementations.d.ts.map +1 -1
- package/dist/shared/tool-implementations.js +37 -13
- 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 +269 -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 +62 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +128 -0
- package/src/core/workspace.ts +89 -0
- package/src/embedded/exec-sandbox.ts +372 -0
- package/src/embedded/just-bash-bootstrap.ts +543 -0
- package/src/embedded/mcp-cli-commands.ts +402 -0
- package/src/gateway/gateway-integration.ts +298 -0
- package/src/gateway/message-batcher.ts +123 -0
- package/src/gateway/sse-client.ts +951 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +141 -0
- package/src/instructions/builder.ts +45 -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 +427 -0
- package/src/openclaw/processor.ts +198 -0
- package/src/openclaw/sandbox-leak.ts +105 -0
- package/src/openclaw/session-context.ts +320 -0
- package/src/openclaw/tool-policy.ts +248 -0
- package/src/openclaw/tools.ts +277 -0
- package/src/openclaw/worker.ts +1847 -0
- package/src/server.ts +334 -0
- package/src/shared/audio-provider-suggestions.ts +132 -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 +940 -0
- package/src/shared/worker-env-keys.ts +8 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createOpenClawCustomTools } from "../openclaw/custom-tools";
|
|
7
|
+
|
|
8
|
+
const originalFetch = globalThis.fetch;
|
|
9
|
+
|
|
10
|
+
describe("createOpenClawCustomTools", () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
globalThis.fetch = originalFetch;
|
|
13
|
+
mock.restore();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("registers every built-in Lobu tool", () => {
|
|
17
|
+
const tools = createOpenClawCustomTools({
|
|
18
|
+
gatewayUrl: "http://gateway",
|
|
19
|
+
workerToken: "worker-token",
|
|
20
|
+
channelId: "channel-1",
|
|
21
|
+
conversationId: "conversation-1",
|
|
22
|
+
platform: "telegram",
|
|
23
|
+
workspaceDir: "/tmp/test-workspace",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(tools.map((tool) => tool.name)).toEqual([
|
|
27
|
+
"UploadUserFile",
|
|
28
|
+
"GenerateImage",
|
|
29
|
+
"GenerateAudio",
|
|
30
|
+
"GetChannelHistory",
|
|
31
|
+
"AskUserQuestion",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("UploadUserFile emits a file-uploaded custom event after a successful upload", async () => {
|
|
36
|
+
const tempDir = mkdtempSync(join(tmpdir(), "lobu-custom-tool-"));
|
|
37
|
+
const filePath = join(tempDir, "proof.txt");
|
|
38
|
+
writeFileSync(filePath, "proof");
|
|
39
|
+
|
|
40
|
+
const events: Array<{ name: string; data: Record<string, unknown> }> = [];
|
|
41
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
42
|
+
const url = String(input);
|
|
43
|
+
if (!url.endsWith("/internal/files/upload")) {
|
|
44
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
45
|
+
}
|
|
46
|
+
return Response.json({
|
|
47
|
+
fileId: "file-123",
|
|
48
|
+
name: "proof.txt",
|
|
49
|
+
permalink: "https://files.example/proof.txt",
|
|
50
|
+
});
|
|
51
|
+
}) as unknown as typeof fetch;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const uploadTool = createOpenClawCustomTools({
|
|
55
|
+
gatewayUrl: "http://gateway",
|
|
56
|
+
workerToken: "worker-token",
|
|
57
|
+
channelId: "channel-1",
|
|
58
|
+
conversationId: "conversation-1",
|
|
59
|
+
platform: "telegram",
|
|
60
|
+
workspaceDir: tempDir,
|
|
61
|
+
onCustomEvent: (name, data) => {
|
|
62
|
+
events.push({ name, data });
|
|
63
|
+
},
|
|
64
|
+
}).find((tool) => tool.name === "UploadUserFile");
|
|
65
|
+
|
|
66
|
+
expect(uploadTool).toBeDefined();
|
|
67
|
+
|
|
68
|
+
const result = await uploadTool!.execute("tool-call-1", {
|
|
69
|
+
file_path: filePath,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.content[0]?.text).toContain(
|
|
73
|
+
"Successfully showed proof.txt to the user"
|
|
74
|
+
);
|
|
75
|
+
expect(events).toEqual([
|
|
76
|
+
{
|
|
77
|
+
name: "file-uploaded",
|
|
78
|
+
data: {
|
|
79
|
+
tool: "UploadUserFile",
|
|
80
|
+
platform: "telegram",
|
|
81
|
+
fileId: "file-123",
|
|
82
|
+
name: "proof.txt",
|
|
83
|
+
permalink: "https://files.example/proof.txt",
|
|
84
|
+
size: 5,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
} finally {
|
|
89
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resetSandboxProbeForTests } from "../embedded/exec-sandbox";
|
|
6
|
+
import {
|
|
7
|
+
buildBinaryInvocation,
|
|
8
|
+
createEmbeddedBashOps,
|
|
9
|
+
} from "../embedded/just-bash-bootstrap";
|
|
10
|
+
|
|
11
|
+
const tempDirs: string[] = [];
|
|
12
|
+
const originalEnv = {
|
|
13
|
+
PATH: process.env.PATH,
|
|
14
|
+
LOBU_EXEC_SANDBOX: process.env.LOBU_EXEC_SANDBOX,
|
|
15
|
+
LOBU_ALLOW_UNSANDBOXED_EXEC: process.env.LOBU_ALLOW_UNSANDBOXED_EXEC,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function restoreEnv(name: keyof typeof originalEnv): void {
|
|
19
|
+
const value = originalEnv[name];
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
delete process.env[name];
|
|
22
|
+
} else {
|
|
23
|
+
process.env[name] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
for (const dir of tempDirs.splice(0)) {
|
|
29
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
restoreEnv("PATH");
|
|
32
|
+
restoreEnv("LOBU_EXEC_SANDBOX");
|
|
33
|
+
restoreEnv("LOBU_ALLOW_UNSANDBOXED_EXEC");
|
|
34
|
+
resetSandboxProbeForTests();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("createEmbeddedBashOps", () => {
|
|
38
|
+
test("does not register spawned PATH binaries without a sandbox or opt-in", async () => {
|
|
39
|
+
const workspace = fs.realpathSync(
|
|
40
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "lobu-sandbox-"))
|
|
41
|
+
);
|
|
42
|
+
tempDirs.push(workspace);
|
|
43
|
+
|
|
44
|
+
const nixBin = path.join(workspace, "nix", "store", "fake", "bin");
|
|
45
|
+
fs.mkdirSync(nixBin, { recursive: true });
|
|
46
|
+
const hostCat = path.join(nixBin, "hostcat");
|
|
47
|
+
fs.writeFileSync(hostCat, '#!/bin/sh\n/bin/cat "$@"\n', "utf8");
|
|
48
|
+
fs.chmodSync(hostCat, 0o755);
|
|
49
|
+
|
|
50
|
+
process.env.PATH = `${nixBin}:${process.env.PATH ?? ""}`;
|
|
51
|
+
process.env.LOBU_EXEC_SANDBOX = "off";
|
|
52
|
+
delete process.env.LOBU_ALLOW_UNSANDBOXED_EXEC;
|
|
53
|
+
|
|
54
|
+
const ops = await createEmbeddedBashOps({ workspaceDir: workspace });
|
|
55
|
+
const chunks: string[] = [];
|
|
56
|
+
const result = await ops.exec("hostcat /etc/passwd", "/", {
|
|
57
|
+
onData: (chunk) => chunks.push(chunk.toString()),
|
|
58
|
+
timeout: 5,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.exitCode).not.toBe(0);
|
|
62
|
+
expect(chunks.join("")).not.toContain("root:");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("buildBinaryInvocation", () => {
|
|
67
|
+
test("runs node shebang scripts through node", () => {
|
|
68
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lobu-lobu-"));
|
|
69
|
+
tempDirs.push(dir);
|
|
70
|
+
const scriptPath = path.join(dir, "lobu");
|
|
71
|
+
fs.writeFileSync(
|
|
72
|
+
scriptPath,
|
|
73
|
+
"#!/usr/bin/env node\nconsole.log('ok');\n",
|
|
74
|
+
"utf8"
|
|
75
|
+
);
|
|
76
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
77
|
+
|
|
78
|
+
expect(buildBinaryInvocation(scriptPath, ["version"])).toEqual({
|
|
79
|
+
command: "node",
|
|
80
|
+
args: [scriptPath, "version"],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("executes normal binaries directly", () => {
|
|
85
|
+
expect(buildBinaryInvocation("/bin/echo", ["hello"])).toEqual({
|
|
86
|
+
command: "/bin/echo",
|
|
87
|
+
args: ["hello"],
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("wraps via sandbox when context provided", () => {
|
|
92
|
+
const ws = fs.realpathSync(
|
|
93
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "lobu-sandbox-"))
|
|
94
|
+
);
|
|
95
|
+
tempDirs.push(ws);
|
|
96
|
+
const r = buildBinaryInvocation("/bin/echo", ["hi"], {
|
|
97
|
+
strategy: { kind: "sandbox-exec", path: "/usr/bin/sandbox-exec" },
|
|
98
|
+
workspaceDir: ws,
|
|
99
|
+
allowNet: false,
|
|
100
|
+
});
|
|
101
|
+
expect(r.command).toBe("/usr/bin/sandbox-exec");
|
|
102
|
+
expect(r.args[0]).toBe("-p");
|
|
103
|
+
expect(r.args).toContain("/bin/echo");
|
|
104
|
+
expect(r.args).toContain("hi");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("passes bwrap namespace cwd into the sandbox wrapper", () => {
|
|
108
|
+
const ws = fs.realpathSync(
|
|
109
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "lobu-sandbox-"))
|
|
110
|
+
);
|
|
111
|
+
tempDirs.push(ws);
|
|
112
|
+
const r = buildBinaryInvocation("/bin/echo", ["hi"], {
|
|
113
|
+
strategy: { kind: "bwrap", path: "/usr/bin/bwrap" },
|
|
114
|
+
workspaceDir: ws,
|
|
115
|
+
bwrapCwd: "/workspace/subdir",
|
|
116
|
+
});
|
|
117
|
+
const chdir = r.args.indexOf("--chdir");
|
|
118
|
+
expect(r.args[chdir + 1]).toBe("/workspace/subdir");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("sandbox=none falls through to inner invocation", () => {
|
|
122
|
+
const r = buildBinaryInvocation("/bin/echo", ["hi"], {
|
|
123
|
+
strategy: { kind: "none" },
|
|
124
|
+
workspaceDir: "/tmp",
|
|
125
|
+
});
|
|
126
|
+
expect(r).toEqual({ command: "/bin/echo", args: ["hi"] });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { McpToolDef } from "@lobu/core";
|
|
6
|
+
import {
|
|
7
|
+
buildMcpCliCommands,
|
|
8
|
+
type McpCliDeps,
|
|
9
|
+
type McpRuntimeRef,
|
|
10
|
+
} from "../embedded/mcp-cli-commands";
|
|
11
|
+
import type { GatewayParams } from "../shared/tool-implementations";
|
|
12
|
+
|
|
13
|
+
const tempDirs: string[] = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const dir of tempDirs.splice(0)) {
|
|
17
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const gw: GatewayParams = {
|
|
22
|
+
gatewayUrl: "http://gateway",
|
|
23
|
+
workerToken: "worker-token",
|
|
24
|
+
channelId: "channel-1",
|
|
25
|
+
conversationId: "conversation-1",
|
|
26
|
+
platform: "telegram",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const searchKnowledge: McpToolDef = {
|
|
30
|
+
name: "search_memory",
|
|
31
|
+
description: "Search memory",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: { query: { type: "string" } },
|
|
35
|
+
required: ["query"],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async function buildBash(options: {
|
|
40
|
+
ref: McpRuntimeRef;
|
|
41
|
+
callTool: (
|
|
42
|
+
mcpId: string,
|
|
43
|
+
toolName: string,
|
|
44
|
+
payload: Record<string, unknown>
|
|
45
|
+
) => Promise<{ content: Array<{ type: "text"; text: string }> }>;
|
|
46
|
+
}) {
|
|
47
|
+
const { Bash, ReadWriteFs, defineCommand } = await import("just-bash");
|
|
48
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "lobu-mcp-cli-"));
|
|
49
|
+
tempDirs.push(tmp);
|
|
50
|
+
|
|
51
|
+
const callTool: McpCliDeps["callTool"] = async (
|
|
52
|
+
_gw,
|
|
53
|
+
mcpId,
|
|
54
|
+
toolName,
|
|
55
|
+
payload
|
|
56
|
+
) => options.callTool(mcpId, toolName, payload);
|
|
57
|
+
|
|
58
|
+
const cliCommands = buildMcpCliCommands(options.ref, gw, { callTool });
|
|
59
|
+
|
|
60
|
+
const customCommands = cliCommands.map((c) =>
|
|
61
|
+
defineCommand(c.name, async (args: string[], ctx) => {
|
|
62
|
+
return c.execute(args, {
|
|
63
|
+
stdin: typeof ctx.stdin === "string" ? ctx.stdin : "",
|
|
64
|
+
});
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const bash = new Bash({
|
|
69
|
+
fs: new ReadWriteFs({ root: tmp }),
|
|
70
|
+
cwd: "/",
|
|
71
|
+
env: { PATH: "/usr/bin:/bin" },
|
|
72
|
+
customCommands,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return bash;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe("embedded MCP CLI through real just-bash", () => {
|
|
79
|
+
test("heredoc JSON on stdin reaches the handler as a parsed object", async () => {
|
|
80
|
+
const ref: McpRuntimeRef = {
|
|
81
|
+
current: {
|
|
82
|
+
mcpTools: { lobu: [searchKnowledge] },
|
|
83
|
+
mcpStatus: [],
|
|
84
|
+
mcpContext: {},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const calls: Array<{ payload: Record<string, unknown> }> = [];
|
|
88
|
+
const bash = await buildBash({
|
|
89
|
+
ref,
|
|
90
|
+
callTool: async (_mcpId, _toolName, payload) => {
|
|
91
|
+
calls.push({ payload });
|
|
92
|
+
return { content: [{ type: "text", text: "hit" }] };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = await bash.exec(
|
|
97
|
+
`lobu search_memory <<'EOF'
|
|
98
|
+
{"query":"architecture"}
|
|
99
|
+
EOF`,
|
|
100
|
+
{ cwd: "/" }
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.exitCode).toBe(0);
|
|
104
|
+
expect(result.stdout.trim()).toBe("hit");
|
|
105
|
+
expect(calls).toEqual([{ payload: { query: "architecture" } }]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("echo … | <server> <tool> routes the piped JSON into the handler", async () => {
|
|
109
|
+
const ref: McpRuntimeRef = {
|
|
110
|
+
current: {
|
|
111
|
+
mcpTools: { lobu: [searchKnowledge] },
|
|
112
|
+
mcpStatus: [],
|
|
113
|
+
mcpContext: {},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const calls: Array<{ payload: Record<string, unknown> }> = [];
|
|
117
|
+
const bash = await buildBash({
|
|
118
|
+
ref,
|
|
119
|
+
callTool: async (_mcpId, _toolName, payload) => {
|
|
120
|
+
calls.push({ payload });
|
|
121
|
+
return { content: [{ type: "text", text: "ok" }] };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await bash.exec(
|
|
126
|
+
`echo '{"query":"piped"}' | lobu search_memory`,
|
|
127
|
+
{ cwd: "/" }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(result.exitCode).toBe(0);
|
|
131
|
+
expect(calls).toEqual([{ payload: { query: "piped" } }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("<server> --help lists the discovered tools", async () => {
|
|
135
|
+
const ref: McpRuntimeRef = {
|
|
136
|
+
current: {
|
|
137
|
+
mcpTools: { lobu: [searchKnowledge] },
|
|
138
|
+
mcpStatus: [
|
|
139
|
+
{
|
|
140
|
+
id: "lobu",
|
|
141
|
+
name: "Lobu",
|
|
142
|
+
requiresAuth: true,
|
|
143
|
+
requiresInput: false,
|
|
144
|
+
authenticated: true,
|
|
145
|
+
configured: true,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
mcpContext: {},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const bash = await buildBash({
|
|
152
|
+
ref,
|
|
153
|
+
callTool: async () => ({ content: [] }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = await bash.exec("lobu --help", { cwd: "/" });
|
|
157
|
+
expect(result.exitCode).toBe(0);
|
|
158
|
+
expect(result.stdout).toContain("search_memory");
|
|
159
|
+
expect(result.stdout).toContain("auth login|check|logout");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("unknown subcommand exits with stderr diagnostic", async () => {
|
|
163
|
+
const ref: McpRuntimeRef = {
|
|
164
|
+
current: {
|
|
165
|
+
mcpTools: { lobu: [] },
|
|
166
|
+
mcpStatus: [],
|
|
167
|
+
mcpContext: {},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const bash = await buildBash({
|
|
171
|
+
ref,
|
|
172
|
+
callTool: async () => ({ content: [] }),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await bash.exec("lobu mystery", { cwd: "/" });
|
|
176
|
+
expect(result.exitCode).toBe(2);
|
|
177
|
+
expect(result.stderr).toContain("unknown tool");
|
|
178
|
+
});
|
|
179
|
+
});
|