fix(doctor): preserve plugin validation during migration

This commit is contained in:
Omar Shahine
2026-07-17 08:27:38 -07:00
parent 3d21f5470d
commit 0a143a32f4
3 changed files with 52 additions and 2 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ Rapid consecutive text messages from the same sender can be batched into one age
- Debounce applies to text-only messages; media/attachments flush immediately.
- Control commands (stop/abort/status, etc.) bypass debouncing so they dispatch immediately.
- Disabled by default: `messages.inbound.debounceMs` has no built-in default, so debouncing only activates once you set it (globally or per channel).
- iMessage follows the same generic inbound debounce policy when `messages.inbound` is configured. Apple URL-preview split-sends are coalesced by `imsg >= 0.11.1` before OpenClaw receives them, so they do not need an OpenClaw debounce exception.
- iMessage follows the same generic inbound debounce policy when `messages.inbound` is configured. Apple URL-preview split-sends are coalesced by `imsg >= 0.13.1` before OpenClaw receives them, so they do not need an OpenClaw debounce exception.
## Sessions and devices
@@ -273,4 +273,43 @@ describe("runDoctorConfigPreflight", () => {
await expect(fs.readFile(configPath, "utf-8")).resolves.toBe(lastGoodRaw);
});
});
it("does not let a legacy channel key hide unrelated malformed plugin config", async () => {
await withTempHome(async (home) => {
const configPath = await writeOpenClawConfig(home, {
gateway: { mode: "local", port: 19091 },
});
await promoteConfigSnapshotToLastKnownGood(await readConfigFileSnapshot());
const lastGoodRaw = await fs.readFile(configPath, "utf-8");
await fs.writeFile(
configPath,
`${JSON.stringify(
{
gateway: { mode: "local", port: 19092 },
channels: {
imessage: {
enabled: true,
coalesceSameSenderDms: true,
mediaMaxMb: "bad",
},
},
},
null,
2,
)}\n`,
"utf-8",
);
const repaired = await runDoctorConfigPreflight({
migrateState: false,
migrateLegacyConfig: false,
repairPrefixedConfig: true,
invalidConfigNote: false,
});
expect(repaired.snapshot.valid).toBe(true);
expect(repaired.snapshot.config.gateway?.port).toBe(19091);
await expect(fs.readFile(configPath, "utf-8")).resolves.toBe(lastGoodRaw);
});
});
});
+12 -1
View File
@@ -13,12 +13,14 @@ import {
import { formatConfigIssueLines } from "../config/issue-format.js";
import type { ConfigFileSnapshot, LegacyConfigIssue } from "../config/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { validateConfigObjectWithPlugins } from "../config/validation.js";
import { isTruthyEnvValue } from "../infra/env.js";
import type { StartupMigrationLease } from "../infra/startup-migration-checkpoint.js";
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
import { resolveHomeDir } from "../utils.js";
import { noteIncludeConfinementWarning } from "./doctor-config-analysis.js";
import type { CronCodexRuntimePolicyTarget } from "./doctor/cron/store-migration.js";
import { normalizeCompatibilityConfigValues } from "./doctor/shared/legacy-config-core-migrate.js";
import { findDoctorLegacyConfigIssues } from "./doctor/shared/legacy-config-issues.js";
import { resolveStateMigrationConfigInput } from "./doctor/shared/legacy-config-state-migration-input.js";
@@ -335,11 +337,20 @@ export async function runDoctorConfigPreflight(
!next.valid &&
next.legacyIssues.length > 0
) {
next = addDoctorLegacyIssues(
const legacySnapshot = addDoctorLegacyIssues(
await measureStartupPreflightStep("config-snapshot-legacy-plugin", () =>
readConfigFileSnapshot({ ...readOptions, skipPluginValidation: true }),
),
);
if (legacySnapshot.valid) {
// Only bypass the stale schema long enough to prove that applying the
// registered doctor migrations leaves no unrelated plugin errors.
const rawConfig = legacySnapshot.sourceConfig ?? legacySnapshot.config ?? {};
const normalized = normalizeCompatibilityConfigValues(rawConfig);
if (validateConfigObjectWithPlugins(normalized.config).ok) {
next = legacySnapshot;
}
}
}
return next;
};