fix(plugins): preserve legacy runtime-doctor imports (#121220)

* fix(plugins): preserve legacy runtime doctor imports

* fix(plugins): map legacy doctor package types
This commit is contained in:
Jason (Json)
2026-08-09 19:01:49 -06:00
committed by GitHub
parent d7cdb2e60b
commit 0b663e7a62
11 changed files with 128 additions and 2 deletions
+1 -1
View File
@@ -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,
@@ -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"
],
+3
View File
@@ -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"
],
+3
View File
@@ -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"
},
+1
View File
@@ -11,6 +11,7 @@
"secret-provider-alias",
"session-store-paths",
"plugin-state-store-runtime",
"runtime-doctor",
"runtime-doctor-migrations",
"runtime-env",
"proxy-capture",
@@ -139,6 +139,7 @@
"request-url",
"response-limit-runtime",
"retry-runtime",
"runtime-doctor",
"runtime-doctor-migrations",
"runtime-fetch",
"sandbox",
+2 -1
View File
@@ -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";
+16
View File
@@ -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],
);
}
});
});
+5
View File
@@ -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";
@@ -34,6 +34,15 @@ const FORBIDDEN_SPECIFIER_RULES = new Map<string, { reason: string; kinds: Set<C
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
[
"openclaw/plugin-sdk/runtime-doctor",
{
reason:
"the retired package path exists only for shipped plugin artifacts; " +
"current source must use openclaw/plugin-sdk/runtime-doctor-migrations",
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
[
"openclaw/plugin-sdk/ssrf-runtime",
{
@@ -137,6 +137,48 @@ module.exports = {
);
}
function writeLegacyRuntimeDoctorPlugin(params: {
pluginRoot: string;
pluginId: string;
importedSymbols: readonly string[];
}): void {
fs.mkdirSync(path.join(params.pluginRoot, "dist"), { recursive: true });
fs.writeFileSync(
path.join(params.pluginRoot, "openclaw.plugin.json"),
JSON.stringify({
id: params.pluginId,
doctorContract: { configRepair: true },
configSchema: {},
}),
"utf8",
);
fs.writeFileSync(
path.join(params.pluginRoot, "package.json"),
JSON.stringify({
name: `@openclaw/${params.pluginId}`,
version: "2026.7.2-beta.7",
type: "module",
openclaw: { extensions: ["./dist/index.js"] },
}),
"utf8",
);
fs.writeFileSync(path.join(params.pluginRoot, "dist", "index.js"), "export {};\n", "utf8");
fs.writeFileSync(
path.join(params.pluginRoot, "dist", "doctor-contract-api.js"),
`import { ${params.importedSymbols.join(", ")} } from "openclaw/plugin-sdk/runtime-doctor";
const importedHelpers = [${params.importedSymbols.join(", ")}];
if (importedHelpers.some((helper) => 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-");