fix: allow disabled plugin config writes (#63296) (thanks @fuller-stack-dev)

* fix(config): ignore synthesized disabled plugin config on write

* test(config): keep write-prepare regression generic

* test(config): cover explicit disabled plugin config preservation

* fix(config): skip disabled plugin config validation

* fix(config): avoid tdz in plugin validation

* fix: allow disabled plugin config writes (#63296) (thanks @fuller-stack-dev)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
fuller-stack-dev
2026-04-08 21:32:31 -06:00
committed by GitHub
parent 223fe07db9
commit 6384271963
3 changed files with 61 additions and 4 deletions

View File

@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- WhatsApp/auto-reply: keep inbound reply, media, and composing sends on the current socket across reconnects, wait through reconnect gaps, and retry timeout-only send failures without dropping the active socket ref. (#62892) Thanks @mcaxtr.
- Config/plugins: let config writes keep disabled plugin entries without forcing required plugin config schemas or crashing raw plugin validation, so slot switches and similar plugin-state updates persist cleanly. (#63296) Thanks @fuller-stack-dev.
## 2026.4.9

View File

@@ -25,6 +25,15 @@ vi.mock("../plugins/manifest-registry.js", () => ({
loadPluginManifestRegistry: mockLoadPluginManifestRegistry,
}));
vi.mock("../plugins/doctor-contract-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/doctor-contract-registry.js")>();
return {
...actual,
listPluginDoctorLegacyConfigRules: () => [],
applyPluginDoctorCompatibilityMigrations: () => ({ next: null, changes: [] }),
};
});
vi.mock("./backup-rotation.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./backup-rotation.js")>();
return {
@@ -180,4 +189,53 @@ describe("config io write", () => {
expect(overwriteLogs).toHaveLength(0);
});
});
it("writes disabled plugin entries without requiring plugin config", async () => {
mockLoadPluginManifestRegistry.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "required-plugin",
origin: "bundled",
channels: [],
providers: [],
kind: ["tool"],
configSchema: {
type: "object",
properties: {
token: { type: "string" },
},
required: ["token"],
additionalProperties: true,
},
},
],
} satisfies PluginManifestRegistry);
await withSuiteHome(async (home) => {
const io = createConfigIO({
env: { VITEST: "true" } as NodeJS.ProcessEnv,
homedir: () => home,
logger: silentLogger,
});
await expect(
io.writeConfigFile({
agents: { list: [{ id: "main", default: true }] },
plugins: {
entries: {
"required-plugin": {
enabled: false,
},
},
},
}),
).resolves.toEqual({ persistedHash: expect.any(String) });
});
mockLoadPluginManifestRegistry.mockReturnValue({
diagnostics: [],
plugins: [],
} satisfies PluginManifestRegistry);
});
});

View File

@@ -970,9 +970,6 @@ function validateConfigObjectWithPluginsBase(
const entry = normalizedPlugins.entries[pluginId];
const entryExists = entry !== undefined;
const entryHasConfig = Boolean(entry?.config);
const shouldReplacePluginConfig = opts.applyDefaults
? entryExists || entryHasConfig
: entryHasConfig;
const activationState = resolveEffectivePluginActivationState({
id: pluginId,
@@ -999,7 +996,8 @@ function validateConfigObjectWithPluginsBase(
}
}
const shouldValidate = enabled || entryExists || entryHasConfig;
const shouldReplacePluginConfig = entryHasConfig || (opts.applyDefaults && enabled);
const shouldValidate = enabled || entryHasConfig;
if (shouldValidate) {
if (record.configSchema) {
const res = validateJsonSchemaValue({