mirror of https://github.com/openclaw/openclaw.git
test: add socket and ssh helper coverage
This commit is contained in:
parent
dbef3dfef0
commit
c3fadff0ce
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const requestJsonlSocketMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("./jsonl-socket.js", () => ({
|
||||||
|
requestJsonlSocket: (...args: unknown[]) => requestJsonlSocketMock(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { requestExecHostViaSocket } from "./exec-host.js";
|
||||||
|
|
||||||
|
describe("requestExecHostViaSocket", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
requestJsonlSocketMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when socket credentials are missing", async () => {
|
||||||
|
await expect(
|
||||||
|
requestExecHostViaSocket({
|
||||||
|
socketPath: "",
|
||||||
|
token: "secret",
|
||||||
|
request: { command: ["echo", "hi"] },
|
||||||
|
}),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
await expect(
|
||||||
|
requestExecHostViaSocket({
|
||||||
|
socketPath: "/tmp/socket",
|
||||||
|
token: "",
|
||||||
|
request: { command: ["echo", "hi"] },
|
||||||
|
}),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
expect(requestJsonlSocketMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds an exec payload and forwards the default timeout", async () => {
|
||||||
|
requestJsonlSocketMock.mockResolvedValueOnce({ ok: true, payload: { success: true } });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
requestExecHostViaSocket({
|
||||||
|
socketPath: "/tmp/socket",
|
||||||
|
token: "secret",
|
||||||
|
request: {
|
||||||
|
command: ["echo", "hi"],
|
||||||
|
cwd: "/tmp",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ ok: true, payload: { success: true } });
|
||||||
|
|
||||||
|
const call = requestJsonlSocketMock.mock.calls[0]?.[0] as
|
||||||
|
| {
|
||||||
|
socketPath: string;
|
||||||
|
payload: string;
|
||||||
|
timeoutMs: number;
|
||||||
|
accept: (msg: unknown) => unknown;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
if (!call) {
|
||||||
|
throw new Error("expected requestJsonlSocket call");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(call.socketPath).toBe("/tmp/socket");
|
||||||
|
expect(call.timeoutMs).toBe(20_000);
|
||||||
|
const payload = JSON.parse(call.payload) as {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
nonce: string;
|
||||||
|
ts: number;
|
||||||
|
hmac: string;
|
||||||
|
requestJson: string;
|
||||||
|
};
|
||||||
|
expect(payload.type).toBe("exec");
|
||||||
|
expect(payload.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
||||||
|
expect(payload.nonce).toMatch(/^[0-9a-f]{32}$/);
|
||||||
|
expect(typeof payload.ts).toBe("number");
|
||||||
|
expect(payload.hmac).toMatch(/^[0-9a-f]{64}$/);
|
||||||
|
expect(JSON.parse(payload.requestJson)).toEqual({
|
||||||
|
command: ["echo", "hi"],
|
||||||
|
cwd: "/tmp",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts only exec response messages and maps malformed matches to null", async () => {
|
||||||
|
requestJsonlSocketMock.mockImplementationOnce(async ({ accept }) => {
|
||||||
|
expect(accept({ type: "ignore" })).toBeUndefined();
|
||||||
|
expect(accept({ type: "exec-res", ok: true, payload: { success: true } })).toEqual({
|
||||||
|
ok: true,
|
||||||
|
payload: { success: true },
|
||||||
|
});
|
||||||
|
expect(accept({ type: "exec-res", ok: false, error: { code: "DENIED" } })).toEqual({
|
||||||
|
ok: false,
|
||||||
|
error: { code: "DENIED" },
|
||||||
|
});
|
||||||
|
expect(accept({ type: "exec-res", ok: true })).toBeNull();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
requestExecHostViaSocket({
|
||||||
|
socketPath: "/tmp/socket",
|
||||||
|
token: "secret",
|
||||||
|
timeoutMs: 123,
|
||||||
|
request: { command: ["echo", "hi"] },
|
||||||
|
}),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(requestJsonlSocketMock.mock.calls[0]?.[0] as { timeoutMs?: number } | undefined)?.timeoutMs,
|
||||||
|
).toBe(123);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { parseSshTarget } from "./ssh-tunnel.js";
|
|
||||||
|
|
||||||
describe("infra parsing", () => {
|
|
||||||
describe("parseSshTarget", () => {
|
|
||||||
it("parses user@host:port targets", () => {
|
|
||||||
expect(parseSshTarget("me@example.com:2222")).toEqual({
|
|
||||||
user: "me",
|
|
||||||
host: "example.com",
|
|
||||||
port: 2222,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses host-only targets with default port", () => {
|
|
||||||
expect(parseSshTarget("example.com")).toEqual({
|
|
||||||
user: undefined,
|
|
||||||
host: "example.com",
|
|
||||||
port: 22,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects hostnames that start with '-'", () => {
|
|
||||||
expect(parseSshTarget("-V")).toBeNull();
|
|
||||||
expect(parseSshTarget("me@-badhost")).toBeNull();
|
|
||||||
expect(parseSshTarget("-oProxyCommand=echo")).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import net from "node:net";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||||
|
import { requestJsonlSocket } from "./jsonl-socket.js";
|
||||||
|
|
||||||
|
describe.runIf(process.platform !== "win32")("requestJsonlSocket", () => {
|
||||||
|
it("ignores malformed and non-accepted lines until one is accepted", async () => {
|
||||||
|
await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
|
||||||
|
const socketPath = path.join(dir, "socket.sock");
|
||||||
|
const server = net.createServer((socket) => {
|
||||||
|
socket.on("data", () => {
|
||||||
|
socket.write("{bad json}\n");
|
||||||
|
socket.write('{"type":"ignore"}\n');
|
||||||
|
socket.write('{"type":"done","value":42}\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(socketPath, resolve));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(
|
||||||
|
requestJsonlSocket({
|
||||||
|
socketPath,
|
||||||
|
payload: '{"hello":"world"}',
|
||||||
|
timeoutMs: 500,
|
||||||
|
accept: (msg) => {
|
||||||
|
const value = msg as { type?: string; value?: number };
|
||||||
|
return value.type === "done" ? (value.value ?? null) : undefined;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toBe(42);
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null on timeout and on socket errors", async () => {
|
||||||
|
await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
|
||||||
|
const socketPath = path.join(dir, "socket.sock");
|
||||||
|
const server = net.createServer(() => {
|
||||||
|
// Intentionally never reply.
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(socketPath, resolve));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(
|
||||||
|
requestJsonlSocket({
|
||||||
|
socketPath,
|
||||||
|
payload: "{}",
|
||||||
|
timeoutMs: 50,
|
||||||
|
accept: () => undefined,
|
||||||
|
}),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
requestJsonlSocket({
|
||||||
|
socketPath,
|
||||||
|
payload: "{}",
|
||||||
|
timeoutMs: 50,
|
||||||
|
accept: () => undefined,
|
||||||
|
}),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseSshTarget } from "./ssh-tunnel.js";
|
||||||
|
|
||||||
|
describe("parseSshTarget", () => {
|
||||||
|
it("parses user@host:port targets", () => {
|
||||||
|
expect(parseSshTarget("me@example.com:2222")).toEqual({
|
||||||
|
user: "me",
|
||||||
|
host: "example.com",
|
||||||
|
port: 2222,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips an ssh prefix and keeps the default port when missing", () => {
|
||||||
|
expect(parseSshTarget(" ssh alice@example.com ")).toEqual({
|
||||||
|
user: "alice",
|
||||||
|
host: "example.com",
|
||||||
|
port: 22,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid hosts and ports", () => {
|
||||||
|
expect(parseSshTarget("")).toBeNull();
|
||||||
|
expect(parseSshTarget("me@example.com:0")).toBeNull();
|
||||||
|
expect(parseSshTarget("me@example.com:not-a-port")).toBeNull();
|
||||||
|
expect(parseSshTarget("-V")).toBeNull();
|
||||||
|
expect(parseSshTarget("me@-badhost")).toBeNull();
|
||||||
|
expect(parseSshTarget("-oProxyCommand=echo")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue