Download src/daemon/hosted-stop.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/daemon/hosted-stop.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/daemon/hosted-stop.test.ts
-
curl -L -o hosted-stop.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/daemon/hosted-stop.test.ts
13.9 kB
| import fs from "node:fs/promises"; | |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { getFileLockProcessStartTime } from "../shared/pid-alive.js"; | |
| import { prepareHostedStopExecutor } from "./hosted-stop-executor.js"; | |
| import { prepareHostedGatewayStop, type GatewayProcessOwner } from "./hosted-stop.js"; | |
| import { probeLaunchAgentState } from "./launchd-runtime.js"; | |
| import { execSystemctl } from "./systemd-exec.js"; | |
| vi.mock("node:fs/promises", () => ({ default: { readFile: vi.fn() } })); | |
| vi.mock("../shared/pid-alive.js", () => ({ getFileLockProcessStartTime: vi.fn() })); | |
| vi.mock("./systemd-exec.js", () => ({ execSystemctl: vi.fn() })); | |
| vi.mock("./hosted-stop-executor.js", () => ({ prepareHostedStopExecutor: vi.fn() })); | |
| vi.mock("./launchd-runtime.js", () => ({ | |
| probeLaunchAgentState: vi.fn(), | |
| resolveLaunchAgentGuiDomain: () => "gui/501", | |
| })); | |
| const platform = Object.getOwnPropertyDescriptor(process, "platform")!; | |
| let cgroup = "/system.slice/openclaw-test.service"; | |
| const executorPid = 4321; | |
| const execute = vi.fn<Awaited<ReturnType<typeof prepareHostedStopExecutor>>["execute"]>(); | |
| const dispose = vi.fn<() => Promise<void>>(); | |
| const processOwner: GatewayProcessOwner = { ownsProcessLifecycle: true, supervisor: "systemd" }; | |
| const commandResult = (stdout: string) => ({ | |
| code: 0, | |
| termination: "exit" as const, | |
| stdout, | |
| stderr: "", | |
| }); | |
| const systemdFields = () => ({ | |
| Id: "openclaw-test.service", | |
| LoadState: "loaded", | |
| ActiveState: "active", | |
| SubState: "running", | |
| MainPID: String(process.pid), | |
| ControlGroup: cgroup, | |
| InvocationID: "a".repeat(32), | |
| ExecMainStartTimestampMonotonic: "100", | |
| Job: "", | |
| CanStop: "yes", | |
| RefuseManualStop: "no", | |
| }); | |
| const renderFields = (fields: Record<string, string>) => | |
| Object.entries(fields) | |
| .map(([key, value]) => `${key}=${value}`) | |
| .join("\n"); | |
| beforeEach(() => { | |
| vi.resetAllMocks(); | |
| dispose.mockResolvedValue(undefined); | |
| processOwner.supervisor = "systemd"; | |
| cgroup = "/system.slice/openclaw-test.service"; | |
| vi.stubEnv("OPENCLAW_SUPERVISOR_MODE", undefined); | |
| vi.stubEnv("OPENCLAW_LAUNCHD_LABEL", "ai.openclaw.test"); | |
| Object.defineProperty(process, "platform", { ...platform, value: "linux" }); | |
| vi.mocked(getFileLockProcessStartTime).mockReturnValue(10); | |
| vi.mocked(execSystemctl).mockImplementation(async () => | |
| commandResult(renderFields(systemdFields())), | |
| ); | |
| vi.mocked(fs.readFile).mockImplementation(async () => `0::${cgroup}\n`); | |
| vi.mocked(prepareHostedStopExecutor).mockImplementation(async (params) => { | |
| const scope = params.scopeArgs?.find((arg) => arg.startsWith("--unit="))?.slice(7); | |
| vi.mocked(fs.readFile).mockImplementation(async (file) => | |
| file === `/proc/${executorPid}/cgroup` | |
| ? `0::${cgroup.slice(0, cgroup.lastIndexOf("/"))}/${scope}\n` | |
| : `0::${cgroup}\n`, | |
| ); | |
| await params.verifyPlacement?.(executorPid); | |
| return { execute, dispose }; | |
| }); | |
| execute.mockImplementation(async (assertCurrent) => { | |
| assertCurrent(); | |
| return { disposition: "accepted", detail: "" }; | |
| }); | |
| }); | |
| afterEach(() => { | |
| Object.defineProperty(process, "platform", platform); | |
| vi.unstubAllEnvs(); | |
| }); | |
| describe("hosted native stop", () => { | |
| it.each([ | |
| { platformName: "linux", supervisor: null }, | |
| { platformName: "darwin", supervisor: null }, | |
| { platformName: "win32", supervisor: null }, | |
| { platformName: "win32", supervisor: "schtasks" }, | |
| ] as const)( | |
| "exits only the owned process on $platformName with supervisor $supervisor", | |
| async ({ platformName, supervisor }) => { | |
| Object.defineProperty(process, "platform", { ...platform, value: platformName }); | |
| const abort = new AbortController(); | |
| const stop = await prepareHostedGatewayStop( | |
| { ownsProcessLifecycle: true, supervisor }, | |
| () => {}, | |
| abort.signal, | |
| ); | |
| await expect(stop.execute(() => {})).resolves.toEqual({ outcome: "exit" }); | |
| await expect( | |
| stop.execute(() => { | |
| throw new Error("retired host"); | |
| }), | |
| ).rejects.toThrow("retired host"); | |
| abort.abort(); | |
| await expect(stop.execute(() => {})).rejects.toThrow(); | |
| await stop.dispose(); | |
| expect(getFileLockProcessStartTime).not.toHaveBeenCalled(); | |
| expect(fs.readFile).not.toHaveBeenCalled(); | |
| expect(probeLaunchAgentState).not.toHaveBeenCalled(); | |
| expect(execSystemctl).not.toHaveBeenCalled(); | |
| expect(prepareHostedStopExecutor).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| it.skipIf(typeof process.getuid !== "function")( | |
| "keeps a user service on its exact user manager", | |
| async () => { | |
| cgroup = `/user.slice/user-${process.getuid!()}.slice/user@${process.getuid!()}.service/app.slice/openclaw-test.service`; | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| expect(prepareHostedStopExecutor).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| scopeArgs: expect.arrayContaining(["--user", "--scope"]), | |
| command: expect.arrayContaining(["--user"]), | |
| }), | |
| ); | |
| await expect(stop.execute(() => {})).resolves.toEqual({ outcome: "accepted" }); | |
| expect(execSystemctl).toHaveBeenCalled(); | |
| expect(vi.mocked(execSystemctl).mock.calls.every(([args]) => args[0] === "--user")).toBe( | |
| true, | |
| ); | |
| await stop.dispose(); | |
| }, | |
| ); | |
| it.each([ | |
| ["CanStop", "no"], | |
| ["RefuseManualStop", "yes"], | |
| ])("rejects %s=%s before acceptance and reports a late refusal", async (field, value) => { | |
| vi.mocked(execSystemctl).mockResolvedValue( | |
| commandResult(renderFields({ ...systemdFields(), [field]: value })), | |
| ); | |
| await expect( | |
| prepareHostedGatewayStop(processOwner, () => {}, new AbortController().signal), | |
| ).rejects.toThrow("does not permit manual stop"); | |
| expect(prepareHostedStopExecutor).not.toHaveBeenCalled(); | |
| vi.mocked(execSystemctl).mockResolvedValue(commandResult(renderFields(systemdFields()))); | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| vi.mocked(execSystemctl).mockResolvedValue( | |
| commandResult(renderFields({ ...systemdFields(), [field]: value })), | |
| ); | |
| await expect(stop.execute(() => {})).resolves.toMatchObject({ outcome: "refused" }); | |
| expect(execute).not.toHaveBeenCalled(); | |
| await stop.dispose(); | |
| }); | |
| it("prepares an independent control scope and acknowledges native acceptance only at execution", async () => { | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| expect(execute).not.toHaveBeenCalled(); | |
| expect(prepareHostedStopExecutor).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| command: [ | |
| "systemctl", | |
| "--system", | |
| "--no-ask-password", | |
| "--no-block", | |
| "stop", | |
| "openclaw-test.service", | |
| ], | |
| scopeArgs: expect.arrayContaining(["--system", "--scope"]), | |
| }), | |
| ); | |
| await expect(stop.execute(() => {})).resolves.toEqual({ outcome: "accepted" }); | |
| expect(execSystemctl).toHaveBeenCalled(); | |
| expect(vi.mocked(execSystemctl).mock.calls.every(([args]) => args[0] === "--system")).toBe( | |
| true, | |
| ); | |
| await stop.dispose(); | |
| }); | |
| it.each([ | |
| ["Id", "other.service"], | |
| ["MainPID", "999999"], | |
| ["InvocationID", "b".repeat(32)], | |
| ["ExecMainStartTimestampMonotonic", "101"], | |
| ["Job", "23"], | |
| ["ControlGroup", "/system.slice/other.service"], | |
| ])("rejects changed %s during preparation before accepting stop", async (field, value) => { | |
| vi.mocked(execSystemctl) | |
| .mockResolvedValueOnce(commandResult(renderFields(systemdFields()))) | |
| .mockResolvedValue(commandResult(renderFields({ ...systemdFields(), [field]: value }))); | |
| await expect( | |
| prepareHostedGatewayStop(processOwner, () => {}, new AbortController().signal), | |
| ).rejects.toThrow(/systemd|instance/); | |
| expect(execute).not.toHaveBeenCalled(); | |
| expect(dispose).toHaveBeenCalledOnce(); | |
| }); | |
| it.each(["cgroup", "unit"])( | |
| "rejects caller revocation during awaited %s discovery", | |
| async (discovery) => { | |
| const assertCaller = vi.fn(); | |
| const revoke = () => { | |
| assertCaller.mockImplementation(() => { | |
| throw new Error("closed caller"); | |
| }); | |
| }; | |
| if (discovery === "cgroup") { | |
| vi.mocked(fs.readFile).mockImplementationOnce(async () => { | |
| revoke(); | |
| return `0::${cgroup}\n`; | |
| }); | |
| } else { | |
| vi.mocked(execSystemctl).mockImplementationOnce(async () => { | |
| revoke(); | |
| return commandResult(renderFields(systemdFields())); | |
| }); | |
| } | |
| await expect( | |
| prepareHostedGatewayStop(processOwner, assertCaller, new AbortController().signal), | |
| ).rejects.toThrow("closed caller"); | |
| expect(prepareHostedStopExecutor).not.toHaveBeenCalled(); | |
| expect(execute).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| it("disposes a prepared executor when its caller is revoked across the await", async () => { | |
| const assertCaller = vi.fn(); | |
| vi.mocked(prepareHostedStopExecutor).mockImplementationOnce(async () => { | |
| assertCaller.mockImplementation(() => { | |
| throw new Error("closed caller"); | |
| }); | |
| return { execute, dispose }; | |
| }); | |
| await expect( | |
| prepareHostedGatewayStop(processOwner, assertCaller, new AbortController().signal), | |
| ).rejects.toThrow("closed caller"); | |
| expect(execute).not.toHaveBeenCalled(); | |
| expect(dispose).toHaveBeenCalledOnce(); | |
| }); | |
| it.each(["refused", "uncertain"] as const)( | |
| "classifies a %s result without manufacturing completion", | |
| async (disposition) => { | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| execute.mockResolvedValueOnce({ disposition, detail: "native fault" }); | |
| await expect(stop.execute(() => {})).resolves.toMatchObject({ | |
| outcome: disposition, | |
| detail: expect.stringContaining("native fault"), | |
| }); | |
| // Reopening requires a fresh native identity query after an explicit refusal. | |
| expect(execSystemctl).toHaveBeenCalledTimes(disposition === "refused" ? 4 : 3); | |
| await stop.dispose(); | |
| }, | |
| ); | |
| it.each([ | |
| ["InvocationID", "b".repeat(32)], | |
| ["ExecMainStartTimestampMonotonic", "101"], | |
| ])("does not execute against a replaced %s at the terminal boundary", async (field, value) => { | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| vi.mocked(execSystemctl).mockResolvedValue( | |
| commandResult(renderFields({ ...systemdFields(), [field]: value })), | |
| ); | |
| await expect(stop.execute(() => {})).resolves.toMatchObject({ outcome: "uncertain" }); | |
| expect(execute).not.toHaveBeenCalled(); | |
| await stop.dispose(); | |
| }); | |
| it("does not execute when the terminal owner is revoked during the native identity query", async () => { | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| const assertCurrent = vi.fn(); | |
| vi.mocked(execSystemctl).mockImplementationOnce(async () => { | |
| assertCurrent.mockImplementation(() => { | |
| throw new Error("retired host"); | |
| }); | |
| return commandResult(renderFields(systemdFields())); | |
| }); | |
| await expect(stop.execute(assertCurrent)).resolves.toEqual({ | |
| outcome: "uncertain", | |
| detail: "retired host", | |
| }); | |
| expect(execute).not.toHaveBeenCalled(); | |
| await stop.dispose(); | |
| }); | |
| it("uses canonical macOS job status for ordinary bootout without persistent mutation", async () => { | |
| Object.defineProperty(process, "platform", { ...platform, value: "darwin" }); | |
| processOwner.supervisor = "launchd"; | |
| vi.mocked(probeLaunchAgentState).mockResolvedValue({ | |
| state: "running", | |
| // Native launchctl prints nested coalition state after the running job's state. | |
| runtime: { pid: process.pid, state: "active" }, | |
| }); | |
| const stop = await prepareHostedGatewayStop( | |
| processOwner, | |
| () => {}, | |
| new AbortController().signal, | |
| ); | |
| expect(prepareHostedStopExecutor).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| command: ["/bin/launchctl", "bootout", "gui/501/ai.openclaw.test"], | |
| }), | |
| ); | |
| expect(execute).not.toHaveBeenCalled(); | |
| await expect(stop.execute(() => {})).resolves.toEqual({ outcome: "accepted" }); | |
| await stop.dispose(); | |
| }); | |
| it("rejects an unrelated launchd PID before preparing an executor", async () => { | |
| Object.defineProperty(process, "platform", { ...platform, value: "darwin" }); | |
| processOwner.supervisor = "launchd"; | |
| vi.mocked(probeLaunchAgentState).mockResolvedValue({ | |
| state: "running", | |
| runtime: { pid: 999999, state: "running" }, | |
| }); | |
| await expect( | |
| prepareHostedGatewayStop(processOwner, () => {}, new AbortController().signal), | |
| ).rejects.toThrow("exact Gateway"); | |
| expect(prepareHostedStopExecutor).not.toHaveBeenCalled(); | |
| }); | |
| it.each(["external", "unowned Windows process"])( | |
| "rejects an unsupported %s owner while still serving", | |
| async (owner) => { | |
| if (owner === "external") { | |
| processOwner.supervisor = "external"; | |
| } else { | |
| Object.defineProperty(process, "platform", { ...platform, value: "win32" }); | |
| processOwner.supervisor = "schtasks"; | |
| } | |
| await expect( | |
| prepareHostedGatewayStop( | |
| { ...processOwner, ownsProcessLifecycle: owner === "external" }, | |
| () => {}, | |
| new AbortController().signal, | |
| ), | |
| ).rejects.toThrow(/external supervisor|does not own the process lifecycle/); | |
| expect(prepareHostedStopExecutor).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| }); | |