diff --git a/src/plugins/installed-plugin-index-record-builder.ts b/src/plugins/installed-plugin-index-record-builder.ts index 3f0d1434be5..3147df8452b 100644 --- a/src/plugins/installed-plugin-index-record-builder.ts +++ b/src/plugins/installed-plugin-index-record-builder.ts @@ -55,8 +55,9 @@ function hasRuntimeContractSurface(record: PluginManifestRecord): boolean { * startup decision so Gateway boot can avoid importing inert plugins. */ function isLegacyImplicitStartupSidecar(record: PluginManifestRecord): boolean { + const channels = Array.isArray(record.channels) ? record.channels : []; return ( - record.channels.length === 0 && + channels.length === 0 && !hasRuntimeContractSurface(record) && record.activation?.onStartup === undefined ); diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 83dbfc52af6..8c0faaf5385 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { PluginCandidate } from "./discovery.js"; +import { buildInstalledPluginIndexRecords } from "./installed-plugin-index-record-builder.js"; import { loadInstalledPluginIndexInstallRecordsSync, writePersistedInstalledPluginIndexInstallRecords, @@ -16,6 +17,7 @@ import { refreshInstalledPluginIndex, } from "./installed-plugin-index.js"; import { recordPluginInstall } from "./installs.js"; +import type { PluginManifestRecord } from "./manifest-registry.js"; import type { OpenClawPackageManifest } from "./manifest.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; @@ -289,6 +291,42 @@ describe("installed plugin index", () => { }); }); + it("tolerates stale manifest records without normalized channels", () => { + const rootDir = makeTempDir(); + writeRuntimeEntry(rootDir); + const manifestPath = path.join(rootDir, "openclaw.plugin.json"); + + const records = buildInstalledPluginIndexRecords({ + candidates: [createPluginCandidate({ rootDir })], + registry: { + plugins: [ + { + id: "stale-record", + providers: [], + cliBackends: [], + skills: [], + hooks: [], + origin: "global", + rootDir, + source: path.join(rootDir, "index.ts"), + manifestPath, + } as unknown as PluginManifestRecord, + ], + diagnostics: [], + }, + diagnostics: [], + installRecords: {}, + }); + + expect(records[0]).toMatchObject({ + pluginId: "stale-record", + startup: { + sidecar: true, + }, + compat: ["legacy-implicit-startup-sidecar"], + }); + }); + it("does not classify or tag explicit startup opt-outs as deprecated implicit sidecars", () => { const rootDir = makeTempDir(); writeRuntimeEntry(rootDir);