Skip to content

Commit 15e1dad

Browse files
Recognize manual update launchd jobs
1 parent 5bc80db commit 15e1dad

9 files changed

Lines changed: 330 additions & 18 deletions

src/cli/daemon-cli/status.gather.test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ const loadGatewayTlsRuntime = vi.fn(async (_cfg?: unknown) => ({
3131
fingerprintSha256: "sha256:11:22:33:44",
3232
}));
3333
const findExtraGatewayServices = vi.fn(async (_env?: unknown, _opts?: unknown) => []);
34-
const findStaleOpenClawUpdateLaunchdJobs = vi.fn<() => Promise<StaleOpenClawUpdateLaunchdJob[]>>(
35-
async () => [],
36-
);
34+
const findStaleOpenClawUpdateLaunchdJobs = vi.fn<
35+
(env?: NodeJS.ProcessEnv) => Promise<StaleOpenClawUpdateLaunchdJob[]>
36+
>(async () => []);
3737
const inspectPortUsage = vi.fn(async (port: number) => ({
3838
port,
3939
status: "free" as const,
@@ -152,7 +152,8 @@ vi.mock("../../daemon/inspect.js", () => ({
152152
}));
153153

154154
vi.mock("../../daemon/launchd.js", () => ({
155-
findStaleOpenClawUpdateLaunchdJobs: () => findStaleOpenClawUpdateLaunchdJobs(),
155+
findStaleOpenClawUpdateLaunchdJobs: (env?: NodeJS.ProcessEnv) =>
156+
findStaleOpenClawUpdateLaunchdJobs(env),
156157
}));
157158

158159
vi.mock("../../daemon/service-audit.js", () => ({
@@ -483,11 +484,23 @@ describe("gatherDaemonStatus", () => {
483484
it.runIf(process.platform === "darwin")(
484485
"surfaces stale updater launchd jobs only during deep status",
485486
async () => {
487+
serviceReadCommand.mockResolvedValueOnce({
488+
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],
489+
environment: {
490+
OPENCLAW_STATE_DIR: "/tmp/openclaw-daemon",
491+
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-daemon/openclaw.json",
492+
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.manual-update.gateway",
493+
},
494+
});
486495
findStaleOpenClawUpdateLaunchdJobs.mockResolvedValueOnce([
487496
{
488497
label: "ai.openclaw.update.2026.5.12",
489498
lastExitStatus: 127,
490499
},
500+
{
501+
label: "ai.openclaw.manual-update.1717168800",
502+
lastExitStatus: 0,
503+
},
491504
]);
492505

493506
const status = await gatherDaemonStatus({
@@ -496,11 +509,19 @@ describe("gatherDaemonStatus", () => {
496509
deep: true,
497510
});
498511

512+
const staleScanEnv = findStaleOpenClawUpdateLaunchdJobs.mock.calls[0]?.[0];
513+
expect(staleScanEnv?.OPENCLAW_STATE_DIR).toBe("/tmp/openclaw-daemon");
514+
expect(staleScanEnv?.OPENCLAW_CONFIG_PATH).toBe("/tmp/openclaw-daemon/openclaw.json");
515+
expect(staleScanEnv?.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.manual-update.gateway");
499516
expect(status.service.staleUpdateLaunchdJobs).toEqual([
500517
{
501518
label: "ai.openclaw.update.2026.5.12",
502519
lastExitStatus: 127,
503520
},
521+
{
522+
label: "ai.openclaw.manual-update.1717168800",
523+
lastExitStatus: 0,
524+
},
504525
]);
505526
},
506527
);

src/cli/daemon-cli/status.gather.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,9 @@ export async function gatherDaemonStatus(
553553
const staleUpdateLaunchdJobs =
554554
opts.deep && process.platform === "darwin"
555555
? await loadLaunchdModule()
556-
.then(({ findStaleOpenClawUpdateLaunchdJobs }) => findStaleOpenClawUpdateLaunchdJobs())
556+
.then(({ findStaleOpenClawUpdateLaunchdJobs }) =>
557+
findStaleOpenClawUpdateLaunchdJobs(serviceEnv),
558+
)
557559
.catch(() => [])
558560
: [];
559561

src/cli/daemon-cli/status.print.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ describe("printDaemonStatus", () => {
192192
label: "ai.openclaw.update.2026.5.12",
193193
lastExitStatus: 127,
194194
},
195+
{
196+
label: "ai.openclaw.manual-update.1717168800",
197+
lastExitStatus: 0,
198+
},
195199
],
196200
},
197201
gateway: {
@@ -208,6 +212,7 @@ describe("printDaemonStatus", () => {
208212

209213
expectMockLineContains(runtime.error, "Stale OpenClaw updater launchd job(s) detected.");
210214
expectMockLineContains(runtime.error, "ai.openclaw.update.2026.5.12");
215+
expectMockLineContains(runtime.error, "ai.openclaw.manual-update.1717168800");
211216
expectMockLineContains(runtime.error, "launchctl remove <label>");
212217
expectMockLineContains(runtime.error, formatCliCommand("openclaw gateway restart"));
213218
});

src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it, vi } from "vitest";
22
import type { OpenClawConfig } from "../config/config.js";
33
import {
4+
collectMacGatewayPlatformWarnings,
45
collectMacLaunchAgentOverrideWarning,
56
collectMacLaunchctlGatewayEnvOverrideWarning,
67
collectMacStaleOpenClawUpdateLaunchdJobsWarning,
@@ -128,16 +129,22 @@ describe("noteMacStaleOpenClawUpdateLaunchdJobs", () => {
128129
label: "ai.openclaw.update.2026.5.12",
129130
lastExitStatus: 127,
130131
},
132+
{
133+
label: "ai.openclaw.manual-update.1717168800",
134+
lastExitStatus: 0,
135+
},
131136
]);
132137

133138
const warning = await collectMacStaleOpenClawUpdateLaunchdJobsWarning({
134139
platform: "darwin",
140+
env: process.env,
135141
findJobs,
136142
});
137143

138144
expect(findJobs).toHaveBeenCalledTimes(1);
139145
expect(warning).toContain("Stale OpenClaw updater launchd job(s) detected");
140146
expect(warning).toContain("ai.openclaw.update.2026.5.12");
147+
expect(warning).toContain("ai.openclaw.manual-update.1717168800");
141148
expect(warning).toContain("launchctl remove <label>");
142149
expect(warning).toContain("openclaw gateway restart");
143150
});
@@ -149,10 +156,15 @@ describe("noteMacStaleOpenClawUpdateLaunchdJobs", () => {
149156
label: "ai.openclaw.update.2026.5.12",
150157
lastExitStatus: 127,
151158
},
159+
{
160+
label: "ai.openclaw.manual-update.1717168800",
161+
lastExitStatus: 0,
162+
},
152163
]);
153164

154165
await noteMacStaleOpenClawUpdateLaunchdJobs({
155166
platform: "darwin",
167+
env: process.env,
156168
findJobs,
157169
noteFn,
158170
});
@@ -162,6 +174,7 @@ describe("noteMacStaleOpenClawUpdateLaunchdJobs", () => {
162174
expect(title).toBe("Gateway (macOS)");
163175
expect(message).toContain("Stale OpenClaw updater launchd job(s) detected");
164176
expect(message).toContain("ai.openclaw.update.2026.5.12");
177+
expect(message).toContain("ai.openclaw.manual-update.1717168800");
165178
expect(message).toContain("launchctl remove <label>");
166179
expect(message).toContain("openclaw gateway restart");
167180
});
@@ -172,12 +185,79 @@ describe("noteMacStaleOpenClawUpdateLaunchdJobs", () => {
172185

173186
await noteMacStaleOpenClawUpdateLaunchdJobs({
174187
platform: "darwin",
188+
env: process.env,
175189
findJobs,
176190
noteFn,
177191
});
178192

179193
expect(noteFn).not.toHaveBeenCalled();
180194
});
195+
196+
it("passes the installed service env into doctor stale updater scans", async () => {
197+
const serviceEnv = {
198+
...process.env,
199+
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.manual-update.gateway",
200+
};
201+
const findJobs = vi.fn(async (env?: NodeJS.ProcessEnv) => {
202+
if (env?.OPENCLAW_LAUNCHD_LABEL === "ai.openclaw.manual-update.gateway") {
203+
return [
204+
{
205+
label: "ai.openclaw.manual-update.1717168800",
206+
lastExitStatus: 0,
207+
},
208+
];
209+
}
210+
return [
211+
{
212+
label: "ai.openclaw.manual-update.gateway",
213+
lastExitStatus: 0,
214+
},
215+
];
216+
});
217+
218+
const warning = await collectMacStaleOpenClawUpdateLaunchdJobsWarning({
219+
platform: "darwin",
220+
env: serviceEnv,
221+
findJobs,
222+
});
223+
224+
expect(findJobs).toHaveBeenCalledWith(serviceEnv);
225+
expect(warning).toContain("ai.openclaw.manual-update.1717168800");
226+
expect(warning).not.toContain("ai.openclaw.manual-update.gateway");
227+
});
228+
229+
it("passes the installed service env into structured doctor platform warnings", async () => {
230+
const serviceEnv = {
231+
...process.env,
232+
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.manual-update.gateway",
233+
};
234+
const findJobs = vi.fn(async (env?: NodeJS.ProcessEnv) =>
235+
env?.OPENCLAW_LAUNCHD_LABEL === "ai.openclaw.manual-update.gateway"
236+
? [
237+
{
238+
label: "ai.openclaw.manual-update.1717168800",
239+
lastExitStatus: 0,
240+
},
241+
]
242+
: [
243+
{
244+
label: "ai.openclaw.manual-update.gateway",
245+
lastExitStatus: 0,
246+
},
247+
],
248+
);
249+
250+
const warnings = await collectMacGatewayPlatformWarnings({} as OpenClawConfig, {
251+
platform: "darwin",
252+
env: serviceEnv,
253+
findJobs,
254+
getenv: async () => undefined,
255+
});
256+
257+
expect(findJobs).toHaveBeenCalledWith(serviceEnv);
258+
expect(warnings.join("\n")).toContain("ai.openclaw.manual-update.1717168800");
259+
expect(warnings.join("\n")).not.toContain("ai.openclaw.manual-update.gateway");
260+
});
181261
});
182262

183263
describe("collectMacLaunchAgentOverrideWarning", () => {

src/commands/doctor-platform-notes.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ export async function noteMacLaunchAgentOverrides() {
5050

5151
export async function collectMacStaleOpenClawUpdateLaunchdJobsWarning(deps?: {
5252
platform?: NodeJS.Platform;
53+
env?: NodeJS.ProcessEnv;
5354
findJobs?: typeof findStaleOpenClawUpdateLaunchdJobs;
5455
}): Promise<string | null> {
5556
const platform = deps?.platform ?? process.platform;
5657
if (platform !== "darwin") {
5758
return null;
5859
}
59-
const jobs = await (deps?.findJobs ?? findStaleOpenClawUpdateLaunchdJobs)().catch(() => []);
60+
const env = deps?.env ?? (await resolveInstalledGatewayServiceEnv());
61+
const jobs = await (deps?.findJobs ?? findStaleOpenClawUpdateLaunchdJobs)(env).catch(() => []);
6062
if (jobs.length === 0) {
6163
return null;
6264
}
@@ -77,6 +79,7 @@ export async function collectMacStaleOpenClawUpdateLaunchdJobsWarning(deps?: {
7779

7880
export async function noteMacStaleOpenClawUpdateLaunchdJobs(deps?: {
7981
platform?: NodeJS.Platform;
82+
env?: NodeJS.ProcessEnv;
8083
findJobs?: typeof findStaleOpenClawUpdateLaunchdJobs;
8184
noteFn?: typeof note;
8285
}) {
@@ -173,23 +176,53 @@ export async function noteMacLaunchctlGatewayEnvOverrides(
173176

174177
export async function collectMacGatewayPlatformWarnings(
175178
cfg: OpenClawConfig,
179+
deps?: {
180+
platform?: NodeJS.Platform;
181+
env?: NodeJS.ProcessEnv;
182+
findJobs?: typeof findStaleOpenClawUpdateLaunchdJobs;
183+
getenv?: (name: string) => Promise<string | undefined>;
184+
},
176185
): Promise<readonly string[]> {
177186
const warnings: string[] = [];
178187
const launchAgentWarning = collectMacLaunchAgentOverrideWarning();
179188
if (launchAgentWarning) {
180189
warnings.push(launchAgentWarning);
181190
}
182-
const staleUpdateWarning = await collectMacStaleOpenClawUpdateLaunchdJobsWarning();
191+
const staleUpdateWarning = await collectMacStaleOpenClawUpdateLaunchdJobsWarning({
192+
platform: deps?.platform,
193+
env: deps?.env,
194+
findJobs: deps?.findJobs,
195+
});
183196
if (staleUpdateWarning) {
184197
warnings.push(staleUpdateWarning);
185198
}
186-
const launchctlWarning = await collectMacLaunchctlGatewayEnvOverrideWarning(cfg);
199+
const launchctlWarning = await collectMacLaunchctlGatewayEnvOverrideWarning(cfg, {
200+
platform: deps?.platform,
201+
getenv: deps?.getenv,
202+
});
187203
if (launchctlWarning) {
188204
warnings.push(launchctlWarning);
189205
}
190206
return warnings;
191207
}
192208

209+
async function resolveInstalledGatewayServiceEnv(): Promise<NodeJS.ProcessEnv> {
210+
try {
211+
const { resolveGatewayService } = await import("../daemon/service.js");
212+
const command = await resolveGatewayService().readCommand(process.env);
213+
if (command?.environment) {
214+
return {
215+
...process.env,
216+
...command.environment,
217+
};
218+
}
219+
} catch {
220+
// Doctor warnings are best-effort; fall back to the CLI env if the service
221+
// command cannot be read.
222+
}
223+
return process.env;
224+
}
225+
193226
function isTruthyEnvValue(value: string | undefined): boolean {
194227
return Boolean(normalizeOptionalString(value));
195228
}

0 commit comments

Comments
 (0)