From 3aadeba93fdebc34a2ea6f5fea8bf3f58b48cd4a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 05:36:51 +0100 Subject: [PATCH] fix(update): skip legacy parent doctor config writes --- src/flows/doctor-health-contributions.test.ts | 49 +++++++------------ src/flows/doctor-health-contributions.ts | 30 ++---------- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/src/flows/doctor-health-contributions.test.ts b/src/flows/doctor-health-contributions.test.ts index c8a93a73db1..b9d2bb8f1c6 100644 --- a/src/flows/doctor-health-contributions.test.ts +++ b/src/flows/doctor-health-contributions.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { resolveDoctorHealthContributions, - shouldSkipLegacyUpdateDoctorMetadataWrite, + shouldSkipLegacyUpdateDoctorConfigWrite, } from "./doctor-health-contributions.js"; describe("doctor health contributions", () => { @@ -30,54 +30,39 @@ describe("doctor health contributions", () => { expect(ids.indexOf("doctor:command-owner")).toBeLessThan(ids.indexOf("doctor:write-config")); }); - it("skips metadata-only doctor writes under legacy update parents", () => { + it("skips doctor config writes under legacy update parents", () => { expect( - shouldSkipLegacyUpdateDoctorMetadataWrite({ + shouldSkipLegacyUpdateDoctorConfigWrite({ env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, - before: { gateway: { mode: "local" }, meta: { lastTouchedVersion: "2026.4.26" } }, - after: { - gateway: { mode: "local" }, - meta: { lastTouchedVersion: "2026.4.27" }, - wizard: { lastRunCommand: "doctor" }, - }, }), ).toBe(true); }); - it("keeps real doctor repairs writable during update", () => { + it("keeps doctor writes outside legacy update writable", () => { expect( - shouldSkipLegacyUpdateDoctorMetadataWrite({ - env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, - before: { gateway: { mode: "local" } }, - after: { gateway: { mode: "remote" } }, - }), - ).toBe(false); - }); - - it("keeps repair writes from doctor config preflight writable during legacy update", () => { - expect( - shouldSkipLegacyUpdateDoctorMetadataWrite({ - env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, - hasPendingConfigWrite: true, - before: { gateway: { mode: "remote" } }, - after: { - gateway: { mode: "remote" }, - meta: { lastTouchedVersion: "2026.4.27" }, - wizard: { lastRunCommand: "doctor" }, - }, + shouldSkipLegacyUpdateDoctorConfigWrite({ + env: {}, }), ).toBe(false); }); it("keeps current update parents writable", () => { expect( - shouldSkipLegacyUpdateDoctorMetadataWrite({ + shouldSkipLegacyUpdateDoctorConfigWrite({ env: { OPENCLAW_UPDATE_IN_PROGRESS: "1", OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1", }, - before: { meta: { lastTouchedVersion: "2026.4.26" } }, - after: { meta: { lastTouchedVersion: "2026.4.27" } }, + }), + ).toBe(false); + }); + + it("treats falsey update env values as normal writes", () => { + expect( + shouldSkipLegacyUpdateDoctorConfigWrite({ + env: { + OPENCLAW_UPDATE_IN_PROGRESS: "0", + }, }), ).toBe(false); }); diff --git a/src/flows/doctor-health-contributions.ts b/src/flows/doctor-health-contributions.ts index dfbf24a8c03..6d7a8901293 100644 --- a/src/flows/doctor-health-contributions.ts +++ b/src/flows/doctor-health-contributions.ts @@ -1,5 +1,4 @@ import fs from "node:fs"; -import { isDeepStrictEqual } from "node:util"; import type { probeGatewayMemoryStatus } from "../commands/doctor-gateway-health.js"; import type { DoctorOptions, DoctorPrompter } from "../commands/doctor-prompter.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -52,16 +51,8 @@ function isTruthyEnvValue(value: string | undefined): boolean { return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no"; } -function omitDoctorWriteMetadata(cfg: OpenClawConfig): OpenClawConfig { - const { meta: _meta, wizard: _wizard, ...rest } = cfg; - return rest; -} - -export function shouldSkipLegacyUpdateDoctorMetadataWrite(params: { +export function shouldSkipLegacyUpdateDoctorConfigWrite(params: { env: NodeJS.ProcessEnv; - hasPendingConfigWrite?: boolean; - before: OpenClawConfig; - after: OpenClawConfig; }): boolean { if (!isTruthyEnvValue(params.env.OPENCLAW_UPDATE_IN_PROGRESS)) { return false; @@ -69,13 +60,7 @@ export function shouldSkipLegacyUpdateDoctorMetadataWrite(params: { if (isTruthyEnvValue(params.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV])) { return false; } - if (params.hasPendingConfigWrite === true) { - return false; - } - return isDeepStrictEqual( - omitDoctorWriteMetadata(params.before), - omitDoctorWriteMetadata(params.after), - ); + return true; } function createDoctorHealthContribution(params: { @@ -534,15 +519,8 @@ async function runWriteConfigHealth(ctx: DoctorHealthFlowContext): Promise command: "doctor", mode: resolveDoctorMode(ctx.cfg), }); - if ( - shouldSkipLegacyUpdateDoctorMetadataWrite({ - env: ctx.env ?? process.env, - hasPendingConfigWrite: ctx.configResult.shouldWriteConfig === true, - before: ctx.cfgForPersistence, - after: ctx.cfg, - }) - ) { - ctx.runtime.log("Skipping doctor metadata-only config write during legacy update handoff."); + if (shouldSkipLegacyUpdateDoctorConfigWrite({ env: ctx.env })) { + ctx.runtime.log("Skipping doctor config write during legacy update handoff."); return; } await replaceConfigFile({