fix(doctor): keep cleanup hints scoped to detected services (#115559)

This commit is contained in:
Peter Steinberger
2026-07-29 01:08:19 -04:00
committed by GitHub
parent 3bb04a551d
commit aa1e8683d8
7 changed files with 313 additions and 26 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ import type {
import { resolveSecretInputRef } from "../../config/types.secrets.js";
import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js";
import { inspectGatewayHeapLimit, type GatewayHeapLimitReport } from "../../daemon/gateway-heap.js";
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
import type { ExtraGatewayService, FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
import type { ServiceConfigAudit } from "../../daemon/service-audit.js";
import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js";
@@ -345,7 +345,7 @@ export type DaemonStatus = {
healthy: boolean;
staleGatewayPids: number[];
};
extraServices: Array<{ label: string; detail: string; scope: string }>;
extraServices: ExtraGatewayService[];
/**
* Plugin version drift report. Surfaces active official external plugins
* whose installed version does not match the running gateway version, which
+49 -2
View File
@@ -12,6 +12,9 @@ const resolveControlUiLinksMock = vi.hoisted(() =>
);
const isSystemdUnavailableDetailMock = vi.hoisted(() => vi.fn(() => false));
const renderSystemdUnavailableHintsMock = vi.hoisted(() => vi.fn<() => string[]>(() => []));
const renderGatewayServiceCleanupHintsMock = vi.hoisted(() =>
vi.fn<(_services: unknown) => string[]>(() => []),
);
const isWSLEnvMock = vi.hoisted(() =>
vi.fn((env?: Record<string, string | undefined>) => Boolean(env?.WSL_DISTRO_NAME)),
);
@@ -35,7 +38,7 @@ vi.mock("../../gateway/control-ui-links.js", () => ({
}));
vi.mock("../../daemon/inspect.js", () => ({
renderGatewayServiceCleanupHints: () => [],
renderGatewayServiceCleanupHints: renderGatewayServiceCleanupHintsMock,
}));
vi.mock("../../daemon/restart-logs.js", () => ({
@@ -93,6 +96,7 @@ describe("printDaemonStatus", () => {
beforeEach(() => {
runtime.log.mockReset();
runtime.error.mockReset();
renderGatewayServiceCleanupHintsMock.mockReset().mockReturnValue([]);
resolveControlUiLinksMock.mockClear();
isSystemdUnavailableDetailMock.mockReset().mockReturnValue(false);
renderSystemdUnavailableHintsMock.mockReset().mockReturnValue([]);
@@ -741,7 +745,14 @@ describe("printDaemonStatus", () => {
listeners: [],
hints: [],
},
extraServices: [{ label: "ai.openclaw.gateway.rescue", scope: "user", detail: "loaded" }],
extraServices: [
{
platform: "darwin",
label: "ai.openclaw.gateway.rescue",
scope: "user",
detail: "loaded",
},
],
},
{ json: false },
);
@@ -751,6 +762,42 @@ describe("printDaemonStatus", () => {
expect(runtime.error).not.toHaveBeenCalled();
});
it("renders cleanup hints for the detected extra gateway without targeting the active gateway", () => {
const extraService = {
platform: "darwin" as const,
label: "com.example.openclaw-gateway",
scope: "user" as const,
detail: "plist: /Users/test/Library/LaunchAgents/com.example.openclaw-gateway.plist",
};
renderGatewayServiceCleanupHintsMock.mockReturnValue([
"launchctl bootout gui/$UID/com.example.openclaw-gateway",
"rm /Users/test/Library/LaunchAgents/com.example.openclaw-gateway.plist",
]);
printDaemonStatus(
{
service: {
label: "LaunchAgent",
loaded: true,
loadedText: "loaded",
notLoadedText: "not loaded",
runtime: { status: "running", pid: 8000 },
},
extraServices: [extraService],
},
{ json: false },
);
expect(renderGatewayServiceCleanupHintsMock).toHaveBeenCalledWith([extraService]);
expectMockLineContains(
runtime.log,
"Cleanup hint: launchctl bootout gui/$UID/com.example.openclaw-gateway",
);
expect(runtime.log.mock.calls.map(([line]) => line).join("\n")).not.toContain(
"ai.openclaw.gateway",
);
});
it("prints a terse plugin drift warning outside deep mode", () => {
printDaemonStatus(
{
+1 -1
View File
@@ -508,7 +508,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean; d
for (const svc of extraServices) {
defaultRuntime.log(`- ${warnText(svc.label)} (${svc.scope}, ${svc.detail})`);
}
for (const hint of renderGatewayServiceCleanupHints()) {
for (const hint of renderGatewayServiceCleanupHints(extraServices)) {
defaultRuntime.log(`${infoText("Cleanup hint:")} ${hint}`);
}
spacer();