mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 14:11:36 +00:00
714 lines
25 KiB
TypeScript
714 lines
25 KiB
TypeScript
/** Tests Code Mode guest execution. */
|
|
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { applyCodeModeCatalog, createCodeModeTools } from "./code-mode.js";
|
|
import {
|
|
resetCodeModeTestState,
|
|
pluginTool,
|
|
resultDetails,
|
|
createCodeModeHarness,
|
|
runUntilCompleted,
|
|
testing,
|
|
} from "./code-mode.test-support.js";
|
|
import { createToolSearchCatalogRef } from "./tool-search.js";
|
|
|
|
describe("Code Mode guest execution", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
resetCodeModeTestState();
|
|
});
|
|
|
|
it("accepts command as an exec-compatible code alias", async () => {
|
|
const { config, catalogRef, tools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...tools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
const result = resultDetails(
|
|
await expectDefined(tools[0], "tools[0] test invariant").execute("code-call-command-alias", {
|
|
command: "return 7;",
|
|
}),
|
|
);
|
|
|
|
expect(result.status).toBe("completed");
|
|
expect(result.value).toBe(7);
|
|
});
|
|
|
|
it("rejects divergent code and command aliases", async () => {
|
|
const { config, catalogRef, tools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...tools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
await expect(
|
|
expectDefined(tools[0], "tools[0] test invariant").execute("code-call-divergent-alias", {
|
|
code: "return 1;",
|
|
command: "return 2;",
|
|
}),
|
|
).rejects.toThrow("code and command must match when both are provided");
|
|
});
|
|
|
|
it.each([
|
|
{ code: "ls -la /workspace/" },
|
|
{ code: "ls -1" },
|
|
{ command: "ls -la /workspace/" },
|
|
{ code: "pwd", command: "pwd" },
|
|
{ command: "pwd;" },
|
|
{ command: "pwd; // inspect the workspace" },
|
|
{ code: "# inspect the workspace\npwd" },
|
|
{ code: "#!/bin/sh\npwd" },
|
|
{ code: "pwd\nls -la /workspace" },
|
|
{ command: "pwd;ls -la /workspace" },
|
|
{ command: "/bin/ls /workspace/" },
|
|
{ command: "./gradlew test" },
|
|
{ code: ".\\gradlew.bat test" },
|
|
{ command: ".\\script.ps1" },
|
|
{ code: "C:\\workspace\\run.cmd /q" },
|
|
{ code: "/workspace/run.sh --verbose" },
|
|
{ command: "sh -c 'ls /workspace/'" },
|
|
{ command: "git status" },
|
|
{ command: 'git status; const note = "git";' },
|
|
{ command: "ls -1; const metadata = { ls: true };" },
|
|
{ code: "ls -1; const note = 'function ls';" },
|
|
{ command: "ls -1; let ls = 7;" },
|
|
{ command: "npm test" },
|
|
{ command: "NODE_ENV=test npm test" },
|
|
{ code: "NODE_ENV=test\nnpm test" },
|
|
{ code: "FOO=bar ./gradlew test" },
|
|
{ command: 'GREETING="hello world" npm test' },
|
|
{ command: "whoami" },
|
|
{ code: "set -euo pipefail" },
|
|
{ command: "exit" },
|
|
{ command: "if [ -d /workspace ]; then pwd; fi" },
|
|
{ code: "while test -d /workspace; do pwd; done" },
|
|
{ command: 'for ((i=0; i<3; i++)); do echo "$i"; done' },
|
|
{ code: "function task { pwd; }" },
|
|
{ command: "source ./env" },
|
|
{ code: "command ls" },
|
|
{ command: "go test ./..." },
|
|
{ code: "cargo test" },
|
|
{ command: "sort /workspace/file" },
|
|
{ code: "wc -l file" },
|
|
{ command: "jq . file.json" },
|
|
{ code: "exec ls" },
|
|
{ command: "custom-tool --format=json" },
|
|
{ command: "ls > output" },
|
|
{ code: "ls>output" },
|
|
{ command: "ls >output" },
|
|
{ code: "ls >> output" },
|
|
{ command: "cat<input" },
|
|
{ code: "// inspect the workspace\npwd" },
|
|
{ command: "/* inspect the workspace */ pwd;" },
|
|
{ code: "rg code-mode src/agents" },
|
|
])("rejects shell source before starting a guest worker: %j", async (args) => {
|
|
const { config, catalogRef, tools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...tools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = resultDetails(
|
|
await expectDefined(tools[0], "tools[0] test invariant").execute(
|
|
"code-call-shell-source",
|
|
args,
|
|
),
|
|
);
|
|
|
|
expect(details.status).toBe("failed");
|
|
expect(details.code).toBe("invalid_input");
|
|
expect(details.error).toMatch(/JavaScript or TypeScript, not shell commands/);
|
|
expect(testing.activeRuns.size).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
{ code: "true;", value: null },
|
|
{ code: "false;", value: null },
|
|
{ code: "return true || false;", value: true },
|
|
{ code: "return -1;", value: -1 },
|
|
{ code: "return /foo/.test('foo');", value: true },
|
|
{ code: "Infinity -1; return 42;", value: 42 },
|
|
{ code: "eval; return typeof eval;", value: "function" },
|
|
{ code: "if (true) { return -1; }", value: -1 },
|
|
{ code: "for (let i = 0; i < 3; i++) { if (i === 2) { return i; } }", value: 2 },
|
|
{ code: "function task() { return 7; } return task();", value: 7 },
|
|
{ code: "// explain the guest program\nreturn 7;", value: 7 },
|
|
{ code: "const ls = 7; return ls;", value: 7 },
|
|
{ code: "const echo = (value) => value; return echo('hello');", value: "hello" },
|
|
{ code: "test instanceof Function; function test() {}", value: null },
|
|
{ code: "ls -1; function ls() {}", value: null },
|
|
{ code: "ls -1; function/**/ls() {}", value: null },
|
|
{ code: "ls > limit; function ls() {} var limit = 1;", value: null },
|
|
{ code: "echo `hello`; function echo(parts) { return parts[0]; }", value: null },
|
|
{ code: "pwd; var { pwd } = { pwd: 7 }; return pwd;", value: 7 },
|
|
{ code: "pwd; var [pwd] = [7]; return pwd;", value: 7 },
|
|
{ code: "pwd; for (var pwd of [7]) {} return pwd;", value: 7 },
|
|
{ code: "pwd; var other = 1, pwd = 7; return pwd;", value: 7 },
|
|
{ code: "pwd; function* pwd() { yield 7; } return pwd().next().value;", value: 7 },
|
|
{ code: "pwd; function/**/pwd() { return 7; } return pwd();", value: 7 },
|
|
{ code: "pwd; var/**/{ pwd } = { pwd: 7 }; return pwd;", value: 7 },
|
|
{ code: "node -version; function/**/node() {}; var version = 1;", value: null },
|
|
])(
|
|
"executes valid shell-like JavaScript without false rejection: %j",
|
|
async ({ code, value }) => {
|
|
const { config, catalogRef, tools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...tools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = resultDetails(
|
|
await expectDefined(tools[0], "tools[0] test invariant").execute(
|
|
"code-call-valid-shell-like-source",
|
|
{ code },
|
|
),
|
|
);
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toBe(value);
|
|
},
|
|
);
|
|
|
|
it("runs JavaScript through QuickJS-WASI and resumes nested tool calls with wait", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
const ticket = pluginTool("fake_create_ticket", "Create a fake ticket");
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, ticket],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: `
|
|
const hits = await tools.search("ticket", { limit: 1 });
|
|
const called = await tools.callValue(hits[0].id, { value: "ship" });
|
|
text("created");
|
|
return called;
|
|
`,
|
|
});
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toEqual({
|
|
name: "fake_create_ticket",
|
|
input: { value: "ship" },
|
|
});
|
|
expect(details.output).toEqual([{ type: "text", text: "created" }]);
|
|
expect(details.telemetry).toMatchObject({ searchCount: 1, describeCount: 0, callCount: 1 });
|
|
expect(ticket.execute).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("returns structured values from named tools while preserving the raw call envelope", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
const ticket = pluginTool("fake_create_ticket", "Create a fake ticket");
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, ticket],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: `
|
|
const id = "openclaw:fake-code-mode:fake_create_ticket";
|
|
const input = { value: "ship" };
|
|
return {
|
|
named: await tools.fake_create_ticket(input),
|
|
value: await tools.callValue(id, input),
|
|
envelope: await tools.call(id, input),
|
|
};
|
|
`,
|
|
});
|
|
|
|
const expectedValue = {
|
|
name: "fake_create_ticket",
|
|
input: { value: "ship" },
|
|
};
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toEqual({
|
|
named: expectedValue,
|
|
value: expectedValue,
|
|
envelope: {
|
|
tool: expect.objectContaining({
|
|
id: "openclaw:fake-code-mode:fake_create_ticket",
|
|
name: "fake_create_ticket",
|
|
}),
|
|
result: expect.objectContaining({ details: expectedValue }),
|
|
},
|
|
});
|
|
expect(details.telemetry).toMatchObject({ callCount: 3 });
|
|
expect(ticket.execute).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("uses tools recovery guidance for guessed tool ids", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
const writeTool = pluginTool("write", "Write a file to the workspace");
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, writeTool],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: `
|
|
try {
|
|
await tools.call("file_write", {
|
|
path: "memory/2026-05-22.md",
|
|
content: "remember this",
|
|
});
|
|
return "unexpected success";
|
|
} catch (error) {
|
|
return error.message;
|
|
}
|
|
`,
|
|
});
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toBe(
|
|
"Unknown tool id: file_write. Did you mean: write? Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name.",
|
|
);
|
|
expect(writeTool.execute).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses tools recovery guidance when no generic Code Mode suggestion matches", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: codeModeTools,
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: `
|
|
try {
|
|
await tools.call("missing_tool", {});
|
|
return "unexpected success";
|
|
} catch (error) {
|
|
return error.message;
|
|
}
|
|
`,
|
|
});
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toBe(
|
|
"Unknown tool id: missing_tool. Use tools.search to find a tool, tools.describe to inspect it, then tools.call with the exact id or name.",
|
|
);
|
|
});
|
|
|
|
it("does not load TypeScript for plain JavaScript code mode runs", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: "return 42;",
|
|
});
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toBe(42);
|
|
expect(testing.getTypescriptRuntimePromise()).toBeNull();
|
|
});
|
|
|
|
it("allows identifiers and strings that contain import without module access", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: `
|
|
const important = 41;
|
|
const message = "import docs later";
|
|
return important + (message.includes("import") ? 1 : 0);
|
|
`,
|
|
});
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toBe(42);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "template-literal import text",
|
|
code: "return `import('node:fs')`;",
|
|
value: "import('node:fs')",
|
|
},
|
|
{
|
|
name: "template-literal require text",
|
|
code: "return `require('node:fs')`;",
|
|
value: "require('node:fs')",
|
|
},
|
|
{
|
|
name: "nested template-literal module text",
|
|
code: "return `outer ${`require('node:fs')`}`;",
|
|
value: "outer require('node:fs')",
|
|
},
|
|
{
|
|
name: "regular-expression module text",
|
|
code: 'return /import.meta/.test("import.meta");',
|
|
value: true,
|
|
},
|
|
{
|
|
name: "regular-expression module text inside interpolation",
|
|
code: 'return `${/import.meta/.test("import.meta")}`;',
|
|
value: "true",
|
|
},
|
|
{
|
|
name: "ordinary import method",
|
|
code: "const api = { import(value) { return value; } }; return api.import(42);",
|
|
value: 42,
|
|
},
|
|
{
|
|
name: "ordinary require method",
|
|
code: "const api = { require(value) { return value; } }; return api.require(42);",
|
|
value: 42,
|
|
},
|
|
{
|
|
name: "optional ordinary import method",
|
|
code: "const api = { import(value) { return value; } }; return api?.import?.(42);",
|
|
value: 42,
|
|
},
|
|
{
|
|
name: "computed ordinary require method",
|
|
code: 'const api = { require(value) { return value; } }; return api["require"](42);',
|
|
value: 42,
|
|
},
|
|
{
|
|
name: "ordinary import metadata property",
|
|
code: "const api = { import: { meta: 42 } }; return api.import.meta;",
|
|
value: 42,
|
|
},
|
|
])("executes harmless $name in the real guest worker", async ({ code, value }) => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code,
|
|
});
|
|
|
|
expect(details).toMatchObject({ status: "completed", value });
|
|
expect(testing.activeRuns.size).toBe(0);
|
|
});
|
|
|
|
it("never exposes Node module-loader globals to the real guest worker", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: "return [typeof process, typeof module, typeof require];",
|
|
});
|
|
|
|
expect(details).toMatchObject({
|
|
status: "completed",
|
|
value: ["undefined", "undefined", "undefined"],
|
|
});
|
|
expect(testing.activeRuns.size).toBe(0);
|
|
});
|
|
|
|
it("isolates and cleans up 12 concurrent real guest workers", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
const execTool = expectDefined(codeModeTools[0], "codeModeTools[0] test invariant");
|
|
|
|
const results = await Promise.all(
|
|
Array.from({ length: 12 }, async (_, index) =>
|
|
resultDetails(
|
|
await execTool.execute(`code-call-concurrent-worker-${index}`, {
|
|
code: `return { index: ${index}, message: \`require('node:fs')\` };`,
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(results).toEqual(
|
|
Array.from({ length: 12 }, (_, index) =>
|
|
expect.objectContaining({
|
|
status: "completed",
|
|
value: { index, message: "require('node:fs')" },
|
|
}),
|
|
),
|
|
);
|
|
expect(testing.activeRuns.size).toBe(0);
|
|
expect(testing.resumingRunIds.size).toBe(0);
|
|
});
|
|
|
|
it("fails pending promises that have no host bridge work", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const beforeRunCount = testing.activeRuns.size;
|
|
const details = resultDetails(
|
|
await expectDefined(codeModeTools[0], "codeModeTools[0] test invariant").execute(
|
|
"code-call-empty-wait",
|
|
{
|
|
code: "await new Promise(() => undefined); return 'never';",
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(details.status).toBe("failed");
|
|
expect(String(details.error)).toContain("pending without host work");
|
|
expect(testing.activeRuns.size).toBe(beforeRunCount);
|
|
});
|
|
|
|
it("surfaces the QuickJS error name and message for guest syntax errors", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = resultDetails(
|
|
await expectDefined(codeModeTools[0], "codeModeTools[0] test invariant").execute(
|
|
"code-call-syntax",
|
|
{ code: "const x = ;" },
|
|
),
|
|
);
|
|
|
|
expect(details.status).toBe("failed");
|
|
const error = String(details.error);
|
|
// Regression guard: QuickJS stacks are frames only, so the error used to
|
|
// collapse to a bare "at openclaw-code-mode:user.js:..." location with the
|
|
// actual cause dropped. The model now sees the name and message.
|
|
expect(error).toContain("SyntaxError");
|
|
expect(error).toContain("unexpected token");
|
|
expect(error.startsWith("at ")).toBe(false);
|
|
});
|
|
|
|
it("surfaces the QuickJS error name and message for guest runtime errors", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = resultDetails(
|
|
await expectDefined(codeModeTools[0], "codeModeTools[0] test invariant").execute(
|
|
"code-call-runtime",
|
|
{ code: "return missingFn();" },
|
|
),
|
|
);
|
|
|
|
expect(details.status).toBe("failed");
|
|
const error = String(details.error);
|
|
expect(error).toContain("ReferenceError");
|
|
expect(error).toContain("missingFn is not defined");
|
|
expect(error.startsWith("at ")).toBe(false);
|
|
});
|
|
|
|
it("does not expose the raw host request callback", async () => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = resultDetails(
|
|
await expectDefined(codeModeTools[0], "codeModeTools[0] test invariant").execute(
|
|
"code-hidden-host-request",
|
|
{ code: "return typeof globalThis.__openclawHostRequest;" },
|
|
),
|
|
);
|
|
|
|
expect(details).toMatchObject({
|
|
status: "completed",
|
|
value: "undefined",
|
|
});
|
|
});
|
|
|
|
it("clamps omitted code-mode catalog search limits to maxSearchLimit", async () => {
|
|
const catalogRef = createToolSearchCatalogRef();
|
|
const config = {
|
|
tools: {
|
|
codeMode: {
|
|
enabled: true,
|
|
maxSearchLimit: 3,
|
|
},
|
|
},
|
|
} as never;
|
|
const ctx = {
|
|
config,
|
|
runtimeConfig: config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
};
|
|
const codeModeTools = createCodeModeTools(ctx);
|
|
applyCodeModeCatalog({
|
|
tools: [
|
|
...codeModeTools,
|
|
pluginTool("fake_ticket_one", "ticket helper"),
|
|
pluginTool("fake_ticket_two", "ticket helper"),
|
|
pluginTool("fake_ticket_three", "ticket helper"),
|
|
pluginTool("fake_ticket_four", "ticket helper"),
|
|
pluginTool("fake_ticket_five", "ticket helper"),
|
|
],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = await runUntilCompleted({
|
|
execTool: expectDefined(codeModeTools[0], "codeModeTools[0] test invariant"),
|
|
waitTool: expectDefined(codeModeTools[1], "codeModeTools[1] test invariant"),
|
|
code: 'const hits = await tools.search("ticket"); return hits.length;',
|
|
});
|
|
|
|
expect(details.status).toBe("completed");
|
|
expect(details.value).toBe(3);
|
|
});
|
|
|
|
it.each([
|
|
"const fs = require('node:fs'); return fs;",
|
|
String.raw`return r\u0065quire('node:fs');`,
|
|
"return require?.('node:fs');",
|
|
"return (require)('node:fs');",
|
|
"return (0, require)('node:fs');",
|
|
"const load = require; return load('node:fs');",
|
|
"return module.require('node:fs');",
|
|
"return process.getBuiltinModule('node:fs');",
|
|
"return import('node:fs');",
|
|
"return import.meta.url;",
|
|
"return `${import('node:fs')}`;",
|
|
"return `${require('node:fs')}`;",
|
|
"return `${`nested ${import('node:fs')}`}`;",
|
|
"return `${`nested ${require('node:fs')}`}`;",
|
|
"return `${({ value: import('node:fs') }).value}`;",
|
|
"const message = `import('node:fs')`; return require('node:fs');",
|
|
"const pattern = /import.meta/; return import('node:fs');",
|
|
"let value = 1; return value++ / import('node:fs');",
|
|
"let value = 1; return value-- / import('node:fs');",
|
|
"const value = { of: 1 }; return value.of / import('node:fs');",
|
|
"const value = { return: 1 }; return value.return / import('node:fs');",
|
|
"const value = { if() { return 1; } }; return value.if() / import('node:fs');",
|
|
"const value = { return: 1 }; return value?.return / import('node:fs') / 1;",
|
|
"const value = { return: 1 }; return value?.return / require('node:fs') / 1;",
|
|
"const value = { if() { return 1; } }; return value?.if() / import('node:fs');",
|
|
"function run() { const await = 1; return await / (globalThis.pending = import('node:fs')); } run(); return globalThis.pending;",
|
|
"class Guest { #return = 1; run() { return this.#return / (globalThis.pending = import('node:fs')); } } new Guest().run(); return globalThis.pending;",
|
|
])("rejects module access: %s", async (code) => {
|
|
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
|
applyCodeModeCatalog({
|
|
tools: [...codeModeTools, pluginTool("fake_noop", "Noop")],
|
|
config,
|
|
sessionId: "session-code-mode",
|
|
sessionKey: "agent:main:main",
|
|
runId: "run-code-mode",
|
|
catalogRef,
|
|
});
|
|
|
|
const details = resultDetails(
|
|
await expectDefined(codeModeTools[0], "codeModeTools[0] test invariant").execute(
|
|
"code-call-import",
|
|
{
|
|
code,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(details.status).toBe("failed");
|
|
expect(String(details.error)).toContain("module access is disabled");
|
|
});
|
|
});
|