


















@@ -139,7 +139,10 @@ vi.mock("../../infra/update-managed-service-handoff.js", () => ({
139139 ? `openclaw update --yes --timeout ${Math.ceil(params.timeoutMs / 1000)}`
140140 : "openclaw update --yes",
141141buildManagedServiceHandoffUnavailableMessage: (command: string) =>
142-`Run \`${command}\` from a shell outside the gateway service.`,
142+[
143+"OpenClaw updates cannot safely run inside the live gateway process without a managed-service handoff.",
144+`Run \`${command}\` from a shell outside the gateway service, or restart/update from the host UI.`,
145+].join("\n"),
143146}));
144147145148vi.mock("./validation.js", () => ({
@@ -219,6 +222,33 @@ function firstMockCall(
219222return call;
220223}
221224225+async function withProcessEnv<T>(
226+updates: Record<string, string | undefined>,
227+run: () => Promise<T>,
228+): Promise<T> {
229+const previous = new Map<string, string | undefined>();
230+for (const key of Object.keys(updates)) {
231+previous.set(key, process.env[key]);
232+const value = updates[key];
233+if (value === undefined) {
234+delete process.env[key];
235+} else {
236+process.env[key] = value;
237+}
238+}
239+try {
240+return await run();
241+} finally {
242+for (const [key, value] of previous) {
243+if (value === undefined) {
244+delete process.env[key];
245+} else {
246+process.env[key] = value;
247+}
248+}
249+}
250+}
251+222252function mockGlobalInstallSurface() {
223253resolveUpdateInstallSurfaceMock.mockResolvedValueOnce({
224254kind: "global",
@@ -365,7 +395,9 @@ describe("update.run restart scheduling", () => {
365395detectRespawnSupervisorMock.mockReturnValueOnce("launchd");
366396mockGlobalInstallSurface();
367397368-const payload = await captureUpdateRunPayload();
398+const payload = await withProcessEnv({ OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway" }, () =>
399+captureUpdateRunPayload(),
400+);
369401370402expect(runGatewayUpdateMock).not.toHaveBeenCalled();
371403expect(startManagedServiceUpdateHandoffMock).toHaveBeenCalledTimes(1);
@@ -427,7 +459,9 @@ describe("update.run restart scheduling", () => {
427459detectRespawnSupervisorMock.mockReturnValueOnce("systemd");
428460mockGlobalInstallSurface();
429461430-await invokeUpdateRun({ restartDelayMs: 0 });
462+await withProcessEnv({ OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway.service" }, () =>
463+invokeUpdateRun({ restartDelayMs: 0 }),
464+);
431465432466expect(startManagedServiceUpdateHandoffMock).toHaveBeenCalledWith(
433467expect.objectContaining({
@@ -452,7 +486,9 @@ describe("update.run restart scheduling", () => {
452486throw Object.assign(new Error("uv_cwd"), { code: "ENOENT", syscall: "uv_cwd" });
453487});
454488try {
455-await invokeUpdateRun({});
489+await withProcessEnv({ OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway" }, () =>
490+invokeUpdateRun({}),
491+);
456492} finally {
457493cwdSpy.mockRestore();
458494}
@@ -468,19 +504,9 @@ describe("update.run restart scheduling", () => {
468504it("hands supervised git/dev updates to the CLI path instead of rebuilding live dist in-process", async () => {
469505detectRespawnSupervisorMock.mockReturnValueOnce("launchd");
470506mockGitInstallSurface("/tmp/openclaw-git");
471-const previousLaunchdLabel = process.env.OPENCLAW_LAUNCHD_LABEL;
472-process.env.OPENCLAW_LAUNCHD_LABEL = "ai.openclaw.gateway";
473-474-let payload: UpdateRunPayload | undefined;
475-try {
476-payload = await captureUpdateRunPayload();
477-} finally {
478-if (previousLaunchdLabel === undefined) {
479-delete process.env.OPENCLAW_LAUNCHD_LABEL;
480-} else {
481-process.env.OPENCLAW_LAUNCHD_LABEL = previousLaunchdLabel;
482-}
483-}
507+const payload = await withProcessEnv({ OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway" }, () =>
508+captureUpdateRunPayload(),
509+);
484510485511expect(runGatewayUpdateMock).not.toHaveBeenCalled();
486512expect(startManagedServiceUpdateHandoffMock).toHaveBeenCalledTimes(1);
@@ -528,29 +554,17 @@ describe("update.run restart scheduling", () => {
528554expect(readCapturedPayload().status).toBe("ok");
529555});
530556531-it("hands systemd-supervised git/dev updates to handoff from systemd markers", async () => {
532-const previousSystemdUnit = process.env.OPENCLAW_SYSTEMD_UNIT;
533-const previousInvocationId = process.env.INVOCATION_ID;
534-delete process.env.OPENCLAW_SYSTEMD_UNIT;
535-process.env.INVOCATION_ID = "8a77e69a8f604bf0b7984879b9f17a7c";
557+it("hands systemd-supervised git/dev updates to handoff from the durable unit identity", async () => {
536558detectRespawnSupervisorMock.mockReturnValueOnce("systemd");
537559mockGitInstallSurface("/tmp/openclaw-git");
538560539-let payload: UpdateRunPayload | undefined;
540-try {
541-payload = await captureUpdateRunPayload();
542-} finally {
543-if (previousSystemdUnit === undefined) {
544-delete process.env.OPENCLAW_SYSTEMD_UNIT;
545-} else {
546-process.env.OPENCLAW_SYSTEMD_UNIT = previousSystemdUnit;
547-}
548-if (previousInvocationId === undefined) {
549-delete process.env.INVOCATION_ID;
550-} else {
551-process.env.INVOCATION_ID = previousInvocationId;
552-}
553-}
561+const payload = await withProcessEnv(
562+{
563+OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway.service",
564+INVOCATION_ID: "8a77e69a8f604bf0b7984879b9f17a7c",
565+},
566+() => captureUpdateRunPayload(),
567+);
554568555569expect(runGatewayUpdateMock).not.toHaveBeenCalled();
556570expect(startManagedServiceUpdateHandoffMock).toHaveBeenCalledTimes(1);
@@ -567,6 +581,29 @@ describe("update.run restart scheduling", () => {
567581expect(payload?.handoff?.status).toBe("started");
568582});
569583584+it("does not hand off systemd-supervised git/dev updates from generic systemd markers alone", async () => {
585+detectRespawnSupervisorMock.mockReturnValueOnce("systemd");
586+mockGitInstallSurface("/tmp/openclaw-git");
587+588+const payload = await withProcessEnv(
589+{
590+OPENCLAW_SYSTEMD_UNIT: undefined,
591+INVOCATION_ID: "8a77e69a8f604bf0b7984879b9f17a7c",
592+},
593+() => captureUpdateRunPayload(),
594+);
595+596+expect(runGatewayUpdateMock).not.toHaveBeenCalled();
597+expect(startManagedServiceUpdateHandoffMock).not.toHaveBeenCalled();
598+expect(scheduleGatewaySigusr1RestartMock).not.toHaveBeenCalled();
599+expect(payload?.ok).toBe(false);
600+expect(payload?.restart).toBeNull();
601+expect(payload?.result?.status).toBe("skipped");
602+expect(payload?.result?.reason).toBe("managed-service-handoff-unavailable");
603+expect(payload?.result?.mode).toBe("git");
604+expect(payload?.handoff?.status).toBe("unavailable");
605+});
606+570607it("returns a safe command when package updates cannot be handed off", async () => {
571608mockGlobalInstallSurface();
572609@@ -583,7 +620,8 @@ describe("update.run restart scheduling", () => {
583620status: "unavailable",
584621command: "openclaw update --yes --timeout 1800",
585622message:
586-"Run `openclaw update --yes --timeout 1800` from a shell outside the gateway service.",
623+"OpenClaw updates cannot safely run inside the live gateway process without a managed-service handoff.\n" +
624+"Run `openclaw update --yes --timeout 1800` from a shell outside the gateway service, or restart/update from the host UI.",
587625});
588626});
589627此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。