From 1c81b82f483951537d0dc0d7da178c8d28434cea Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 17 Mar 2026 20:49:39 -0700 Subject: [PATCH] Config: warn on plugin compatibility debt --- src/commands/config-validation.test.ts | 76 ++++++++++++++++++++++++++ src/commands/config-validation.ts | 17 ++++++ 2 files changed, 93 insertions(+) create mode 100644 src/commands/config-validation.test.ts diff --git a/src/commands/config-validation.test.ts b/src/commands/config-validation.test.ts new file mode 100644 index 00000000000..8ff9f595af0 --- /dev/null +++ b/src/commands/config-validation.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const readConfigFileSnapshot = vi.fn(); +const buildPluginCompatibilityNotices = vi.fn(() => []); + +vi.mock("../config/config.js", () => ({ + readConfigFileSnapshot, +})); + +vi.mock("../plugins/status.js", () => ({ + buildPluginCompatibilityNotices, + formatPluginCompatibilityNotice: (notice: { pluginId: string; message: string }) => + `${notice.pluginId} ${notice.message}`, +})); + +describe("requireValidConfigSnapshot", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns config and emits a non-blocking compatibility advisory", async () => { + readConfigFileSnapshot.mockResolvedValue({ + exists: true, + valid: true, + config: { plugins: {} }, + issues: [], + }); + buildPluginCompatibilityNotices.mockReturnValue([ + { + pluginId: "legacy-plugin", + code: "legacy-before-agent-start", + severity: "warn", + message: + "still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.", + }, + ]); + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + const { requireValidConfigSnapshot } = await import("./config-validation.js"); + const config = await requireValidConfigSnapshot(runtime); + + expect(config).toEqual({ plugins: {} }); + expect(runtime.error).not.toHaveBeenCalled(); + expect(runtime.exit).not.toHaveBeenCalled(); + expect(String(runtime.log.mock.calls[0]?.[0])).toContain("Plugin compatibility: 1 notice."); + expect(String(runtime.log.mock.calls[0]?.[0])).toContain( + "legacy-plugin still relies on legacy before_agent_start", + ); + }); + + it("blocks invalid config before emitting compatibility advice", async () => { + readConfigFileSnapshot.mockResolvedValue({ + exists: true, + valid: false, + config: {}, + issues: [{ path: "routing.allowFrom", message: "Legacy key" }], + }); + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + const { requireValidConfigSnapshot } = await import("./config-validation.js"); + const config = await requireValidConfigSnapshot(runtime); + + expect(config).toBeNull(); + expect(runtime.error).toHaveBeenCalled(); + expect(runtime.exit).toHaveBeenCalledWith(1); + expect(runtime.log).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/config-validation.ts b/src/commands/config-validation.ts index 707c6e87eff..5ece0a1cf36 100644 --- a/src/commands/config-validation.ts +++ b/src/commands/config-validation.ts @@ -1,6 +1,10 @@ import { formatCliCommand } from "../cli/command-format.js"; import { type OpenClawConfig, readConfigFileSnapshot } from "../config/config.js"; import { formatConfigIssueLines } from "../config/issue-format.js"; +import { + buildPluginCompatibilityNotices, + formatPluginCompatibilityNotice, +} from "../plugins/status.js"; import type { RuntimeEnv } from "../runtime.js"; export async function requireValidConfigSnapshot( @@ -17,5 +21,18 @@ export async function requireValidConfigSnapshot( runtime.exit(1); return null; } + const compatibility = buildPluginCompatibilityNotices({ config: snapshot.config }); + if (compatibility.length > 0) { + runtime.log( + [ + `Plugin compatibility: ${compatibility.length} notice${compatibility.length === 1 ? "" : "s"}.`, + ...compatibility + .slice(0, 3) + .map((notice) => `- ${formatPluginCompatibilityNotice(notice)}`), + ...(compatibility.length > 3 ? [`- ... +${compatibility.length - 3} more`] : []), + `Review: ${formatCliCommand("openclaw doctor")}`, + ].join("\n"), + ); + } return snapshot.config; }