

























@@ -9,6 +9,7 @@ const consumeGatewaySigusr1RestartAuthorization = vi.fn(() => true);
99const consumeGatewayRestartIntentSync = vi.fn(() => false);
1010const isGatewaySigusr1RestartExternallyAllowed = vi.fn(() => false);
1111const markGatewaySigusr1RestartHandled = vi.fn();
12+const peekGatewaySigusr1RestartReason = vi.fn<() => string | undefined>(() => undefined);
1213const scheduleGatewaySigusr1Restart = vi.fn((_opts?: { delayMs?: number; reason?: string }) => ({
1314ok: true,
1415pid: process.pid,
@@ -30,6 +31,17 @@ const waitForBundledRuntimeDepsInstallIdle = vi.fn(async (_timeoutMs?: number) =
3031const restartGatewayProcessWithFreshPid = vi.fn<
3132() => { mode: "spawned" | "supervised" | "disabled" | "failed"; pid?: number; detail?: string }
3233>(() => ({ mode: "disabled" }));
34+const respawnGatewayProcessForUpdate = vi.fn<
35+() => {
36+mode: "spawned" | "supervised" | "disabled" | "failed";
37+pid?: number;
38+detail?: string;
39+child?: { kill: () => void };
40+}
41+>(() => ({ mode: "disabled", detail: "OPENCLAW_NO_RESPAWN" }));
42+const markUpdateRestartSentinelFailure = vi.fn<(reason: string) => Promise<null>>(
43+async (_reason: string) => null,
44+);
3345const abortEmbeddedPiRun = vi.fn(
3446(_sessionId?: string, _opts?: { mode?: "all" | "compacting" }) => false,
3547);
@@ -58,14 +70,20 @@ vi.mock("../../infra/restart.js", () => ({
5870consumeGatewayRestartIntentSync: () => consumeGatewayRestartIntentSync(),
5971isGatewaySigusr1RestartExternallyAllowed: () => isGatewaySigusr1RestartExternallyAllowed(),
6072markGatewaySigusr1RestartHandled: () => markGatewaySigusr1RestartHandled(),
73+peekGatewaySigusr1RestartReason: () => peekGatewaySigusr1RestartReason(),
6174scheduleGatewaySigusr1Restart: (opts?: { delayMs?: number; reason?: string }) =>
6275scheduleGatewaySigusr1Restart(opts),
6376}));
64776578vi.mock("../../infra/process-respawn.js", () => ({
79+respawnGatewayProcessForUpdate: () => respawnGatewayProcessForUpdate(),
6680restartGatewayProcessWithFreshPid: () => restartGatewayProcessWithFreshPid(),
6781}));
688283+vi.mock("../../infra/restart-sentinel.js", () => ({
84+markUpdateRestartSentinelFailure: (reason: string) => markUpdateRestartSentinelFailure(reason),
85+}));
86+6987vi.mock("../../process/command-queue.js", () => ({
7088getActiveTaskCount: () => getActiveTaskCount(),
7189markGatewayDraining: () => markGatewayDraining(),
@@ -195,13 +213,17 @@ async function runLoopWithStart(params: {
195213start: ReturnType<typeof vi.fn>;
196214runtime: LoopRuntime;
197215lockPort?: number;
216+healthHost?: string;
217+waitForHealthyChild?: (port: number, pid?: number, host?: string) => Promise<boolean>;
198218}) {
199219vi.resetModules();
200220const { runGatewayLoop } = await import("./run-loop.js");
201221const loopPromise = runGatewayLoop({
202222start: params.start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
203223runtime: params.runtime,
204224lockPort: params.lockPort,
225+healthHost: params.healthHost,
226+waitForHealthyChild: params.waitForHealthyChild,
205227});
206228return { loopPromise };
207229}
@@ -292,6 +314,12 @@ describe("runGatewayLoop", () => {
292314},
293315},
294316});
317+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
318+respawnGatewayProcessForUpdate.mockReturnValue({
319+mode: "disabled",
320+detail: "OPENCLAW_NO_RESPAWN",
321+});
322+markUpdateRestartSentinelFailure.mockClear();
295323296324await withIsolatedSignals(async ({ captureSignal }) => {
297325getActiveTaskCount.mockReturnValueOnce(2).mockReturnValueOnce(0);
@@ -453,6 +481,7 @@ describe("runGatewayLoop", () => {
453481454482it("releases the lock before exiting on spawned restart", async () => {
455483vi.clearAllMocks();
484+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
456485457486await withIsolatedSignals(async ({ captureSignal }) => {
458487const lockRelease = vi.fn(async () => {});
@@ -484,6 +513,7 @@ describe("runGatewayLoop", () => {
484513485514it("waits briefly before exiting on launchd supervised restart", async () => {
486515vi.clearAllMocks();
516+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
487517try {
488518setPlatform("darwin");
489519process.env.LAUNCH_JOB_LABEL = "ai.openclaw.gateway";
@@ -511,6 +541,7 @@ describe("runGatewayLoop", () => {
511541512542it("forwards lockPort to initial and restart lock acquisitions", async () => {
513543vi.clearAllMocks();
544+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
514545515546await withIsolatedSignals(async ({ captureSignal }) => {
516547const closeFirst = vi.fn(async () => {});
@@ -549,6 +580,7 @@ describe("runGatewayLoop", () => {
549580550581it("exits when lock reacquire fails during in-process restart fallback", async () => {
551582vi.clearAllMocks();
583+peekGatewaySigusr1RestartReason.mockReturnValue(undefined);
552584553585await withIsolatedSignals(async ({ captureSignal }) => {
554586const lockRelease = vi.fn(async () => {});
@@ -574,6 +606,103 @@ describe("runGatewayLoop", () => {
574606);
575607});
576608});
609+610+it("hard-respawns update restarts and exits only after the replacement becomes healthy", async () => {
611+vi.clearAllMocks();
612+peekGatewaySigusr1RestartReason.mockReturnValue("update.run");
613+respawnGatewayProcessForUpdate.mockReturnValueOnce({
614+mode: "spawned",
615+pid: 7777,
616+child: { kill: vi.fn() },
617+});
618+619+await withIsolatedSignals(async ({ captureSignal }) => {
620+const waitForHealthyChild = vi.fn(async () => true);
621+const close = vi.fn(async () => {});
622+const { start, started } = createSignaledStart(close);
623+const { runtime, exited } = createRuntimeWithExitSignal();
624+await runLoopWithStart({ start, runtime, lockPort: 18789, waitForHealthyChild });
625+await waitForStart(started);
626+const sigusr1 = captureSignal("SIGUSR1");
627+628+sigusr1();
629+630+await expect(exited).resolves.toBe(0);
631+expect(waitForHealthyChild).toHaveBeenCalledWith(18789, 7777, "127.0.0.1");
632+expect(respawnGatewayProcessForUpdate).toHaveBeenCalledTimes(1);
633+expect(start).toHaveBeenCalledTimes(1);
634+expect(markUpdateRestartSentinelFailure).not.toHaveBeenCalled();
635+});
636+});
637+638+it("probes the configured gateway host for update respawn health", async () => {
639+vi.clearAllMocks();
640+peekGatewaySigusr1RestartReason.mockReturnValue("update.run");
641+respawnGatewayProcessForUpdate.mockReturnValueOnce({
642+mode: "spawned",
643+pid: 7778,
644+child: { kill: vi.fn() },
645+});
646+647+await withIsolatedSignals(async ({ captureSignal }) => {
648+const waitForHealthyChild = vi.fn(async () => true);
649+const close = vi.fn(async () => {});
650+const { start, started } = createSignaledStart(close);
651+const { runtime, exited } = createRuntimeWithExitSignal();
652+await runLoopWithStart({
653+ start,
654+ runtime,
655+lockPort: 18789,
656+healthHost: "10.0.0.25",
657+ waitForHealthyChild,
658+});
659+await waitForStart(started);
660+const sigusr1 = captureSignal("SIGUSR1");
661+662+sigusr1();
663+664+await expect(exited).resolves.toBe(0);
665+expect(waitForHealthyChild).toHaveBeenCalledWith(18789, 7778, "10.0.0.25");
666+});
667+});
668+669+it("marks update respawn failures and falls back to in-process restart", async () => {
670+vi.clearAllMocks();
671+peekGatewaySigusr1RestartReason.mockReturnValue("update.run");
672+const kill = vi.fn();
673+respawnGatewayProcessForUpdate.mockReturnValueOnce({
674+mode: "spawned",
675+pid: 8888,
676+child: { kill },
677+});
678+679+await withIsolatedSignals(async ({ captureSignal }) => {
680+const waitForHealthyChild = vi.fn(async () => false);
681+const closeFirst = vi.fn(async () => {});
682+const closeSecond = vi.fn(async () => {});
683+const { runtime, exited } = createRuntimeWithExitSignal();
684+const start = vi
685+.fn()
686+.mockResolvedValueOnce({ close: closeFirst })
687+.mockResolvedValueOnce({ close: closeSecond });
688+689+await runLoopWithStart({ start, runtime, lockPort: 18789, waitForHealthyChild });
690+await new Promise<void>((resolve) => setImmediate(resolve));
691+const sigusr1 = captureSignal("SIGUSR1");
692+const sigterm = captureSignal("SIGTERM");
693+694+sigusr1();
695+await new Promise<void>((resolve) => setImmediate(resolve));
696+697+expect(waitForHealthyChild).toHaveBeenCalledWith(18789, 8888, "127.0.0.1");
698+expect(kill).toHaveBeenCalledTimes(1);
699+expect(markUpdateRestartSentinelFailure).toHaveBeenCalledWith("restart-unhealthy");
700+expect(start).toHaveBeenCalledTimes(2);
701+702+sigterm();
703+await expect(exited).resolves.toBe(0);
704+});
705+});
577706});
578707579708describe("gateway discover routing helpers", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。