Download src/cli/gateway-cli/host-lifecycle.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 9.12 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/gateway-cli/host-lifecycle.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/gateway-cli/host-lifecycle.test.ts
-
curl -L -o host-lifecycle.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/gateway-cli/host-lifecycle.test.ts
9.12 kB
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { prepareHostedGatewayStop, type HostedGatewayStop } from "../../daemon/hosted-stop.js"; | |
| import { getGatewayProcessInstanceId } from "../../gateway/process-instance.js"; | |
| import { | |
| armGatewaySuspendHandoff, | |
| consumeGatewaySuspendHandoff, | |
| prepareGatewaySuspend, | |
| resumeGatewaySuspend, | |
| } from "../../infra/gateway-suspend-coordinator.js"; | |
| import { scheduleSafeGatewayRestart } from "../../infra/restart-coordinator.js"; | |
| import { createGatewayHostLifecycle } from "./host-lifecycle.js"; | |
| vi.mock("../../daemon/hosted-stop.js", () => ({ prepareHostedGatewayStop: vi.fn() })); | |
| vi.mock("../../infra/restart-coordinator.js", () => ({ scheduleSafeGatewayRestart: vi.fn() })); | |
| describe("Gateway host lifecycle authority", () => { | |
| let current = true; | |
| let serving = true; | |
| const acceptStop = vi.fn(); | |
| const execute = vi.fn<HostedGatewayStop["execute"]>(); | |
| const dispose = vi.fn<HostedGatewayStop["dispose"]>(); | |
| const owners: ReturnType<typeof createGatewayHostLifecycle>[] = []; | |
| const owner = () => { | |
| const host = createGatewayHostLifecycle({ | |
| isCurrent: () => current, | |
| isServing: () => serving, | |
| acceptStop, | |
| processOwner: { ownsProcessLifecycle: true, supervisor: null }, | |
| }); | |
| owners.push(host); | |
| return host; | |
| }; | |
| beforeEach(() => { | |
| vi.resetAllMocks(); | |
| dispose.mockResolvedValue(undefined); | |
| current = serving = true; | |
| execute.mockResolvedValue({ outcome: "accepted" }); | |
| vi.mocked(prepareHostedGatewayStop).mockResolvedValue({ execute, dispose }); | |
| acceptStop.mockImplementation(() => { | |
| serving = false; | |
| }); | |
| }); | |
| afterEach(async () => { | |
| for (const host of owners.splice(0)) { | |
| await host.retire(); | |
| } | |
| }); | |
| it("reports an already-running start and a local scheduled restart without native discovery", async () => { | |
| const host = owner(); | |
| const guard = vi.fn(); | |
| await expect(host.capability.request("start", guard)).resolves.toEqual({ | |
| ok: true, | |
| value: { outcome: "already-running" }, | |
| }); | |
| await expect(host.capability.request("restart", guard)).resolves.toEqual({ | |
| ok: true, | |
| value: { outcome: "scheduled" }, | |
| }); | |
| expect(prepareHostedGatewayStop).not.toHaveBeenCalled(); | |
| expect(scheduleSafeGatewayRestart).toHaveBeenCalledExactlyOnceWith({ | |
| reason: "gateway.restart.safe", | |
| delayMs: 0, | |
| }); | |
| expect(guard).toHaveBeenCalledTimes(2); | |
| }); | |
| it("transfers only the accepted stop across teardown, without awaiting native completion", async () => { | |
| const host = owner(); | |
| await expect(host.capability.request("stop", () => {})).resolves.toEqual({ | |
| ok: true, | |
| value: { outcome: "scheduled" }, | |
| }); | |
| expect(acceptStop).toHaveBeenCalledOnce(); | |
| expect(execute).not.toHaveBeenCalled(); | |
| await expect(host.capability.request("start", () => {})).resolves.toMatchObject({ ok: false }); | |
| await expect(host.finishStop()).resolves.toEqual({ outcome: "accepted" }); | |
| expect(execute).toHaveBeenCalledOnce(); | |
| await expect(host.finishStop()).resolves.toEqual({ outcome: "retired" }); | |
| }); | |
| it.each(["retired", "replaced", "closed", "approval revoked"])( | |
| "rejects after awaited preparation when %s", | |
| async (change) => { | |
| const host = owner(); | |
| let approve = true; | |
| let resolvePreparation!: (stop: HostedGatewayStop) => void; | |
| vi.mocked(prepareHostedGatewayStop).mockImplementation( | |
| () => | |
| new Promise((resolve) => { | |
| resolvePreparation = resolve; | |
| }), | |
| ); | |
| const request = host.capability.request("stop", () => { | |
| if (!approve) { | |
| throw new Error("approval revoked"); | |
| } | |
| }); | |
| const retirement = change === "retired" ? host.retire() : undefined; | |
| if (change === "replaced") { | |
| current = false; | |
| } | |
| if (change === "closed") { | |
| serving = false; | |
| } | |
| if (change === "approval revoked") { | |
| approve = false; | |
| } | |
| resolvePreparation({ execute, dispose }); | |
| await expect(request).resolves.toMatchObject({ ok: false }); | |
| await retirement; | |
| expect(acceptStop).not.toHaveBeenCalled(); | |
| expect(execute).not.toHaveBeenCalled(); | |
| expect(dispose).toHaveBeenCalledOnce(); | |
| }, | |
| ); | |
| it("keeps serving on preparation failure and preserves a concurrent preparation owner", async () => { | |
| const host = owner(); | |
| let rejectPreparation!: (error: Error) => void; | |
| vi.mocked(prepareHostedGatewayStop).mockImplementationOnce( | |
| () => | |
| new Promise((_resolve, reject) => { | |
| rejectPreparation = reject; | |
| }), | |
| ); | |
| const first = host.capability.request("stop", () => {}); | |
| await expect(host.capability.request("stop", () => {})).resolves.toMatchObject({ ok: false }); | |
| await expect(host.capability.request("stop", () => {})).resolves.toMatchObject({ ok: false }); | |
| expect(prepareHostedGatewayStop).toHaveBeenCalledOnce(); | |
| rejectPreparation(new Error("native permission denied")); | |
| await expect(first).resolves.toEqual({ ok: false, error: "native permission denied" }); | |
| expect(acceptStop).not.toHaveBeenCalled(); | |
| await expect(host.capability.request("start", () => {})).resolves.toMatchObject({ ok: true }); | |
| }); | |
| it("retires accepted native authority on preemption", async () => { | |
| const host = owner(); | |
| await host.capability.request("stop", () => {}); | |
| await host.retire(); | |
| await expect(host.finishStop()).resolves.toEqual({ outcome: "retired" }); | |
| expect(execute).not.toHaveBeenCalled(); | |
| }); | |
| it("does not carry an armed lease into another host iteration in the same process", async () => { | |
| const first = owner(); | |
| const processInstanceId = getGatewayProcessInstanceId(); | |
| const lease = prepareGatewaySuspend({ | |
| requestId: "host-iteration-handoff", | |
| drain: true, | |
| pauseScheduling: () => {}, | |
| resumeScheduling: () => {}, | |
| inspect: { getRootRequests: () => 1, getTerminalPersistence: () => 0 }, | |
| }); | |
| if (lease.status !== "draining" || !first.capability.externalRestart) { | |
| throw new Error("missing owned lease"); | |
| } | |
| try { | |
| expect( | |
| armGatewaySuspendHandoff({ | |
| suspensionId: lease.suspensionId, | |
| owner: first.capability.externalRestart, | |
| }).ok, | |
| ).toBe(true); | |
| await first.retire(); | |
| const next = owner(); | |
| expect(getGatewayProcessInstanceId()).toBe(processInstanceId); | |
| expect(first.capability.externalRestart.isCurrent()).toBe(false); | |
| expect(next.capability.externalRestart?.isCurrent()).toBe(true); | |
| expect(consumeGatewaySuspendHandoff(first.capability.externalRestart)).toEqual({ | |
| ok: true, | |
| value: false, | |
| }); | |
| expect(consumeGatewaySuspendHandoff(next.capability.externalRestart)).toEqual({ | |
| ok: true, | |
| value: false, | |
| }); | |
| } finally { | |
| resumeGatewaySuspend(lease.suspensionId); | |
| } | |
| }); | |
| it.each(["preparing", "accepted"])( | |
| "joins %s executor cleanup before retiring the owner", | |
| async (phase) => { | |
| const host = owner(); | |
| let finishClose!: () => void; | |
| dispose.mockImplementation( | |
| () => | |
| new Promise((resolve) => { | |
| finishClose = resolve; | |
| }), | |
| ); | |
| let finishPrepare!: (stop: HostedGatewayStop) => void; | |
| if (phase === "preparing") { | |
| vi.mocked(prepareHostedGatewayStop).mockImplementationOnce( | |
| () => | |
| new Promise((resolve) => { | |
| finishPrepare = resolve; | |
| }), | |
| ); | |
| } | |
| const request = host.capability.request("stop", () => {}); | |
| if (phase === "accepted") { | |
| await request; | |
| } | |
| let retired = false; | |
| const retirement = host.retire().then(() => { | |
| retired = true; | |
| }); | |
| if (phase === "preparing") { | |
| finishPrepare({ execute, dispose }); | |
| } | |
| await expect(host.capability.request("restart", () => {})).resolves.toMatchObject({ | |
| ok: false, | |
| }); | |
| expect(retired).toBe(false); | |
| expect(execute).not.toHaveBeenCalled(); | |
| finishClose(); | |
| await retirement; | |
| await request; | |
| expect(dispose).toHaveBeenCalledOnce(); | |
| }, | |
| ); | |
| it("joins an in-flight native inspection on retirement and discards its late result", async () => { | |
| const host = owner(); | |
| let finishNative!: (result: { outcome: "accepted" }) => void; | |
| execute.mockImplementationOnce( | |
| () => | |
| new Promise((resolve) => { | |
| finishNative = resolve; | |
| }), | |
| ); | |
| await host.capability.request("stop", () => {}); | |
| const finishing = host.finishStop(); | |
| let retired = false; | |
| const retirement = host.retire().then(() => { | |
| retired = true; | |
| }); | |
| try { | |
| await new Promise<void>((resolve) => { | |
| setImmediate(resolve); | |
| }); | |
| expect(retired).toBe(false); | |
| current = false; | |
| } finally { | |
| finishNative({ outcome: "accepted" }); | |
| } | |
| await expect(finishing).resolves.toEqual({ outcome: "retired" }); | |
| await retirement; | |
| }); | |
| }); | |