refactor(doctor): extract legacy and unknown-key steps (#51938)

This commit is contained in:
Vincent Koc
2026-03-21 16:59:22 -07:00
committed by GitHub
parent 865a90ccab
commit 85722d4cf2
3 changed files with 173 additions and 45 deletions

View File

@@ -1,13 +1,12 @@
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { CONFIG_PATH, migrateLegacyConfig } from "../config/config.js";
import { formatConfigIssueLines } from "../config/issue-format.js";
import { CONFIG_PATH } from "../config/config.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { detectLegacyMatrixCrypto } from "../infra/matrix-legacy-crypto.js";
import { detectLegacyMatrixState } from "../infra/matrix-legacy-state.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import { note } from "../terminal/note.js";
import { noteOpencodeProviderOverrides, stripUnknownConfigKeys } from "./doctor-config-analysis.js";
import { noteOpencodeProviderOverrides } from "./doctor-config-analysis.js";
import { runDoctorConfigPreflight } from "./doctor-config-preflight.js";
import { normalizeCompatibilityConfigValues } from "./doctor-legacy-config.js";
import type { DoctorOptions } from "./doctor-prompter.js";
@@ -29,6 +28,10 @@ import {
scanTelegramAllowFromUsernameEntries,
} from "./doctor/providers/telegram.js";
import { maybeRepairAllowlistPolicyAllowFrom } from "./doctor/shared/allowlist-policy-repair.js";
import {
applyLegacyCompatibilityStep,
applyUnknownConfigKeyStep,
} from "./doctor/shared/config-flow-steps.js";
import { applyDoctorConfigMutation } from "./doctor/shared/config-mutation-state.js";
import {
collectMissingDefaultAccountBindingWarnings,
@@ -69,30 +72,20 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
let pendingChanges = false;
let shouldWriteConfig = false;
let fixHints: string[] = [];
const doctorFixCommand = formatCliCommand("openclaw doctor --fix");
if (snapshot.legacyIssues.length > 0) {
note(
formatConfigIssueLines(snapshot.legacyIssues, "-").join("\n"),
"Compatibility config keys detected",
);
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
if (changes.length > 0) {
note(changes.join("\n"), "Doctor changes");
}
if (migrated) {
candidate = migrated;
pendingChanges = pendingChanges || changes.length > 0;
}
if (shouldRepair) {
// Compatibility migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into channels.whatsapp.allowFrom.
if (migrated) {
cfg = migrated;
}
} else {
fixHints.push(
`Run "${formatCliCommand("openclaw doctor --fix")}" to apply compatibility migrations.`,
);
}
const legacyStep = applyLegacyCompatibilityStep({
snapshot,
state: { cfg, candidate, pendingChanges, fixHints },
shouldRepair,
doctorFixCommand,
});
({ cfg, candidate, pendingChanges, fixHints } = legacyStep.state);
if (legacyStep.issueLines.length > 0) {
note(legacyStep.issueLines.join("\n"), "Compatibility config keys detected");
}
if (legacyStep.changeLines.length > 0) {
note(legacyStep.changeLines.join("\n"), "Doctor changes");
}
const normalized = normalizeCompatibilityConfigValues(candidate);
@@ -102,7 +95,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
state: { cfg, candidate, pendingChanges, fixHints },
mutation: normalized,
shouldRepair,
fixHint: `Run "${formatCliCommand("openclaw doctor --fix")}" to apply these changes.`,
fixHint: `Run "${doctorFixCommand}" to apply these changes.`,
}));
}
@@ -113,7 +106,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
state: { cfg, candidate, pendingChanges, fixHints },
mutation: autoEnable,
shouldRepair,
fixHint: `Run "${formatCliCommand("openclaw doctor --fix")}" to apply these changes.`,
fixHint: `Run "${doctorFixCommand}" to apply these changes.`,
}));
}
@@ -250,7 +243,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
note(
collectTelegramAllowFromUsernameWarnings({
hits,
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
doctorFixCommand,
}).join("\n"),
"Doctor warnings",
);
@@ -261,7 +254,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
note(
collectDiscordNumericIdWarnings({
hits: discordHits,
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
doctorFixCommand,
}).join("\n"),
"Doctor warnings",
);
@@ -272,7 +265,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
note(
collectOpenPolicyAllowFromWarnings({
changes: allowFromScan.changes,
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
doctorFixCommand,
}).join("\n"),
"Doctor warnings",
);
@@ -294,7 +287,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
note(
collectLegacyToolsBySenderWarnings({
hits: toolsBySenderHits,
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
doctorFixCommand,
}).join("\n"),
"Doctor warnings",
);
@@ -305,7 +298,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
note(
collectExecSafeBinCoverageWarnings({
hits: safeBinCoverage,
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
doctorFixCommand,
}).join("\n"),
"Doctor warnings",
);
@@ -325,18 +318,15 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
note(collectMutableAllowlistWarnings(mutableAllowlistHits).join("\n"), "Doctor warnings");
}
const unknown = stripUnknownConfigKeys(candidate);
if (unknown.removed.length > 0) {
const lines = unknown.removed.map((path) => `- ${path}`).join("\n");
candidate = unknown.config;
pendingChanges = true;
if (shouldRepair) {
cfg = unknown.config;
note(lines, "Doctor changes");
} else {
note(lines, "Unknown config keys");
fixHints.push('Run "openclaw doctor --fix" to remove these keys.');
}
const unknownStep = applyUnknownConfigKeyStep({
state: { cfg, candidate, pendingChanges, fixHints },
shouldRepair,
doctorFixCommand,
});
({ cfg, candidate, pendingChanges, fixHints } = unknownStep.state);
if (unknownStep.removed.length > 0) {
const lines = unknownStep.removed.map((path) => `- ${path}`).join("\n");
note(lines, shouldRepair ? "Doctor changes" : "Unknown config keys");
}
if (!shouldRepair && pendingChanges) {

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import type { DoctorConfigPreflightResult } from "../../doctor-config-preflight.js";
import { applyLegacyCompatibilityStep, applyUnknownConfigKeyStep } from "./config-flow-steps.js";
describe("doctor config flow steps", () => {
it("collects legacy compatibility issue lines and preview fix hints", () => {
const result = applyLegacyCompatibilityStep({
snapshot: {
exists: true,
parsed: { heartbeat: { enabled: true } },
legacyIssues: [{ path: "heartbeat", message: "use agents.defaults.heartbeat" }],
path: "/tmp/config.json",
valid: true,
issues: [],
raw: "{}",
resolved: {},
config: {},
warnings: [],
} satisfies DoctorConfigPreflightResult["snapshot"],
state: {
cfg: {},
candidate: {},
pendingChanges: false,
fixHints: [],
},
shouldRepair: false,
doctorFixCommand: "openclaw doctor --fix",
});
expect(result.issueLines).toEqual([expect.stringContaining("- heartbeat:")]);
expect(result.changeLines).not.toEqual([]);
expect(result.state.fixHints).toContain(
'Run "openclaw doctor --fix" to apply compatibility migrations.',
);
});
it("removes unknown keys and adds preview hint", () => {
const result = applyUnknownConfigKeyStep({
state: {
cfg: {},
candidate: { bogus: true } as unknown as OpenClawConfig,
pendingChanges: false,
fixHints: [],
},
shouldRepair: false,
doctorFixCommand: "openclaw doctor --fix",
});
expect(result.removed).toEqual(["bogus"]);
expect(result.state.candidate).toEqual({});
expect(result.state.fixHints).toContain('Run "openclaw doctor --fix" to remove these keys.');
});
});

View File

@@ -0,0 +1,84 @@
import { migrateLegacyConfig } from "../../../config/config.js";
import { formatConfigIssueLines } from "../../../config/issue-format.js";
import { stripUnknownConfigKeys } from "../../doctor-config-analysis.js";
import type { DoctorConfigPreflightResult } from "../../doctor-config-preflight.js";
import type { DoctorConfigMutationState } from "./config-mutation-state.js";
export function applyLegacyCompatibilityStep(params: {
snapshot: DoctorConfigPreflightResult["snapshot"];
state: DoctorConfigMutationState;
shouldRepair: boolean;
doctorFixCommand: string;
}): {
state: DoctorConfigMutationState;
issueLines: string[];
changeLines: string[];
} {
if (params.snapshot.legacyIssues.length === 0) {
return {
state: params.state,
issueLines: [],
changeLines: [],
};
}
const issueLines = formatConfigIssueLines(params.snapshot.legacyIssues, "-");
const { config: migrated, changes } = migrateLegacyConfig(params.snapshot.parsed);
if (!migrated) {
return {
state: {
...params.state,
fixHints: params.shouldRepair
? params.state.fixHints
: [
...params.state.fixHints,
`Run "${params.doctorFixCommand}" to apply compatibility migrations.`,
],
},
issueLines,
changeLines: changes,
};
}
return {
state: {
cfg: params.shouldRepair ? migrated : params.state.cfg,
candidate: migrated,
pendingChanges: params.state.pendingChanges || changes.length > 0,
fixHints: params.shouldRepair
? params.state.fixHints
: [
...params.state.fixHints,
`Run "${params.doctorFixCommand}" to apply compatibility migrations.`,
],
},
issueLines,
changeLines: changes,
};
}
export function applyUnknownConfigKeyStep(params: {
state: DoctorConfigMutationState;
shouldRepair: boolean;
doctorFixCommand: string;
}): {
state: DoctorConfigMutationState;
removed: string[];
} {
const unknown = stripUnknownConfigKeys(params.state.candidate);
if (unknown.removed.length === 0) {
return { state: params.state, removed: [] };
}
return {
state: {
cfg: params.shouldRepair ? unknown.config : params.state.cfg,
candidate: unknown.config,
pendingChanges: true,
fixHints: params.shouldRepair
? params.state.fixHints
: [...params.state.fixHints, `Run "${params.doctorFixCommand}" to remove these keys.`],
},
removed: unknown.removed,
};
}