124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import type { ClawdbotPluginApi, ClawdbotPluginToolContext } from "../../../src/plugins/types.js";
|
|
import { createLobsterTool } from "./lobster-tool.js";
|
|
|
|
async function writeFakeLobsterScript(scriptBody: string, prefix = "clawdbot-lobster-plugin-") {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
const isWindows = process.platform === "win32";
|
|
|
|
if (isWindows) {
|
|
const scriptPath = path.join(dir, "lobster.js");
|
|
const cmdPath = path.join(dir, "lobster.cmd");
|
|
await fs.writeFile(scriptPath, scriptBody, { encoding: "utf8" });
|
|
const cmd = `@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`;
|
|
await fs.writeFile(cmdPath, cmd, { encoding: "utf8" });
|
|
return { dir, binPath: cmdPath };
|
|
}
|
|
|
|
const binPath = path.join(dir, "lobster");
|
|
const file = `#!/usr/bin/env node\n${scriptBody}\n`;
|
|
await fs.writeFile(binPath, file, { encoding: "utf8", mode: 0o755 });
|
|
return { dir, binPath };
|
|
}
|
|
|
|
async function writeFakeLobster(params: { payload: unknown }) {
|
|
const scriptBody =
|
|
`const payload = ${JSON.stringify(params.payload)};\n` +
|
|
`process.stdout.write(JSON.stringify(payload));\n`;
|
|
return await writeFakeLobsterScript(scriptBody);
|
|
}
|
|
|
|
function fakeApi(): ClawdbotPluginApi {
|
|
return {
|
|
id: "lobster",
|
|
name: "lobster",
|
|
source: "test",
|
|
config: {} as any,
|
|
runtime: { version: "test" } as any,
|
|
logger: { info() {}, warn() {}, error() {}, debug() {} },
|
|
registerTool() {},
|
|
registerHttpHandler() {},
|
|
registerChannel() {},
|
|
registerGatewayMethod() {},
|
|
registerCli() {},
|
|
registerService() {},
|
|
registerProvider() {},
|
|
resolvePath: (p) => p,
|
|
};
|
|
}
|
|
|
|
function fakeCtx(overrides: Partial<ClawdbotPluginToolContext> = {}): ClawdbotPluginToolContext {
|
|
return {
|
|
config: {} as any,
|
|
workspaceDir: "/tmp",
|
|
agentDir: "/tmp",
|
|
agentId: "main",
|
|
sessionKey: "main",
|
|
messageChannel: undefined,
|
|
agentAccountId: undefined,
|
|
sandboxed: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("lobster plugin tool", () => {
|
|
it("runs lobster and returns parsed envelope in details", async () => {
|
|
const fake = await writeFakeLobster({
|
|
payload: { ok: true, status: "ok", output: [{ hello: "world" }], requiresApproval: null },
|
|
});
|
|
|
|
const tool = createLobsterTool(fakeApi());
|
|
const res = await tool.execute("call1", {
|
|
action: "run",
|
|
pipeline: "noop",
|
|
lobsterPath: fake.binPath,
|
|
timeoutMs: 1000,
|
|
});
|
|
|
|
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|
|
});
|
|
|
|
it("requires absolute lobsterPath when provided", async () => {
|
|
const tool = createLobsterTool(fakeApi());
|
|
await expect(
|
|
tool.execute("call2", {
|
|
action: "run",
|
|
pipeline: "noop",
|
|
lobsterPath: "./lobster",
|
|
}),
|
|
).rejects.toThrow(/absolute path/);
|
|
});
|
|
|
|
it("rejects invalid JSON from lobster", async () => {
|
|
const { binPath } = await writeFakeLobsterScript(
|
|
`process.stdout.write("nope");\n`,
|
|
"clawdbot-lobster-plugin-bad-",
|
|
);
|
|
|
|
const tool = createLobsterTool(fakeApi());
|
|
await expect(
|
|
tool.execute("call3", {
|
|
action: "run",
|
|
pipeline: "noop",
|
|
lobsterPath: binPath,
|
|
}),
|
|
).rejects.toThrow(/invalid JSON/);
|
|
});
|
|
|
|
it("can be gated off in sandboxed contexts", async () => {
|
|
const api = fakeApi();
|
|
const factoryTool = (ctx: ClawdbotPluginToolContext) => {
|
|
if (ctx.sandboxed) return null;
|
|
return createLobsterTool(api);
|
|
};
|
|
|
|
expect(factoryTool(fakeCtx({ sandboxed: true }))).toBeNull();
|
|
expect(factoryTool(fakeCtx({ sandboxed: false }))?.name).toBe("lobster");
|
|
});
|
|
});
|