Files
openclaw/src/auto-reply/reply/bash-command.stop.test.ts
2026-04-11 01:17:51 +01:00

172 lines
5.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { MsgContext } from "../templating.js";
const { getSessionMock, getFinishedSessionMock, killProcessTreeMock } = vi.hoisted(() => ({
getSessionMock: vi.fn(),
getFinishedSessionMock: vi.fn(),
killProcessTreeMock: vi.fn(),
}));
vi.mock("../../agents/bash-process-registry.js", () => ({
getSession: getSessionMock,
getFinishedSession: getFinishedSessionMock,
markExited: vi.fn(),
}));
vi.mock("../../process/kill-tree.js", () => ({
killProcessTree: killProcessTreeMock,
}));
const { handleBashChatCommand } = await import("./bash-command.js");
function buildParams(commandBody: string) {
const cfg = {
commands: { bash: true },
} as OpenClawConfig;
const ctx = {
CommandBody: commandBody,
SessionKey: "session-key",
} as MsgContext;
return {
ctx,
cfg,
sessionKey: "session-key",
isGroup: false,
elevated: {
enabled: true,
allowed: true,
failures: [],
},
};
}
function buildElevatedDeniedParams(commandBody: string) {
const base = buildParams(commandBody);
return {
...base,
ctx: {
...base.ctx,
SessionKey: "agent:main:telegram:slash-session",
} as MsgContext,
agentId: "main",
sessionKey: "agent:target:telegram:direct:target-session",
elevated: {
enabled: true,
allowed: false,
failures: [],
},
};
}
function buildRunningSession(overrides?: Record<string, unknown>) {
return {
id: "session-1",
scopeKey: "chat:bash",
backgrounded: true,
pid: 4242,
exited: false,
startedAt: Date.now(),
tail: "",
...overrides,
};
}
describe("handleBashChatCommand stop", () => {
beforeEach(() => {
getSessionMock.mockReset();
getFinishedSessionMock.mockReset();
killProcessTreeMock.mockReset();
});
it("returns immediately with a stopping message and fires killProcessTree", async () => {
const session = buildRunningSession();
getSessionMock.mockReturnValue(session);
getFinishedSessionMock.mockReturnValue(undefined);
const result = await handleBashChatCommand(buildParams("/bash stop session-1"));
expect(result.text).toContain("bash stopping");
expect(result.text).toContain("!poll session-1");
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
});
it("includes the full session ID so the user can poll after starting a new job", async () => {
const session = buildRunningSession({ id: "deep-forest-42" });
getSessionMock.mockReturnValue(session);
getFinishedSessionMock.mockReturnValue(undefined);
const result = await handleBashChatCommand(buildParams("/bash stop deep-forest-42"));
expect(result.text).toContain("!poll deep-forest-42");
});
it("does not call markExited synchronously (defers to supervisor lifecycle)", async () => {
const session = buildRunningSession();
getSessionMock.mockReturnValue(session);
getFinishedSessionMock.mockReturnValue(undefined);
await handleBashChatCommand(buildParams("/bash stop session-1"));
expect(session.exited).toBe(false);
});
it("returns no-running-job when session is not found", async () => {
getSessionMock.mockReturnValue(undefined);
getFinishedSessionMock.mockReturnValue(undefined);
const result = await handleBashChatCommand(buildParams("/bash stop session-1"));
expect(result.text).toContain("No running bash job found");
expect(killProcessTreeMock).not.toHaveBeenCalled();
});
it("fails stop when session has no pid", async () => {
const session = buildRunningSession({ pid: undefined, child: undefined });
getSessionMock.mockReturnValue(session);
getFinishedSessionMock.mockReturnValue(undefined);
const result = await handleBashChatCommand(buildParams("/bash stop session-1"));
expect(result.text).toContain("Unable to stop bash session");
expect(result.text).toContain("!poll session-1");
expect(killProcessTreeMock).not.toHaveBeenCalled();
});
it("uses the canonical target session for elevated sandbox explanation", async () => {
const sandboxRuntime = await import("../../agents/sandbox.js");
const resolveSandboxRuntimeStatusSpy = vi
.spyOn(sandboxRuntime, "resolveSandboxRuntimeStatus")
.mockReturnValue({
agentId: "target",
sessionKey: "agent:target:telegram:direct:target-session",
mainSessionKey: "agent:target:main",
mode: "non-main",
sandboxed: true,
toolPolicy: {
allow: [],
deny: ["bash"],
sources: {
allow: { source: "default", key: "agents.defaults.tools.sandbox.tools.allow" },
deny: { source: "default", key: "agents.defaults.tools.sandbox.tools.deny" },
},
},
});
const result = await handleBashChatCommand(buildElevatedDeniedParams("/bash pwd"));
expect(resolveSandboxRuntimeStatusSpy).toHaveBeenCalledWith({
cfg: expect.any(Object),
sessionKey: "agent:target:telegram:direct:target-session",
});
expect(result.text).toContain(
"openclaw sandbox explain --session agent:target:telegram:direct:target-session",
);
expect(result.text).not.toContain(
"openclaw sandbox explain --session agent:main:telegram:slash-session",
);
});
});