diff --git a/src/agents/openclaw-tools.tts-config.test.ts b/src/agents/openclaw-tools.tts-config.test.ts index 0758ff22f4b..7ba078480ef 100644 --- a/src/agents/openclaw-tools.tts-config.test.ts +++ b/src/agents/openclaw-tools.tts-config.test.ts @@ -1,13 +1,116 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { AnyAgentTool } from "./tools/common.js"; -const mocks = vi.hoisted(() => ({ - textToSpeech: vi.fn(async () => ({ - success: true, - audioPath: "/tmp/openclaw/tts-config-test.opus", - provider: "microsoft", - voiceCompatible: true, - })), +const mocks = vi.hoisted(() => { + const stubTool = (name: string) => + ({ + name, + label: name, + displaySummary: name, + description: name, + parameters: { type: "object", properties: {} }, + execute: vi.fn(), + }) satisfies AnyAgentTool; + + return { + stubTool, + textToSpeech: vi.fn(async () => ({ + success: true, + audioPath: "/tmp/openclaw/tts-config-test.opus", + provider: "microsoft", + voiceCompatible: true, + })), + }; +}); + +vi.mock("./openclaw-plugin-tools.js", () => ({ + resolveOpenClawPluginToolsForOptions: () => [], +})); + +vi.mock("./openclaw-tools.nodes-workspace-guard.js", () => ({ + applyNodesToolWorkspaceGuard: (tool: AnyAgentTool) => tool, +})); + +vi.mock("./tools/agents-list-tool.js", () => ({ + createAgentsListTool: () => mocks.stubTool("agents_list"), +})); + +vi.mock("./tools/canvas-tool.js", () => ({ + createCanvasTool: () => mocks.stubTool("canvas"), +})); + +vi.mock("./tools/cron-tool.js", () => ({ + createCronTool: () => mocks.stubTool("cron"), +})); + +vi.mock("./tools/gateway-tool.js", () => ({ + createGatewayTool: () => mocks.stubTool("gateway"), +})); + +vi.mock("./tools/image-generate-tool.js", () => ({ + createImageGenerateTool: () => mocks.stubTool("image_generate"), +})); + +vi.mock("./tools/image-tool.js", () => ({ + createImageTool: () => mocks.stubTool("image"), +})); + +vi.mock("./tools/message-tool.js", () => ({ + createMessageTool: () => mocks.stubTool("message"), +})); + +vi.mock("./tools/music-generate-tool.js", () => ({ + createMusicGenerateTool: () => mocks.stubTool("music_generate"), +})); + +vi.mock("./tools/nodes-tool.js", () => ({ + createNodesTool: () => mocks.stubTool("nodes"), +})); + +vi.mock("./tools/pdf-tool.js", () => ({ + createPdfTool: () => mocks.stubTool("pdf"), +})); + +vi.mock("./tools/session-status-tool.js", () => ({ + createSessionStatusTool: () => mocks.stubTool("session_status"), +})); + +vi.mock("./tools/sessions-history-tool.js", () => ({ + createSessionsHistoryTool: () => mocks.stubTool("sessions_history"), +})); + +vi.mock("./tools/sessions-list-tool.js", () => ({ + createSessionsListTool: () => mocks.stubTool("sessions_list"), +})); + +vi.mock("./tools/sessions-send-tool.js", () => ({ + createSessionsSendTool: () => mocks.stubTool("sessions_send"), +})); + +vi.mock("./tools/sessions-spawn-tool.js", () => ({ + createSessionsSpawnTool: () => mocks.stubTool("sessions_spawn"), +})); + +vi.mock("./tools/sessions-yield-tool.js", () => ({ + createSessionsYieldTool: () => mocks.stubTool("sessions_yield"), +})); + +vi.mock("./tools/subagents-tool.js", () => ({ + createSubagentsTool: () => mocks.stubTool("subagents"), +})); + +vi.mock("./tools/update-plan-tool.js", () => ({ + createUpdatePlanTool: () => mocks.stubTool("update_plan"), +})); + +vi.mock("./tools/video-generate-tool.js", () => ({ + createVideoGenerateTool: () => mocks.stubTool("video_generate"), +})); + +vi.mock("./tools/web-tools.js", () => ({ + createWebFetchTool: () => mocks.stubTool("web_fetch"), + createWebSearchTool: () => mocks.stubTool("web_search"), })); vi.mock("../tts/tts.js", () => ({ diff --git a/src/gateway/server.node-pairing-auto-approve.test.ts b/src/gateway/server.node-pairing-auto-approve.test.ts index 22c11248af8..2732eb1442c 100644 --- a/src/gateway/server.node-pairing-auto-approve.test.ts +++ b/src/gateway/server.node-pairing-auto-approve.test.ts @@ -1,3 +1,4 @@ +import net from "node:net"; import { describe, expect, test } from "vitest"; import { WebSocket } from "ws"; import { writeConfigFile } from "../config/config.js"; @@ -23,7 +24,9 @@ const NODE_CLIENT = { }; async function openLanGatewayWs(params: { host: string; port: number }): Promise { - const ws = new WebSocket(`ws://${params.host}:${params.port}`); + const ws = new WebSocket(`ws://${params.host}:${params.port}`, { + localAddress: params.host, + }); trackConnectChallengeNonce(ws); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 10_000); @@ -46,10 +49,46 @@ async function openLanGatewayWs(params: { host: string; port: number }): Promise return ws; } +async function canUseLanSelfConnect(host: string): Promise { + return await new Promise((resolve) => { + let settled = false; + let client: net.Socket | undefined; + const server = net.createServer((socket) => { + socket.on("error", () => {}); + socket.end("ok"); + }); + const done = (ok: boolean) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + client?.destroy(); + server.close(() => resolve(ok)); + }; + const timer = setTimeout(() => done(false), 1_000); + server.once("error", () => done(false)); + server.listen(0, "0.0.0.0", () => { + const address = server.address(); + if (!address || typeof address === "string") { + done(false); + return; + } + let sawData = false; + client = net.connect({ host, port: address.port, localAddress: host }); + client.on("data", () => { + sawData = true; + }); + client.once("error", () => done(false)); + client.once("close", () => done(sawData)); + }); + }); +} + describe("gateway trusted CIDR node pairing auto-approve", () => { test("stays disabled by default for a direct non-loopback node", async () => { const lanIp = pickPrimaryLanIPv4(); - if (!lanIp) { + if (!lanIp || !(await canUseLanSelfConnect(lanIp))) { return; } const started = await startServer(TOKEN, { bind: "lan", controlUiEnabled: false }); @@ -82,7 +121,7 @@ describe("gateway trusted CIDR node pairing auto-approve", () => { test("auto-approves first-time node pairing from a matching direct non-loopback CIDR", async () => { const lanIp = pickPrimaryLanIPv4(); - if (!lanIp) { + if (!lanIp || !(await canUseLanSelfConnect(lanIp))) { return; } await writeConfigFile({