melbot / src /gateway /server-methods /exec-approval.test.ts
amos-fernandes's picture
Upload 4501 files
3a65265 verified
import { describe, expect, it, vi } from "vitest";
import { ExecApprovalManager } from "../exec-approval-manager.js";
import { createExecApprovalHandlers } from "./exec-approval.js";
import { validateExecApprovalRequestParams } from "../protocol/index.js";
const noop = () => {};
describe("exec approval handlers", () => {
describe("ExecApprovalRequestParams validation", () => {
it("accepts request with resolvedPath omitted", () => {
const params = {
command: "echo hi",
cwd: "/tmp",
host: "node",
};
expect(validateExecApprovalRequestParams(params)).toBe(true);
});
it("accepts request with resolvedPath as string", () => {
const params = {
command: "echo hi",
cwd: "/tmp",
host: "node",
resolvedPath: "/usr/bin/echo",
};
expect(validateExecApprovalRequestParams(params)).toBe(true);
});
it("accepts request with resolvedPath as undefined", () => {
const params = {
command: "echo hi",
cwd: "/tmp",
host: "node",
resolvedPath: undefined,
};
expect(validateExecApprovalRequestParams(params)).toBe(true);
});
// Fixed: null is now accepted (Type.Union([Type.String(), Type.Null()]))
// This matches the calling code in bash-tools.exec.ts which passes null.
it("accepts request with resolvedPath as null", () => {
const params = {
command: "echo hi",
cwd: "/tmp",
host: "node",
resolvedPath: null,
};
expect(validateExecApprovalRequestParams(params)).toBe(true);
});
});
it("broadcasts request + resolve", async () => {
const manager = new ExecApprovalManager();
const handlers = createExecApprovalHandlers(manager);
const broadcasts: Array<{ event: string; payload: unknown }> = [];
const respond = vi.fn();
const context = {
broadcast: (event: string, payload: unknown) => {
broadcasts.push({ event, payload });
},
};
const requestPromise = handlers["exec.approval.request"]({
params: {
command: "echo ok",
cwd: "/tmp",
host: "node",
timeoutMs: 2000,
},
respond,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.request"]
>[0]["context"],
client: null,
req: { id: "req-1", type: "req", method: "exec.approval.request" },
isWebchatConnect: noop,
});
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
expect(requested).toBeTruthy();
const id = (requested?.payload as { id?: string })?.id ?? "";
expect(id).not.toBe("");
const resolveRespond = vi.fn();
await handlers["exec.approval.resolve"]({
params: { id, decision: "allow-once" },
respond: resolveRespond,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.resolve"]
>[0]["context"],
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
isWebchatConnect: noop,
});
await requestPromise;
expect(resolveRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({ id, decision: "allow-once" }),
undefined,
);
expect(broadcasts.some((entry) => entry.event === "exec.approval.resolved")).toBe(true);
});
it("accepts resolve during broadcast", async () => {
const manager = new ExecApprovalManager();
const handlers = createExecApprovalHandlers(manager);
const respond = vi.fn();
const resolveRespond = vi.fn();
const resolveContext = {
broadcast: () => {},
};
const context = {
broadcast: (event: string, payload: unknown) => {
if (event !== "exec.approval.requested") return;
const id = (payload as { id?: string })?.id ?? "";
void handlers["exec.approval.resolve"]({
params: { id, decision: "allow-once" },
respond: resolveRespond,
context: resolveContext as unknown as Parameters<
(typeof handlers)["exec.approval.resolve"]
>[0]["context"],
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
isWebchatConnect: noop,
});
},
};
await handlers["exec.approval.request"]({
params: {
command: "echo ok",
cwd: "/tmp",
host: "node",
timeoutMs: 2000,
},
respond,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.request"]
>[0]["context"],
client: null,
req: { id: "req-1", type: "req", method: "exec.approval.request" },
isWebchatConnect: noop,
});
expect(resolveRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({ decision: "allow-once" }),
undefined,
);
});
it("accepts explicit approval ids", async () => {
const manager = new ExecApprovalManager();
const handlers = createExecApprovalHandlers(manager);
const broadcasts: Array<{ event: string; payload: unknown }> = [];
const respond = vi.fn();
const context = {
broadcast: (event: string, payload: unknown) => {
broadcasts.push({ event, payload });
},
};
const requestPromise = handlers["exec.approval.request"]({
params: {
id: "approval-123",
command: "echo ok",
cwd: "/tmp",
host: "gateway",
timeoutMs: 2000,
},
respond,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.request"]
>[0]["context"],
client: null,
req: { id: "req-1", type: "req", method: "exec.approval.request" },
isWebchatConnect: noop,
});
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
const id = (requested?.payload as { id?: string })?.id ?? "";
expect(id).toBe("approval-123");
const resolveRespond = vi.fn();
await handlers["exec.approval.resolve"]({
params: { id, decision: "allow-once" },
respond: resolveRespond,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.resolve"]
>[0]["context"],
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
isWebchatConnect: noop,
});
await requestPromise;
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({ id: "approval-123", decision: "allow-once" }),
undefined,
);
});
it("rejects duplicate approval ids", async () => {
const manager = new ExecApprovalManager();
const handlers = createExecApprovalHandlers(manager);
const respondA = vi.fn();
const respondB = vi.fn();
const broadcasts: Array<{ event: string; payload: unknown }> = [];
const context = {
broadcast: (event: string, payload: unknown) => {
broadcasts.push({ event, payload });
},
};
const requestPromise = handlers["exec.approval.request"]({
params: {
id: "dup-1",
command: "echo ok",
},
respond: respondA,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.request"]
>[0]["context"],
client: null,
req: { id: "req-1", type: "req", method: "exec.approval.request" },
isWebchatConnect: noop,
});
await handlers["exec.approval.request"]({
params: {
id: "dup-1",
command: "echo again",
},
respond: respondB,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.request"]
>[0]["context"],
client: null,
req: { id: "req-2", type: "req", method: "exec.approval.request" },
isWebchatConnect: noop,
});
expect(respondB).toHaveBeenCalledWith(
false,
undefined,
expect.objectContaining({ message: "approval id already pending" }),
);
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
const id = (requested?.payload as { id?: string })?.id ?? "";
const resolveRespond = vi.fn();
await handlers["exec.approval.resolve"]({
params: { id, decision: "deny" },
respond: resolveRespond,
context: context as unknown as Parameters<
(typeof handlers)["exec.approval.resolve"]
>[0]["context"],
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
req: { id: "req-3", type: "req", method: "exec.approval.resolve" },
isWebchatConnect: noop,
});
await requestPromise;
});
});