fix(update): skip legacy parent doctor config writes

This commit is contained in:
Peter Steinberger
2026-04-29 05:36:51 +01:00
parent b85edb3f0c
commit 3aadeba93f
2 changed files with 21 additions and 58 deletions

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
resolveDoctorHealthContributions, resolveDoctorHealthContributions,
shouldSkipLegacyUpdateDoctorMetadataWrite, shouldSkipLegacyUpdateDoctorConfigWrite,
} from "./doctor-health-contributions.js"; } from "./doctor-health-contributions.js";
describe("doctor health contributions", () => { describe("doctor health contributions", () => {
@@ -30,54 +30,39 @@ describe("doctor health contributions", () => {
expect(ids.indexOf("doctor:command-owner")).toBeLessThan(ids.indexOf("doctor:write-config")); 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( expect(
shouldSkipLegacyUpdateDoctorMetadataWrite({ shouldSkipLegacyUpdateDoctorConfigWrite({
env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, 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); ).toBe(true);
}); });
it("keeps real doctor repairs writable during update", () => { it("keeps doctor writes outside legacy update writable", () => {
expect( expect(
shouldSkipLegacyUpdateDoctorMetadataWrite({ shouldSkipLegacyUpdateDoctorConfigWrite({
env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, env: {},
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" },
},
}), }),
).toBe(false); ).toBe(false);
}); });
it("keeps current update parents writable", () => { it("keeps current update parents writable", () => {
expect( expect(
shouldSkipLegacyUpdateDoctorMetadataWrite({ shouldSkipLegacyUpdateDoctorConfigWrite({
env: { env: {
OPENCLAW_UPDATE_IN_PROGRESS: "1", OPENCLAW_UPDATE_IN_PROGRESS: "1",
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "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); ).toBe(false);
}); });

View File

@@ -1,5 +1,4 @@
import fs from "node:fs"; import fs from "node:fs";
import { isDeepStrictEqual } from "node:util";
import type { probeGatewayMemoryStatus } from "../commands/doctor-gateway-health.js"; import type { probeGatewayMemoryStatus } from "../commands/doctor-gateway-health.js";
import type { DoctorOptions, DoctorPrompter } from "../commands/doctor-prompter.js"; import type { DoctorOptions, DoctorPrompter } from "../commands/doctor-prompter.js";
import type { OpenClawConfig } from "../config/types.openclaw.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"; return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
} }
function omitDoctorWriteMetadata(cfg: OpenClawConfig): OpenClawConfig { export function shouldSkipLegacyUpdateDoctorConfigWrite(params: {
const { meta: _meta, wizard: _wizard, ...rest } = cfg;
return rest;
}
export function shouldSkipLegacyUpdateDoctorMetadataWrite(params: {
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
hasPendingConfigWrite?: boolean;
before: OpenClawConfig;
after: OpenClawConfig;
}): boolean { }): boolean {
if (!isTruthyEnvValue(params.env.OPENCLAW_UPDATE_IN_PROGRESS)) { if (!isTruthyEnvValue(params.env.OPENCLAW_UPDATE_IN_PROGRESS)) {
return false; return false;
@@ -69,13 +60,7 @@ export function shouldSkipLegacyUpdateDoctorMetadataWrite(params: {
if (isTruthyEnvValue(params.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV])) { if (isTruthyEnvValue(params.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV])) {
return false; return false;
} }
if (params.hasPendingConfigWrite === true) { return true;
return false;
}
return isDeepStrictEqual(
omitDoctorWriteMetadata(params.before),
omitDoctorWriteMetadata(params.after),
);
} }
function createDoctorHealthContribution(params: { function createDoctorHealthContribution(params: {
@@ -534,15 +519,8 @@ async function runWriteConfigHealth(ctx: DoctorHealthFlowContext): Promise<void>
command: "doctor", command: "doctor",
mode: resolveDoctorMode(ctx.cfg), mode: resolveDoctorMode(ctx.cfg),
}); });
if ( if (shouldSkipLegacyUpdateDoctorConfigWrite({ env: ctx.env })) {
shouldSkipLegacyUpdateDoctorMetadataWrite({ ctx.runtime.log("Skipping doctor config write during legacy update handoff.");
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.");
return; return;
} }
await replaceConfigFile({ await replaceConfigFile({