mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 17:02:46 +00:00
fix: normalize bundled plugin version reporting
This commit is contained in:
@@ -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<void>): Promise<void> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<typeof console
|
||||
if (!touched) {
|
||||
return;
|
||||
}
|
||||
if (isSameOpenClawStableFamily(VERSION, touched)) {
|
||||
return;
|
||||
}
|
||||
const cmp = compareOpenClawVersions(VERSION, touched);
|
||||
if (cmp === null) {
|
||||
return;
|
||||
|
||||
@@ -2,28 +2,59 @@ export type OpenClawVersion = {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
revision: number;
|
||||
revision: number | null;
|
||||
prerelease: string[] | null;
|
||||
};
|
||||
|
||||
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-(\d+))?/;
|
||||
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
||||
|
||||
export function parseOpenClawVersion(raw: string | null | undefined): OpenClawVersion | null {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const match = raw.trim().match(VERSION_RE);
|
||||
const normalized = normalizeLegacyDotBetaVersion(raw.trim());
|
||||
const match = normalized.match(VERSION_RE);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const [, major, minor, patch, revision] = match;
|
||||
const [, major, minor, patch, suffix] = match;
|
||||
const revision = suffix && /^[0-9]+$/.test(suffix) ? Number.parseInt(suffix, 10) : null;
|
||||
return {
|
||||
major: Number.parseInt(major, 10),
|
||||
minor: Number.parseInt(minor, 10),
|
||||
patch: Number.parseInt(patch, 10),
|
||||
revision: revision ? Number.parseInt(revision, 10) : 0,
|
||||
revision,
|
||||
prerelease: suffix && revision == null ? suffix.split(".").filter(Boolean) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeOpenClawVersionBase(raw: string | null | undefined): string | null {
|
||||
const parsed = parseOpenClawVersion(raw);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
||||
}
|
||||
|
||||
export function isSameOpenClawStableFamily(
|
||||
a: string | null | undefined,
|
||||
b: string | null | undefined,
|
||||
): boolean {
|
||||
const parsedA = parseOpenClawVersion(a);
|
||||
const parsedB = parseOpenClawVersion(b);
|
||||
if (!parsedA || !parsedB) {
|
||||
return false;
|
||||
}
|
||||
if (parsedA.prerelease?.length || parsedB.prerelease?.length) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
parsedA.major === parsedB.major &&
|
||||
parsedA.minor === parsedB.minor &&
|
||||
parsedA.patch === parsedB.patch
|
||||
);
|
||||
}
|
||||
|
||||
export function compareOpenClawVersions(
|
||||
a: string | null | undefined,
|
||||
b: string | null | undefined,
|
||||
@@ -42,8 +73,91 @@ export function compareOpenClawVersions(
|
||||
if (parsedA.patch !== parsedB.patch) {
|
||||
return parsedA.patch < parsedB.patch ? -1 : 1;
|
||||
}
|
||||
if (parsedA.revision !== parsedB.revision) {
|
||||
|
||||
const rankA = releaseRank(parsedA);
|
||||
const rankB = releaseRank(parsedB);
|
||||
if (rankA !== rankB) {
|
||||
return rankA < rankB ? -1 : 1;
|
||||
}
|
||||
|
||||
if (
|
||||
parsedA.revision != null &&
|
||||
parsedB.revision != null &&
|
||||
parsedA.revision !== parsedB.revision
|
||||
) {
|
||||
return parsedA.revision < parsedB.revision ? -1 : 1;
|
||||
}
|
||||
|
||||
if (parsedA.prerelease || parsedB.prerelease) {
|
||||
return comparePrerelease(parsedA.prerelease, parsedB.prerelease);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function normalizeLegacyDotBetaVersion(version: string): string {
|
||||
const dotBetaMatch = /^([vV]?[0-9]+\.[0-9]+\.[0-9]+)\.beta(?:\.([0-9A-Za-z.-]+))?$/.exec(version);
|
||||
if (!dotBetaMatch) {
|
||||
return version;
|
||||
}
|
||||
const base = dotBetaMatch[1];
|
||||
const suffix = dotBetaMatch[2];
|
||||
return suffix ? `${base}-beta.${suffix}` : `${base}-beta`;
|
||||
}
|
||||
|
||||
function releaseRank(version: OpenClawVersion): number {
|
||||
if (version.prerelease?.length) {
|
||||
return 0;
|
||||
}
|
||||
if (version.revision != null) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function comparePrerelease(a: string[] | null, b: string[] | null): number {
|
||||
if (!a?.length && !b?.length) {
|
||||
return 0;
|
||||
}
|
||||
if (!a?.length) {
|
||||
return 1;
|
||||
}
|
||||
if (!b?.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const max = Math.max(a.length, b.length);
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const ai = a[i];
|
||||
const bi = b[i];
|
||||
if (ai == null && bi == null) {
|
||||
return 0;
|
||||
}
|
||||
if (ai == null) {
|
||||
return -1;
|
||||
}
|
||||
if (bi == null) {
|
||||
return 1;
|
||||
}
|
||||
if (ai === bi) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const aiNumeric = /^[0-9]+$/.test(ai);
|
||||
const biNumeric = /^[0-9]+$/.test(bi);
|
||||
if (aiNumeric && biNumeric) {
|
||||
const aiNum = Number.parseInt(ai, 10);
|
||||
const biNum = Number.parseInt(bi, 10);
|
||||
return aiNum < biNum ? -1 : 1;
|
||||
}
|
||||
if (aiNumeric && !biNumeric) {
|
||||
return -1;
|
||||
}
|
||||
if (!aiNumeric && biNumeric) {
|
||||
return 1;
|
||||
}
|
||||
return ai < bi ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,63 @@ describe("buildPluginStatusReport", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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: {
|
||||
|
||||
@@ -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<typeof loadConfig>;
|
||||
workspaceDir?: string;
|
||||
@@ -136,6 +152,10 @@ export function buildPluginStatusReport(params?: {
|
||||
return {
|
||||
workspaceDir,
|
||||
...registry,
|
||||
plugins: registry.plugins.map((plugin) => ({
|
||||
...plugin,
|
||||
version: resolveReportedPluginVersion(plugin, params?.env),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user