From b4bda479a4741ea99fa97ef4162dd7097dba6e1e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Mar 2026 18:19:34 -0700 Subject: [PATCH] fix: normalize bundled plugin version reporting --- src/config/io.compat.test.ts | 74 ++++++++++++++++++++ src/config/io.ts | 5 +- src/config/version.ts | 126 +++++++++++++++++++++++++++++++++-- src/plugins/status.test.ts | 57 ++++++++++++++++ src/plugins/status.ts | 20 ++++++ 5 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/config/io.compat.test.ts b/src/config/io.compat.test.ts index c6ea1a04869..cdc0e063388 100644 --- a/src/config/io.compat.test.ts +++ b/src/config/io.compat.test.ts @@ -2,7 +2,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { VERSION } from "../version.js"; import { createConfigIO } from "./io.js"; +import { parseOpenClawVersion } from "./version.js"; async function withTempHome(run: (home: string) => Promise): Promise { const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-")); @@ -157,4 +159,76 @@ describe("config io paths", () => { expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("- gateway.port:")); }); }); + + it("does not warn when config was last touched by a same-base correction publish", async () => { + const parsedVersion = parseOpenClawVersion(VERSION); + if (!parsedVersion) { + throw new Error(`Unable to parse VERSION: ${VERSION}`); + } + const touchedVersion = `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}-${(parsedVersion.revision ?? 0) + 1}`; + + await withTempHome(async (home) => { + const configDir = path.join(home, ".openclaw"); + await fs.mkdir(configDir, { recursive: true }); + const configPath = path.join(configDir, "openclaw.json"); + await fs.writeFile( + configPath, + JSON.stringify({ meta: { lastTouchedVersion: touchedVersion } }, null, 2), + ); + + const logger = { + warn: vi.fn(), + error: vi.fn(), + }; + + const io = createConfigIO({ + env: {} as NodeJS.ProcessEnv, + homedir: () => home, + logger, + }); + + io.loadConfig(); + + expect(logger.warn).not.toHaveBeenCalledWith( + expect.stringContaining("Config was last written by a newer OpenClaw"), + ); + expect(io.configPath).toBe(configPath); + }); + }); + + it("still warns for same-base prerelease configs", async () => { + const parsedVersion = parseOpenClawVersion(VERSION); + if (!parsedVersion) { + throw new Error(`Unable to parse VERSION: ${VERSION}`); + } + const touchedVersion = `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}-beta.1`; + + await withTempHome(async (home) => { + const configDir = path.join(home, ".openclaw"); + await fs.mkdir(configDir, { recursive: true }); + const configPath = path.join(configDir, "openclaw.json"); + await fs.writeFile( + configPath, + JSON.stringify({ meta: { lastTouchedVersion: touchedVersion } }, null, 2), + ); + + const logger = { + warn: vi.fn(), + error: vi.fn(), + }; + + const io = createConfigIO({ + env: {} as NodeJS.ProcessEnv, + homedir: () => home, + logger, + }); + + io.loadConfig(); + + expect(logger.warn).toHaveBeenCalledWith( + `Config was last written by a newer OpenClaw (${touchedVersion}); current version is ${VERSION}.`, + ); + expect(io.configPath).toBe(configPath); + }); + }); }); diff --git a/src/config/io.ts b/src/config/io.ts index ea2abaf1914..67ffc41e6b3 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -53,7 +53,7 @@ import { validateConfigObjectRawWithPlugins, validateConfigObjectWithPlugins, } from "./validation.js"; -import { compareOpenClawVersions } from "./version.js"; +import { compareOpenClawVersions, isSameOpenClawStableFamily } from "./version.js"; // Re-export for backwards compatibility export { CircularIncludeError, ConfigIncludeError } from "./includes.js"; @@ -622,6 +622,9 @@ function warnIfConfigFromFuture(cfg: OpenClawConfig, logger: Pick { ); }); + it("normalizes bundled plugin versions to the core base release", () => { + loadOpenClawPluginsMock.mockReturnValue({ + plugins: [ + { + id: "whatsapp", + name: "WhatsApp", + description: "Bundled channel plugin", + version: "2026.3.22", + source: "/tmp/whatsapp/index.ts", + origin: "bundled", + enabled: true, + status: "loaded", + toolNames: [], + hookNames: [], + channelIds: ["whatsapp"], + providerIds: [], + speechProviderIds: [], + mediaUnderstandingProviderIds: [], + imageGenerationProviderIds: [], + webSearchProviderIds: [], + gatewayMethods: [], + cliCommands: [], + services: [], + commands: [], + httpRoutes: 0, + hookCount: 0, + configSchema: false, + }, + ], + diagnostics: [], + channels: [], + providers: [], + speechProviders: [], + mediaUnderstandingProviders: [], + imageGenerationProviders: [], + webSearchProviders: [], + tools: [], + hooks: [], + typedHooks: [], + channelSetups: [], + httpRoutes: [], + gatewayHandlers: {}, + cliRegistrars: [], + services: [], + commands: [], + }); + + const report = buildPluginStatusReport({ + config: {}, + env: { + OPENCLAW_VERSION: "2026.3.23-1", + } as NodeJS.ProcessEnv, + }); + + expect(report.plugins[0]?.version).toBe("2026.3.23"); + }); + it("builds an inspect report with capability shape and policy", () => { loadConfigMock.mockReturnValue({ plugins: { diff --git a/src/plugins/status.ts b/src/plugins/status.ts index a6b21541522..7ad0bcd8347 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -1,7 +1,9 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js"; import { loadConfig } from "../config/config.js"; +import { normalizeOpenClawVersionBase } from "../config/version.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { resolveRuntimeServiceVersion } from "../version.js"; import { inspectBundleLspRuntimeSupport } from "./bundle-lsp.js"; import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js"; import { normalizePluginsConfig } from "./config-state.js"; @@ -114,6 +116,20 @@ function buildCompatibilityNoticesForInspect( const log = createSubsystemLogger("plugins"); +function resolveReportedPluginVersion( + plugin: PluginRegistry["plugins"][number], + env: NodeJS.ProcessEnv | undefined, +): string | undefined { + if (plugin.origin !== "bundled") { + return plugin.version; + } + return ( + normalizeOpenClawVersionBase(resolveRuntimeServiceVersion(env)) ?? + normalizeOpenClawVersionBase(plugin.version) ?? + plugin.version + ); +} + export function buildPluginStatusReport(params?: { config?: ReturnType; workspaceDir?: string; @@ -136,6 +152,10 @@ export function buildPluginStatusReport(params?: { return { workspaceDir, ...registry, + plugins: registry.plugins.map((plugin) => ({ + ...plugin, + version: resolveReportedPluginVersion(plugin, params?.env), + })), }; }