diff --git a/src/infra/bonjour-errors.test.ts b/src/infra/bonjour-errors.test.ts new file mode 100644 index 00000000000..688335856c4 --- /dev/null +++ b/src/infra/bonjour-errors.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { formatBonjourError } from "./bonjour-errors.js"; + +describe("formatBonjourError", () => { + it("formats named errors with their type prefix", () => { + const err = new Error("timed out"); + err.name = "AbortError"; + expect(formatBonjourError(err)).toBe("AbortError: timed out"); + }); + + it("falls back to plain error strings and non-error values", () => { + expect(formatBonjourError(new Error(""))).toBe("Error"); + expect(formatBonjourError("boom")).toBe("boom"); + expect(formatBonjourError(42)).toBe("42"); + }); +}); diff --git a/src/infra/exec-safety.test.ts b/src/infra/exec-safety.test.ts new file mode 100644 index 00000000000..96dcdba357e --- /dev/null +++ b/src/infra/exec-safety.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { isSafeExecutableValue } from "./exec-safety.js"; + +describe("isSafeExecutableValue", () => { + it("accepts bare executable names and likely paths", () => { + expect(isSafeExecutableValue("node")).toBe(true); + expect(isSafeExecutableValue("/usr/bin/node")).toBe(true); + expect(isSafeExecutableValue("./bin/openclaw")).toBe(true); + expect(isSafeExecutableValue("C:\\Tools\\openclaw.exe")).toBe(true); + expect(isSafeExecutableValue(" tool ")).toBe(true); + }); + + it("rejects blanks, flags, shell metacharacters, quotes, and control chars", () => { + expect(isSafeExecutableValue(undefined)).toBe(false); + expect(isSafeExecutableValue(" ")).toBe(false); + expect(isSafeExecutableValue("-rf")).toBe(false); + expect(isSafeExecutableValue("node;rm -rf /")).toBe(false); + expect(isSafeExecutableValue('node "arg"')).toBe(false); + expect(isSafeExecutableValue("node\nnext")).toBe(false); + expect(isSafeExecutableValue("node\0")).toBe(false); + }); +}); diff --git a/src/infra/net/hostname.test.ts b/src/infra/net/hostname.test.ts new file mode 100644 index 00000000000..90e4c939e91 --- /dev/null +++ b/src/infra/net/hostname.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { normalizeHostname } from "./hostname.js"; + +describe("normalizeHostname", () => { + it("trims, lowercases, and strips a trailing dot", () => { + expect(normalizeHostname(" Example.COM. ")).toBe("example.com"); + }); + + it("unwraps bracketed ipv6 hosts after normalization", () => { + expect(normalizeHostname(" [FD7A:115C:A1E0::1] ")).toBe("fd7a:115c:a1e0::1"); + }); + + it("leaves non-fully-bracketed values otherwise unchanged", () => { + expect(normalizeHostname("[fd7a:115c:a1e0::1")).toBe("[fd7a:115c:a1e0::1"); + expect(normalizeHostname("fd7a:115c:a1e0::1]")).toBe("fd7a:115c:a1e0::1]"); + }); +}); diff --git a/src/infra/ports-probe.test.ts b/src/infra/ports-probe.test.ts new file mode 100644 index 00000000000..ce127970cce --- /dev/null +++ b/src/infra/ports-probe.test.ts @@ -0,0 +1,30 @@ +import net from "node:net"; +import { describe, expect, it } from "vitest"; +import { tryListenOnPort } from "./ports-probe.js"; + +describe("tryListenOnPort", () => { + it("can bind and release an ephemeral loopback port", async () => { + await expect(tryListenOnPort({ port: 0, host: "127.0.0.1", exclusive: true })).resolves.toBe( + undefined, + ); + }); + + it("rejects when the port is already in use", async () => { + const server = net.createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve())); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("expected tcp address"); + } + + try { + await expect( + tryListenOnPort({ port: address.port, host: "127.0.0.1" }), + ).rejects.toMatchObject({ + code: "EADDRINUSE", + }); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }); +});