diff --git a/src/plugins/installed-plugin-index-record-builder.ts b/src/plugins/installed-plugin-index-record-builder.ts index 3147df8452b..2135adab3c4 100644 --- a/src/plugins/installed-plugin-index-record-builder.ts +++ b/src/plugins/installed-plugin-index-record-builder.ts @@ -76,7 +76,9 @@ function buildStartupInfo(record: PluginManifestRecord): InstalledPluginStartupI }; } -function collectCompatCodes(record: PluginManifestRecord): readonly PluginCompatCode[] { +export function collectPluginManifestCompatCodes( + record: PluginManifestRecord, +): readonly PluginCompatCode[] { const codes: PluginCompatCode[] = []; if (isLegacyImplicitStartupSidecar(record)) { codes.push("legacy-implicit-startup-sidecar"); @@ -271,7 +273,7 @@ export function buildInstalledPluginIndexRecords(params: { origin: record.origin, enabled, startup: buildStartupInfo(record), - compat: collectCompatCodes(record), + compat: collectPluginManifestCompatCodes(record), }; if (record.format && record.format !== "openclaw") { indexRecord.format = record.format; diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 406bcff8c61..ff6f131456b 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -57,6 +57,7 @@ import { listRegisteredCompactionProviders, restoreRegisteredCompactionProviders, } from "./compaction-provider.js"; +import type { PluginCompatCode } from "./compat/registry.js"; import { applyTestPluginDefaults, createPluginActivationSource, @@ -71,6 +72,7 @@ import { import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js"; import { getGlobalHookRunner, initializeGlobalHookRunner } from "./hook-runner-global.js"; import { toSafeImportPath } from "./import-specifier.js"; +import { collectPluginManifestCompatCodes } from "./installed-plugin-index-record-builder.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js"; import { clearPluginInteractiveHandlers, @@ -1755,6 +1757,7 @@ function createPluginRecord(params: { origin: PluginRecord["origin"]; workspaceDir?: string; enabled: boolean; + compat?: readonly PluginCompatCode[]; activationState?: PluginActivationState; syntheticAuthRefs?: string[]; configSchema: boolean; @@ -1773,6 +1776,7 @@ function createPluginRecord(params: { origin: params.origin, workspaceDir: params.workspaceDir, enabled: params.enabled, + compat: params.compat, explicitlyEnabled: params.activationState?.explicitlyEnabled, activated: params.activationState?.activated, activationSource: params.activationState?.source, @@ -2435,6 +2439,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi origin: candidate.origin, workspaceDir: candidate.workspaceDir, enabled: false, + compat: collectPluginManifestCompatCodes(manifestRecord), activationState, syntheticAuthRefs: manifestRecord.syntheticAuthRefs, configSchema: Boolean(manifestRecord.configSchema), @@ -2469,6 +2474,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi origin: candidate.origin, workspaceDir: candidate.workspaceDir, enabled: enableState.enabled, + compat: collectPluginManifestCompatCodes(manifestRecord), activationState, syntheticAuthRefs: manifestRecord.syntheticAuthRefs, configSchema: Boolean(manifestRecord.configSchema), @@ -3342,6 +3348,7 @@ export async function loadOpenClawPluginCliRegistry( origin: candidate.origin, workspaceDir: candidate.workspaceDir, enabled: false, + compat: collectPluginManifestCompatCodes(manifestRecord), activationState, syntheticAuthRefs: manifestRecord.syntheticAuthRefs, configSchema: Boolean(manifestRecord.configSchema), @@ -3376,6 +3383,7 @@ export async function loadOpenClawPluginCliRegistry( origin: candidate.origin, workspaceDir: candidate.workspaceDir, enabled: enableState.enabled, + compat: collectPluginManifestCompatCodes(manifestRecord), activationState, syntheticAuthRefs: manifestRecord.syntheticAuthRefs, configSchema: Boolean(manifestRecord.configSchema), diff --git a/src/plugins/status.compatibility.integration.test.ts b/src/plugins/status.compatibility.integration.test.ts new file mode 100644 index 00000000000..9e6d3b401c7 --- /dev/null +++ b/src/plugins/status.compatibility.integration.test.ts @@ -0,0 +1,70 @@ +import fs from "node:fs"; +import path from "node:path"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; +import { withEnv } from "../test-utils/env.js"; +import { + cleanupPluginLoaderFixturesForTest, + makeTempDir, + resetPluginLoaderTestStateForTest, + useNoBundledPlugins, + writePlugin, +} from "./loader.test-fixtures.js"; +import { buildPluginCompatibilitySnapshotNotices } from "./status.js"; + +function addStartupActivation(pluginDir: string, onStartup: boolean): void { + const manifestPath = path.join(pluginDir, "openclaw.plugin.json"); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as Record; + fs.writeFileSync( + manifestPath, + `${JSON.stringify({ ...manifest, activation: { onStartup } }, null, 2)}\n`, + "utf-8", + ); +} + +function buildSnapshotCompatibilityNoticeCodes(plugin: { dir: string; file: string; id: string }) { + const stateDir = makeTempDir(); + return withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => { + useNoBundledPlugins(); + return buildPluginCompatibilitySnapshotNotices({ + config: { + plugins: { + load: { paths: [plugin.file] }, + allow: [plugin.id], + }, + }, + workspaceDir: plugin.dir, + env: process.env, + }).map((notice) => notice.code); + }); +} + +describe("plugin compatibility snapshot notices", () => { + afterEach(() => { + resetPluginLoaderTestStateForTest(); + }); + + afterAll(() => { + cleanupPluginLoaderFixturesForTest(); + }); + + it("reports implicit startup sidecar compatibility from a real legacy manifest", () => { + const plugin = writePlugin({ + id: "legacy-sidecar", + body: `module.exports = { id: "legacy-sidecar", register() {} };\n`, + }); + + expect(buildSnapshotCompatibilityNoticeCodes(plugin)).toEqual([ + "legacy-implicit-startup-sidecar", + ]); + }); + + it("does not report implicit startup compatibility for explicit startup-lazy manifests", () => { + const plugin = writePlugin({ + id: "modern-startup-lazy", + body: `module.exports = { id: "modern-startup-lazy", register() {} };\n`, + }); + addStartupActivation(plugin.dir, false); + + expect(buildSnapshotCompatibilityNoticeCodes(plugin)).toEqual([]); + }); +});