




















@@ -46,6 +46,9 @@ const loadInstalledPluginIndexInstallRecords = vi.fn(
4646const legacyConfigRepairMocks = vi.hoisted(() => ({
4747repairLegacyConfigForUpdateChannel: vi.fn(),
4848}));
49+const launchdUpdateCleanupMocks = vi.hoisted(() => ({
50+disableCurrentOpenClawUpdateLaunchdJob: vi.fn(async () => false),
51+}));
4952const nodeVersionSatisfiesEngine = vi.fn();
5053const spawn = vi.fn();
5154const { defaultRuntime: runtimeCapture, resetRuntimeCapture } = createCliRuntimeCapture();
@@ -233,6 +236,12 @@ vi.mock("../daemon/service.js", () => ({
233236})),
234237}));
235238239+vi.mock("../daemon/launchd.js", async (importOriginal) => ({
240+ ...(await importOriginal<typeof import("../daemon/launchd.js")>()),
241+disableCurrentOpenClawUpdateLaunchdJob:
242+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob,
243+}));
244+236245vi.mock("../infra/ports.js", () => ({
237246inspectPortUsage: (...args: unknown[]) => inspectPortUsage(...args),
238247classifyPortListener: (...args: unknown[]) => classifyPortListener(...args),
@@ -706,6 +715,8 @@ describe("update-cli", () => {
706715repaired: false,
707716}),
708717);
718+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob.mockReset();
719+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob.mockResolvedValue(false);
709720confirm.mockResolvedValue(false);
710721select.mockResolvedValue("stable");
711722vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
@@ -738,11 +749,12 @@ describe("update-cli", () => {
738749expect(call?.[2]?.timeout).toBe(30_000);
739750});
740751741-it("refuses mutating updates in Nix mode before update side effects", async () => {
752+it("disarms legacy launchd updater jobs before refusing mutating updates in Nix mode", async () => {
742753await withEnvAsync({ OPENCLAW_NIX_MODE: "1" }, async () => {
743754await expect(updateCommand({ yes: true })).rejects.toThrow("OPENCLAW_NIX_MODE=1");
744755});
745756757+expect(launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob).toHaveBeenCalledOnce();
746758expect(runGatewayUpdate).not.toHaveBeenCalled();
747759expect(replaceConfigFile).not.toHaveBeenCalled();
748760expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
@@ -1555,6 +1567,9 @@ describe("update-cli", () => {
15551567expect(runDaemonInstall).not.toHaveBeenCalled();
15561568expect(runRestartScript).not.toHaveBeenCalled();
15571569expect(runDaemonRestart).not.toHaveBeenCalled();
1570+expect(
1571+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob,
1572+).not.toHaveBeenCalled();
1558157315591574const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
15601575expect(logs.join("\n")).toContain("Update dry-run");
@@ -1571,6 +1586,9 @@ describe("update-cli", () => {
15711586assert: () => {
15721587expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
15731588expect(runGatewayUpdate).not.toHaveBeenCalled();
1589+expect(
1590+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob,
1591+).not.toHaveBeenCalled();
15741592},
15751593},
15761594] as const)("updateCommand dry-run behavior: $name", runUpdateCliScenario);
@@ -2346,6 +2364,73 @@ describe("update-cli", () => {
23462364expect(requiredServiceStopCallOrder).toBeLessThan(requiredNpmInstallCallOrder);
23472365});
234823662367+it("disarms legacy launchd updater jobs before stopping the gateway", async () => {
2368+const tempDir = await createTrackedTempDir("openclaw-update-launchd-loop-");
2369+const nodeModules = path.join(tempDir, "node_modules");
2370+const pkgRoot = path.join(nodeModules, "openclaw");
2371+const entryPath = path.join(pkgRoot, "dist", "index.js");
2372+mockPackageInstallStatus(pkgRoot);
2373+await fs.mkdir(path.dirname(entryPath), { recursive: true });
2374+await fs.writeFile(
2375+path.join(pkgRoot, "package.json"),
2376+JSON.stringify({ name: "openclaw", version: "2026.4.21" }),
2377+"utf-8",
2378+);
2379+await fs.writeFile(entryPath, "export {};\n", "utf-8");
2380+await writePackageDistInventory(pkgRoot);
2381+serviceReadCommand.mockResolvedValue({
2382+programArguments: ["openclaw", "gateway", "run"],
2383+environment: {
2384+OPENCLAW_SERVICE_MARKER: "openclaw",
2385+OPENCLAW_SERVICE_KIND: "gateway",
2386+},
2387+});
2388+serviceLoaded.mockResolvedValue(true);
2389+serviceReadRuntime.mockResolvedValue({
2390+status: "running",
2391+pid: 4242,
2392+state: "running",
2393+});
2394+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob.mockResolvedValue(true);
2395+pathExists.mockImplementation(async (candidate: string) => {
2396+try {
2397+await fs.access(candidate);
2398+return true;
2399+} catch {
2400+return false;
2401+}
2402+});
2403+vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
2404+if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
2405+return {
2406+stdout: `${nodeModules}\n`,
2407+stderr: "",
2408+code: 0,
2409+signal: null,
2410+killed: false,
2411+termination: "exit",
2412+};
2413+}
2414+return {
2415+stdout: "",
2416+stderr: "",
2417+code: 0,
2418+signal: null,
2419+killed: false,
2420+termination: "exit",
2421+};
2422+});
2423+2424+await updateCommand({ yes: true });
2425+2426+const cleanupOrder =
2427+launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob.mock.invocationCallOrder[0];
2428+const serviceStopOrder = serviceStop.mock.invocationCallOrder[0];
2429+expect(requireValue(cleanupOrder, "launchd updater cleanup order")).toBeLessThan(
2430+requireValue(serviceStopOrder, "service stop order"),
2431+);
2432+});
2433+23492434it("refreshes package installs even when the current version already matches the target", async () => {
23502435const tempDir = await createTrackedTempDir("openclaw-update-current-");
23512436const nodeModules = path.join(tempDir, "node_modules");
@@ -2905,6 +2990,7 @@ describe("update-cli", () => {
2905299029062991expect(replaceConfigFile).not.toHaveBeenCalled();
29072992expect(runCommandWithTimeout).not.toHaveBeenCalled();
2993+expect(launchdUpdateCleanupMocks.disableCurrentOpenClawUpdateLaunchdJob).not.toHaveBeenCalled();
29082994expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
29092995});
29102996此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。