diff --git a/src/infra/gateway-process-argv.test.ts b/src/infra/gateway-process-argv.test.ts new file mode 100644 index 00000000000..f3570316860 --- /dev/null +++ b/src/infra/gateway-process-argv.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { isGatewayArgv, parseProcCmdline } from "./gateway-process-argv.js"; + +describe("parseProcCmdline", () => { + it("splits null-delimited argv and trims empty entries", () => { + expect(parseProcCmdline(" node \0 gateway \0\0 --port \0 18789 \0")).toEqual([ + "node", + "gateway", + "--port", + "18789", + ]); + }); +}); + +describe("isGatewayArgv", () => { + it("requires a gateway token", () => { + expect(isGatewayArgv(["node", "dist/index.js", "--port", "18789"])).toBe(false); + }); + + it("matches known entrypoints across slash and case variants", () => { + expect(isGatewayArgv(["NODE", "C:\\OpenClaw\\DIST\\ENTRY.JS", "gateway"])).toBe(true); + expect(isGatewayArgv(["bun", "/srv/openclaw/scripts/run-node.mjs", "gateway"])).toBe(true); + }); + + it("matches the openclaw executable but gates the gateway binary behind the opt-in flag", () => { + expect(isGatewayArgv(["C:\\bin\\openclaw.cmd", "gateway"])).toBe(true); + expect(isGatewayArgv(["/usr/local/bin/openclaw-gateway", "gateway"])).toBe(false); + expect( + isGatewayArgv(["/usr/local/bin/openclaw-gateway", "gateway"], { + allowGatewayBinary: true, + }), + ).toBe(true); + }); +}); diff --git a/src/infra/package-json.test.ts b/src/infra/package-json.test.ts new file mode 100644 index 00000000000..664fcaa4f14 --- /dev/null +++ b/src/infra/package-json.test.ts @@ -0,0 +1,39 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { withTempDir } from "../test-helpers/temp-dir.js"; +import { readPackageName, readPackageVersion } from "./package-json.js"; + +describe("package-json helpers", () => { + it("reads package version and trims package name", async () => { + await withTempDir({ prefix: "openclaw-package-json-" }, async (root) => { + await fs.writeFile( + path.join(root, "package.json"), + JSON.stringify({ version: "1.2.3", name: " @openclaw/demo " }), + "utf8", + ); + + await expect(readPackageVersion(root)).resolves.toBe("1.2.3"); + await expect(readPackageName(root)).resolves.toBe("@openclaw/demo"); + }); + }); + + it("returns null for missing or invalid package.json data", async () => { + await withTempDir({ prefix: "openclaw-package-json-" }, async (root) => { + await expect(readPackageVersion(root)).resolves.toBeNull(); + await expect(readPackageName(root)).resolves.toBeNull(); + + await fs.writeFile(path.join(root, "package.json"), "{", "utf8"); + await expect(readPackageVersion(root)).resolves.toBeNull(); + await expect(readPackageName(root)).resolves.toBeNull(); + + await fs.writeFile( + path.join(root, "package.json"), + JSON.stringify({ version: 123, name: " " }), + "utf8", + ); + await expect(readPackageVersion(root)).resolves.toBeNull(); + await expect(readPackageName(root)).resolves.toBeNull(); + }); + }); +}); diff --git a/src/infra/path-guards.test.ts b/src/infra/path-guards.test.ts new file mode 100644 index 00000000000..28bf3d7c3b8 --- /dev/null +++ b/src/infra/path-guards.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { + hasNodeErrorCode, + isNodeError, + isNotFoundPathError, + isPathInside, + isSymlinkOpenError, + normalizeWindowsPathForComparison, +} from "./path-guards.js"; + +describe("normalizeWindowsPathForComparison", () => { + it("normalizes extended-length and UNC windows paths", () => { + expect(normalizeWindowsPathForComparison("\\\\?\\C:\\Users\\Peter/Repo")).toBe( + "c:\\users\\peter\\repo", + ); + expect(normalizeWindowsPathForComparison("\\\\?\\UNC\\Server\\Share\\Folder")).toBe( + "\\\\server\\share\\folder", + ); + }); +}); + +describe("node path error helpers", () => { + it("recognizes node-style error objects and exact codes", () => { + const enoent = { code: "ENOENT" }; + + expect(isNodeError(enoent)).toBe(true); + expect(isNodeError({ message: "nope" })).toBe(false); + expect(hasNodeErrorCode(enoent, "ENOENT")).toBe(true); + expect(hasNodeErrorCode(enoent, "EACCES")).toBe(false); + }); + + it("classifies not-found and symlink-open error codes", () => { + expect(isNotFoundPathError({ code: "ENOENT" })).toBe(true); + expect(isNotFoundPathError({ code: "ENOTDIR" })).toBe(true); + expect(isNotFoundPathError({ code: "EACCES" })).toBe(false); + + expect(isSymlinkOpenError({ code: "ELOOP" })).toBe(true); + expect(isSymlinkOpenError({ code: "EINVAL" })).toBe(true); + expect(isSymlinkOpenError({ code: "ENOTSUP" })).toBe(true); + expect(isSymlinkOpenError({ code: "ENOENT" })).toBe(false); + }); +}); + +describe("isPathInside", () => { + it("accepts identical and nested paths but rejects escapes", () => { + expect(isPathInside("/workspace/root", "/workspace/root")).toBe(true); + expect(isPathInside("/workspace/root", "/workspace/root/nested/file.txt")).toBe(true); + expect(isPathInside("/workspace/root", "/workspace/root/../escape.txt")).toBe(false); + }); +}); diff --git a/src/infra/prototype-keys.test.ts b/src/infra/prototype-keys.test.ts new file mode 100644 index 00000000000..f2bd8287226 --- /dev/null +++ b/src/infra/prototype-keys.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { isBlockedObjectKey } from "./prototype-keys.js"; + +describe("isBlockedObjectKey", () => { + it("blocks prototype-pollution keys and allows ordinary keys", () => { + for (const key of ["__proto__", "prototype", "constructor"]) { + expect(isBlockedObjectKey(key)).toBe(true); + } + + for (const key of ["toString", "value", "constructorName"]) { + expect(isBlockedObjectKey(key)).toBe(false); + } + }); +});