























@@ -54,6 +54,15 @@ const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(()
5454const loadConfig = vi.hoisted(() => vi.fn(() => ({})));
5555const recoverInstalledLaunchAgent = vi.hoisted(() => vi.fn());
5656const repairLoadedGatewayServiceForStart = vi.hoisted(() => vi.fn());
57+const findInstalledSystemdGatewayScope = vi.hoisted(() =>
58+vi.fn<() => Promise<{ scope: "user" | "system"; unitName: string; unitPath: string } | null>>(
59+async () => null,
60+),
61+);
62+const restartSystemdService = vi.hoisted(() =>
63+vi.fn<() => Promise<{ outcome: "completed" }>>(async () => ({ outcome: "completed" })),
64+);
65+const stopSystemdService = vi.hoisted(() => vi.fn<() => Promise<void>>(async () => {}));
57665867function requireMockCallArg(
5968mockFn: { mock: { calls: unknown[][] } },
@@ -113,6 +122,12 @@ vi.mock("../../daemon/service.js", () => ({
113122resolveGatewayService: () => service,
114123}));
115124125+vi.mock("../../daemon/systemd.js", () => ({
126+findInstalledSystemdGatewayScope: () => findInstalledSystemdGatewayScope(),
127+restartSystemdService: () => restartSystemdService(),
128+stopSystemdService: () => stopSystemdService(),
129+}));
130+116131vi.mock("./launchd-recovery.js", () => ({
117132recoverInstalledLaunchAgent: (args: { result: "started" | "restarted" }) =>
118133recoverInstalledLaunchAgent(args),
@@ -208,6 +223,12 @@ describe("runDaemonRestart health checks", () => {
208223service.restart.mockResolvedValue({ outcome: "completed" });
209224runServiceStart.mockResolvedValue(undefined);
210225recoverInstalledLaunchAgent.mockResolvedValue(null);
226+findInstalledSystemdGatewayScope.mockReset();
227+findInstalledSystemdGatewayScope.mockResolvedValue(null);
228+restartSystemdService.mockReset();
229+restartSystemdService.mockResolvedValue({ outcome: "completed" });
230+stopSystemdService.mockReset();
231+stopSystemdService.mockResolvedValue(undefined);
211232212233runServiceRestart.mockImplementation(async (params: RestartParams) => {
213234const fail = (message: string, hints?: string[]) => {
@@ -607,6 +628,89 @@ describe("runDaemonRestart health checks", () => {
607628);
608629});
609630631+it("delegates system-scope restart to systemctl without unmanaged signaling when root (openclaw#87577)", async () => {
632+vi.spyOn(process, "platform", "get").mockReturnValue("linux");
633+findInstalledSystemdGatewayScope.mockResolvedValue({
634+scope: "system",
635+unitName: "openclaw.service",
636+unitPath: "/etc/systemd/system/openclaw.service",
637+});
638+restartSystemdService.mockResolvedValue({ outcome: "completed" });
639+findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
640+mockUnmanagedRestart();
641+642+await expect(runDaemonRestart({ json: true })).resolves.toBe(true);
643+644+expect(restartSystemdService).toHaveBeenCalled();
645+expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
646+expect(probeGateway).not.toHaveBeenCalled();
647+});
648+649+it("surfaces systemd sudo guidance and never signals when restarting a system-scope unit as non-root (openclaw#87577)", async () => {
650+vi.spyOn(process, "platform", "get").mockReturnValue("linux");
651+findInstalledSystemdGatewayScope.mockResolvedValue({
652+scope: "system",
653+unitName: "openclaw.service",
654+unitPath: "/etc/systemd/system/openclaw.service",
655+});
656+restartSystemdService.mockRejectedValue(
657+new Error(
658+"openclaw.service is a system-scope unit (/etc/systemd/system/openclaw.service); run `sudo systemctl restart openclaw.service` to restart it",
659+),
660+);
661+findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
662+mockUnmanagedRestart();
663+664+await expect(runDaemonRestart({ json: true })).rejects.toThrow(
665+/sudo systemctl restart openclaw\.service/,
666+);
667+668+expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
669+expect(probeGateway).not.toHaveBeenCalled();
670+});
671+672+it("delegates system-scope stop to systemctl without unmanaged signaling when root (openclaw#87577)", async () => {
673+vi.spyOn(process, "platform", "get").mockReturnValue("linux");
674+findInstalledSystemdGatewayScope.mockResolvedValue({
675+scope: "system",
676+unitName: "openclaw-gateway.service",
677+unitPath: "/etc/systemd/system/openclaw-gateway.service",
678+});
679+stopSystemdService.mockResolvedValue(undefined);
680+findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
681+runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {
682+await params.onNotLoaded?.();
683+});
684+685+await expect(runDaemonStop({ json: true })).resolves.toBeUndefined();
686+expect(stopSystemdService).toHaveBeenCalled();
687+expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
688+});
689+690+it("surfaces systemd sudo guidance and never signals when stopping a system-scope unit as non-root (openclaw#87577)", async () => {
691+vi.spyOn(process, "platform", "get").mockReturnValue("linux");
692+findInstalledSystemdGatewayScope.mockResolvedValue({
693+scope: "system",
694+unitName: "openclaw-gateway.service",
695+unitPath: "/etc/systemd/system/openclaw-gateway.service",
696+});
697+stopSystemdService.mockRejectedValue(
698+new Error(
699+"openclaw-gateway.service is a system-scope unit (/etc/systemd/system/openclaw-gateway.service); run `sudo systemctl stop openclaw-gateway.service` to stop it",
700+),
701+);
702+findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
703+runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {
704+await params.onNotLoaded?.();
705+});
706+707+await expect(runDaemonStop({ json: true })).rejects.toThrow(
708+/sudo systemctl stop openclaw-gateway\.service/,
709+);
710+expect(stopSystemdService).toHaveBeenCalled();
711+expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
712+});
713+610714it("skips unmanaged signaling for pids that are not live gateway processes", async () => {
611715findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
612716runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。