Files
openclaw/src/auto-reply/reply/bash-command.stop.test.ts
mappel-nv 9d58f9e24f Replace killProcessTree references to shell-utils with process/kill-tree (#55213)
* Replace killProcessTree references to shell-utils with process/kill-tree

* Address grace timeout comment

* Align with existing process kill behavior

* bash: fail stop without pid

* bash: lazy-load kill tree on stop

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
2026-03-27 11:25:56 +00:00

120 lines
3.6 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 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();
});
});