From 0b663e7a62efc3d17eda81920b236b8921bd04bb Mon Sep 17 00:00:00 2001 From: "Jason (Json)" <263060202+fuller-stack-dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:01:49 -0600 Subject: [PATCH] fix(plugins): preserve legacy runtime-doctor imports (#121220) * fix(plugins): preserve legacy runtime doctor imports * fix(plugins): map legacy doctor package types --- docs/plugins/sdk-migration.md | 2 +- .../tsconfig.package-boundary.paths.json | 3 + extensions/xai/tsconfig.json | 3 + package.json | 3 + scripts/lib/plugin-sdk-entrypoints.json | 1 + ...lugin-sdk-private-local-only-subpaths.json | 1 + src/plugin-sdk/runtime-doctor-migrations.ts | 3 +- src/plugin-sdk/runtime-doctor.test.ts | 16 ++++ src/plugin-sdk/runtime-doctor.ts | 5 ++ .../doctor-contract-closure-guard.test.ts | 9 ++ ...octor-contract-registry.load-paths.test.ts | 84 +++++++++++++++++++ 11 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/plugin-sdk/runtime-doctor.test.ts create mode 100644 src/plugin-sdk/runtime-doctor.ts diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 890f126e4fa6..6f95a0e8402d 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -89,7 +89,7 @@ Channel plugins should declare `doctorContract.stateMigrations: true` in `openclaw.plugin.json` and export `stateMigrations` from their doctor-contract artifact. Plan-based migrations can use `definePluginDoctorMigrationFromPlans(...)` from -`openclaw/plugin-sdk/runtime-doctor` to preserve existing move, copy, preview, +`openclaw/plugin-sdk/runtime-doctor-migrations` to preserve existing move, copy, preview, and plugin-state import behavior. The setup-entry `legacyStateMigrations` option and feature flag, diff --git a/extensions/tsconfig.package-boundary.paths.json b/extensions/tsconfig.package-boundary.paths.json index f6748439368a..2134791ae3e5 100644 --- a/extensions/tsconfig.package-boundary.paths.json +++ b/extensions/tsconfig.package-boundary.paths.json @@ -14,6 +14,9 @@ "openclaw/plugin-sdk/runtime-doctor-migrations": [ "../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts" ], + "openclaw/plugin-sdk/runtime-doctor": [ + "../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor.d.ts" + ], "openclaw/plugin-sdk/approval-reference-runtime": [ "../packages/plugin-sdk/dist/src/plugin-sdk/approval-reference-runtime.d.ts" ], diff --git a/extensions/xai/tsconfig.json b/extensions/xai/tsconfig.json index 02ed867339fb..57f82ce522ec 100644 --- a/extensions/xai/tsconfig.json +++ b/extensions/xai/tsconfig.json @@ -14,6 +14,9 @@ "openclaw/plugin-sdk/runtime-doctor-migrations": [ "../../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor-migrations.d.ts" ], + "openclaw/plugin-sdk/runtime-doctor": [ + "../../packages/plugin-sdk/dist/src/plugin-sdk/runtime-doctor.d.ts" + ], "openclaw/plugin-sdk/approval-reference-runtime": [ "../../packages/plugin-sdk/dist/src/plugin-sdk/approval-reference-runtime.d.ts" ], diff --git a/package.json b/package.json index 4947225634ff..cba35a0fd4ad 100644 --- a/package.json +++ b/package.json @@ -426,6 +426,9 @@ "./plugin-sdk/plugin-state-store-runtime": { "default": "./dist/plugin-sdk/plugin-state-store-runtime.js" }, + "./plugin-sdk/runtime-doctor": { + "default": "./dist/plugin-sdk/runtime-doctor.js" + }, "./plugin-sdk/runtime-doctor-migrations": { "default": "./dist/plugin-sdk/runtime-doctor-migrations.js" }, diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 2cf37b04d72a..8b6a34e9d7da 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -11,6 +11,7 @@ "secret-provider-alias", "session-store-paths", "plugin-state-store-runtime", + "runtime-doctor", "runtime-doctor-migrations", "runtime-env", "proxy-capture", diff --git a/scripts/lib/plugin-sdk-private-local-only-subpaths.json b/scripts/lib/plugin-sdk-private-local-only-subpaths.json index 8208e3aeaaa1..5b4a0eeb0e95 100644 --- a/scripts/lib/plugin-sdk-private-local-only-subpaths.json +++ b/scripts/lib/plugin-sdk-private-local-only-subpaths.json @@ -139,6 +139,7 @@ "request-url", "response-limit-runtime", "retry-runtime", + "runtime-doctor", "runtime-doctor-migrations", "runtime-fetch", "sandbox", diff --git a/src/plugin-sdk/runtime-doctor-migrations.ts b/src/plugin-sdk/runtime-doctor-migrations.ts index 514384d26ffd..36cc1e0fdfab 100644 --- a/src/plugin-sdk/runtime-doctor-migrations.ts +++ b/src/plugin-sdk/runtime-doctor-migrations.ts @@ -3,7 +3,8 @@ * * Doctor contract enumeration cold-loads plugin `doctor-contract-api` closures, so * this subpath must stay off heavy runtime graphs (state DB, plugin state stores, - * uninstall flows). Those stay on `runtime-doctor`. + * uninstall flows). Those stay on focused repair and plugin-state-store subpaths; + * the deprecated `runtime-doctor` package facade re-exports only this light module. */ import fs from "node:fs/promises"; import { asObjectRecord } from "../config/channel-compat-normalization.js"; diff --git a/src/plugin-sdk/runtime-doctor.test.ts b/src/plugin-sdk/runtime-doctor.test.ts new file mode 100644 index 000000000000..2f224b3b4853 --- /dev/null +++ b/src/plugin-sdk/runtime-doctor.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import * as runtimeDoctorMigrations from "./runtime-doctor-migrations.js"; +import * as legacyRuntimeDoctor from "./runtime-doctor.js"; + +describe("legacy runtime-doctor package facade", () => { + it("is exactly the dependency-light migration surface", () => { + expect(Object.keys(legacyRuntimeDoctor).toSorted()).toEqual( + Object.keys(runtimeDoctorMigrations).toSorted(), + ); + for (const key of Object.keys(runtimeDoctorMigrations)) { + expect(legacyRuntimeDoctor[key as keyof typeof legacyRuntimeDoctor]).toBe( + runtimeDoctorMigrations[key as keyof typeof runtimeDoctorMigrations], + ); + } + }); +}); diff --git a/src/plugin-sdk/runtime-doctor.ts b/src/plugin-sdk/runtime-doctor.ts new file mode 100644 index 000000000000..d222b845f2d1 --- /dev/null +++ b/src/plugin-sdk/runtime-doctor.ts @@ -0,0 +1,5 @@ +/** + * @deprecated Package-only compatibility for pre-split official plugin doctor artifacts. + * Current source must import `runtime-doctor-migrations` directly. + */ +export * from "./runtime-doctor-migrations.js"; diff --git a/src/plugins/doctor-contract-closure-guard.test.ts b/src/plugins/doctor-contract-closure-guard.test.ts index 207216dff874..274c2681827b 100644 --- a/src/plugins/doctor-contract-closure-guard.test.ts +++ b/src/plugins/doctor-contract-closure-guard.test.ts @@ -34,6 +34,15 @@ const FORBIDDEN_SPECIFIER_RULES = new Map typeof helper !== "function")) { + throw new Error("legacy runtime-doctor helper missing"); +} +export const legacyConfigRules = [{ + path: ["plugins", "entries", ${JSON.stringify(params.pluginId)}, "config", "legacyDoctor"], + message: ${JSON.stringify(`${params.pluginId} legacy doctor contract loaded`)}, +}]; +`, + "utf8", + ); +} + function writeLegacyChannelMigrationPlugin(params: { pluginRoot: string; pluginId: string; @@ -531,6 +573,48 @@ describe("doctor contract registry load-path plugins", () => { ]); }); + it.each([ + { + pluginId: "clickclack-legacy-doctor", + importedSymbols: ["asObjectRecord"], + }, + { + pluginId: "codex-legacy-doctor", + importedSymbols: ["archiveLegacyStateSource", "legacyStateFileExists"], + }, + { + pluginId: "discord-legacy-doctor", + importedSymbols: [ + "asObjectRecord", + "collectChannelAccountScopes", + "collectProviderDangerousNameMatchingScopes", + "defineChannelAliasMigration", + "defineKeyMoveMigration", + "hasLegacyAccountStreamingAliases", + "normalizeChannelAccounts", + "stripRetiredChannelKeys", + ], + }, + ])("loads the preserved $pluginId package contract", ({ pluginId, importedSymbols }) => { + const stateDir = tempDirs.make("openclaw-doctor-contract-legacy-package-"); + const pluginRoot = tempDirs.make("openclaw-doctor-contract-legacy-package-"); + writeLegacyRuntimeDoctorPlugin({ pluginRoot, pluginId, importedSymbols }); + const config = createDoctorPluginConfig(pluginRoot, pluginId); + + expect( + listPluginDoctorLegacyConfigRules({ + config, + env: makeHermeticDoctorEnv(stateDir), + pluginIds: [pluginId], + }), + ).toEqual([ + { + path: ["plugins", "entries", pluginId, "config", "legacyDoctor"], + message: `${pluginId} legacy doctor contract loaded`, + }, + ]); + }); + it("applies compatibility normalizers from plugins.load.paths", () => { const stateDir = tempDirs.make("openclaw-doctor-contract-load-paths-"); const pluginRoot = tempDirs.make("openclaw-doctor-contract-load-paths-");