From 4d9e310dadf28bec608e217667fac9d325293a50 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 01:31:06 +0000 Subject: [PATCH] test: strengthen ports, tool policy, and note wrapping --- src/agents/tool-policy.e2e.test.ts | 57 +++++++++++++++++++++++++++++- src/infra/ports.test.ts | 27 ++++++++++++-- src/terminal/table.test.ts | 22 ++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/agents/tool-policy.e2e.test.ts b/src/agents/tool-policy.e2e.test.ts index 227c7f327..171a1723d 100644 --- a/src/agents/tool-policy.e2e.test.ts +++ b/src/agents/tool-policy.e2e.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from "vitest"; +import type { AnyAgentTool } from "./tools/common.js"; import { TOOL_POLICY_CONFORMANCE } from "./tool-policy.conformance.js"; -import { expandToolGroups, resolveToolProfilePolicy, TOOL_GROUPS } from "./tool-policy.js"; +import { + applyOwnerOnlyToolPolicy, + expandToolGroups, + isOwnerOnlyToolName, + normalizeToolName, + resolveToolProfilePolicy, + TOOL_GROUPS, +} from "./tool-policy.js"; describe("tool-policy", () => { it("expands groups and normalizes aliases", () => { @@ -28,6 +36,53 @@ describe("tool-policy", () => { expect(group).toContain("subagents"); expect(group).toContain("session_status"); }); + + it("normalizes tool names and aliases", () => { + expect(normalizeToolName(" BASH ")).toBe("exec"); + expect(normalizeToolName("apply-patch")).toBe("apply_patch"); + expect(normalizeToolName("READ")).toBe("read"); + }); + + it("identifies owner-only tools", () => { + expect(isOwnerOnlyToolName("whatsapp_login")).toBe(true); + expect(isOwnerOnlyToolName("read")).toBe(false); + }); + + it("strips owner-only tools for non-owner senders", async () => { + const tools = [ + { + name: "read", + // oxlint-disable-next-line typescript/no-explicit-any + execute: async () => ({ content: [], details: {} }) as any, + }, + { + name: "whatsapp_login", + // oxlint-disable-next-line typescript/no-explicit-any + execute: async () => ({ content: [], details: {} }) as any, + }, + ] as unknown as AnyAgentTool[]; + + const filtered = applyOwnerOnlyToolPolicy(tools, false); + expect(filtered.map((t) => t.name)).toEqual(["read"]); + }); + + it("keeps owner-only tools for the owner sender", async () => { + const tools = [ + { + name: "read", + // oxlint-disable-next-line typescript/no-explicit-any + execute: async () => ({ content: [], details: {} }) as any, + }, + { + name: "whatsapp_login", + // oxlint-disable-next-line typescript/no-explicit-any + execute: async () => ({ content: [], details: {} }) as any, + }, + ] as unknown as AnyAgentTool[]; + + const filtered = applyOwnerOnlyToolPolicy(tools, true); + expect(filtered.map((t) => t.name)).toEqual(["read", "whatsapp_login"]); + }); }); describe("TOOL_POLICY_CONFORMANCE", () => { diff --git a/src/infra/ports.test.ts b/src/infra/ports.test.ts index d3cf0ebce..a58ca4e43 100644 --- a/src/infra/ports.test.ts +++ b/src/infra/ports.test.ts @@ -1,5 +1,6 @@ import net from "node:net"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { stripAnsi } from "../terminal/ansi.js"; const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn()); @@ -24,7 +25,7 @@ describe("ports helpers", () => { await new Promise((resolve) => server.listen(0, resolve)); const port = (server.address() as net.AddressInfo).port; await expect(ensurePortAvailable(port)).rejects.toBeInstanceOf(PortInUseError); - server.close(); + await new Promise((resolve) => server.close(() => resolve())); }); it("handlePortError exits nicely on EADDRINUSE", async () => { @@ -37,10 +38,30 @@ describe("ports helpers", () => { await handlePortError(new PortInUseError(1234, "details"), 1234, "context", runtime).catch( () => {}, ); - expect(runtime.error).toHaveBeenCalled(); + const messages = runtime.error.mock.calls.map((call) => stripAnsi(String(call[0] ?? ""))); + expect(messages.join("\n")).toContain("context failed: port 1234 is already in use."); + expect(messages.join("\n")).toContain("Resolve by stopping the process"); expect(runtime.exit).toHaveBeenCalledWith(1); }); + it("prints an OpenClaw-specific hint when port details look like another OpenClaw instance", async () => { + const runtime = { + error: vi.fn(), + log: vi.fn(), + exit: vi.fn() as unknown as (code: number) => never, + }; + + await handlePortError( + new PortInUseError(18789, "node dist/index.js openclaw gateway"), + 18789, + "gateway start", + runtime, + ).catch(() => {}); + + const messages = runtime.error.mock.calls.map((call) => stripAnsi(String(call[0] ?? ""))); + expect(messages.join("\n")).toContain("another OpenClaw instance is already running"); + }); + it("classifies ssh and gateway listeners", () => { expect( classifyPortListener({ commandLine: "ssh -N -L 18789:127.0.0.1:18789 user@host" }, 18789), @@ -87,7 +108,7 @@ describeUnix("inspectPortUsage", () => { expect(result.status).toBe("busy"); expect(result.errors?.some((err) => err.includes("ENOENT"))).toBe(true); } finally { - server.close(); + await new Promise((resolve) => server.close(() => resolve())); } }); }); diff --git a/src/terminal/table.test.ts b/src/terminal/table.test.ts index 644f6be23..f8b34516c 100644 --- a/src/terminal/table.test.ts +++ b/src/terminal/table.test.ts @@ -163,4 +163,26 @@ describe("wrapNoteMessage", () => { expect(wrapped).toContain("\n"); expect(wrapped.replace(/\n/g, "")).toBe(input); }); + + it("wraps bullet lines while preserving bullet indentation", () => { + const input = "- one two three four five six seven eight nine ten"; + const wrapped = wrapNoteMessage(input, { maxWidth: 18, columns: 80 }); + const lines = wrapped.split("\n"); + expect(lines.length).toBeGreaterThan(1); + expect(lines[0]?.startsWith("- ")).toBe(true); + expect(lines.slice(1).every((line) => line.startsWith(" "))).toBe(true); + }); + + it("preserves long Windows paths without inserting spaces/newlines", () => { + // No spaces: wrapNoteMessage splits on whitespace, so a "Program Files" style path would wrap. + const input = "C:\\\\State\\\\OpenClaw\\\\bin\\\\openclaw.exe"; + const wrapped = wrapNoteMessage(input, { maxWidth: 10, columns: 80 }); + expect(wrapped).toBe(input); + }); + + it("preserves UNC paths without inserting spaces/newlines", () => { + const input = "\\\\\\\\server\\\\share\\\\some\\\\really\\\\long\\\\path\\\\file.txt"; + const wrapped = wrapNoteMessage(input, { maxWidth: 12, columns: 80 }); + expect(wrapped).toBe(input); + }); });