Keep legacy plugin manifest doctor check lint-only

This commit is contained in:
Gio Della-Libera
2026-07-02 16:01:40 -07:00
parent fa9810e827
commit 2a019933bb
4 changed files with 1 additions and 261 deletions
@@ -9,7 +9,6 @@ import {
collectLegacyPluginManifestContractMigrations,
legacyPluginManifestContractMigrationToHealthFinding,
maybeRepairLegacyPluginManifestContracts,
repairLegacyPluginManifestContractFindings,
} from "./doctor-plugin-manifests.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
@@ -281,63 +280,6 @@ describe("doctor plugin manifest legacy contract repair", () => {
});
});
it("previews legacy manifest rewrites as file effects and diffs", async () => {
const pluginsRoot = await suiteTempDirs.make("preview-capability");
const root = path.join(pluginsRoot, "openai");
fs.mkdirSync(root, { recursive: true });
writePackageJson(root);
writeManifest(root, {
id: "openai",
speechProviders: ["openai"],
configSchema: { type: "object" },
});
const manifestPath = path.join(root, "openclaw.plugin.json");
const result = await repairLegacyPluginManifestContractFindings({
config: configWithPluginLoadPath(pluginsRoot),
env: {
...process.env,
},
manifestRoots: [pluginsRoot],
findings: [
{
checkId: "core/doctor/legacy-plugin-manifests",
severity: "warning",
message: "Plugin manifest openai uses legacy top-level capability keys.",
path: manifestPath,
target: "openai",
requirement: "contracts-capability-keys",
},
],
dryRun: true,
diff: true,
});
expect(result.changes).toEqual([
`- ${manifestPath}: moved speechProviders to contracts.speechProviders`,
]);
expect(result.effects).toEqual([
{
kind: "file",
action: "would-rewrite-legacy-plugin-manifest-contracts",
target: manifestPath,
dryRunSafe: false,
},
]);
expect(result.diffs).toEqual([
expect.objectContaining({
kind: "file",
path: manifestPath,
before: expect.stringContaining('"speechProviders"'),
after: expect.stringContaining('"contracts"'),
}),
]);
const unchanged = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as {
speechProviders?: string[];
};
expect(unchanged.speechProviders).toEqual(["openai"]);
});
it("ignores non-object contracts payloads when collecting migrations", async () => {
const pluginsRoot = await suiteTempDirs.make("non-object-contracts");
const root = path.join(pluginsRoot, "openai");
+1 -132
View File
@@ -6,12 +6,7 @@ import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-
import { z } from "zod";
import { note } from "../../packages/terminal-core/src/note.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type {
HealthFinding,
HealthRepairDiff,
HealthRepairEffect,
HealthRepairResult,
} from "../flows/health-checks.js";
import type { HealthFinding } from "../flows/health-checks.js";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
import type { RuntimeEnv } from "../runtime.js";
import { shortenHomePath } from "../utils.js";
@@ -178,132 +173,6 @@ function migrationToManifestJson(migration: LegacyManifestContractMigration): st
return `${JSON.stringify(migration.nextRaw, null, 2)}\n`;
}
function legacyPluginManifestMigrationToRepairEffect(
migration: LegacyManifestContractMigration,
dryRun: boolean,
): HealthRepairEffect {
return {
kind: "file",
action: dryRun
? "would-rewrite-legacy-plugin-manifest-contracts"
: "rewrite-legacy-plugin-manifest-contracts",
target: migration.manifestPath,
dryRunSafe: false,
};
}
function legacyPluginManifestMigrationToRepairDiff(
migration: LegacyManifestContractMigration,
before: string,
): HealthRepairDiff {
return {
kind: "file",
path: migration.manifestPath,
before,
after: migrationToManifestJson(migration),
};
}
/** Repairs or previews selected legacy plugin manifest contract migrations. */
export async function repairLegacyPluginManifestContractFindings(params: {
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
manifestRoots?: string[];
workspaceDir?: string;
findings: readonly HealthFinding[];
dryRun?: boolean;
diff?: boolean;
deps?: {
readFileSync?: typeof fs.readFileSync;
writeFileSync?: typeof fs.writeFileSync;
};
}): Promise<HealthRepairResult> {
const selectedPaths = new Set(
params.findings
.filter(
(finding) =>
finding.checkId === LEGACY_PLUGIN_MANIFESTS_CHECK_ID &&
finding.requirement === "contracts-capability-keys" &&
typeof finding.path === "string",
)
.map((finding) => finding.path as string),
);
if (selectedPaths.size === 0) {
return {
status: "skipped",
reason: "no repairable legacy plugin manifest findings were present",
changes: [],
};
}
const migrations = collectLegacyPluginManifestContractMigrations({
...(params.config ? { config: params.config } : {}),
...(params.env ? { env: params.env } : {}),
...(params.manifestRoots ? { manifestRoots: params.manifestRoots } : {}),
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
}).filter((migration) => selectedPaths.has(migration.manifestPath));
if (migrations.length === 0) {
return {
status: "skipped",
reason: "selected legacy plugin manifests no longer need repair",
changes: [],
};
}
const readFile = params.deps?.readFileSync ?? fs.readFileSync;
const writeFile = params.deps?.writeFileSync ?? fs.writeFileSync;
const changes: string[] = [];
const diffs: HealthRepairDiff[] = [];
const effects: HealthRepairEffect[] = [];
const warnings: string[] = [];
for (const migration of migrations) {
let before: string | undefined;
if (params.diff === true) {
try {
before = readFile(migration.manifestPath, "utf-8");
diffs.push(legacyPluginManifestMigrationToRepairDiff(migration, before));
} catch (error) {
warnings.push(
`Could not read legacy plugin manifest at ${migration.manifestPath}: ${String(error)}`,
);
}
}
if (params.dryRun !== true) {
try {
writeFile(migration.manifestPath, migrationToManifestJson(migration), "utf-8");
} catch (error) {
warnings.push(
`Failed to rewrite legacy plugin manifest at ${migration.manifestPath}: ${String(error)}`,
);
continue;
}
}
changes.push(...migration.changeLines);
effects.push(legacyPluginManifestMigrationToRepairEffect(migration, params.dryRun === true));
}
if (changes.length === 0 && warnings.length > 0) {
return {
status: "failed",
reason: "could not rewrite selected legacy plugin manifests",
changes,
warnings,
diffs,
effects,
};
}
return {
changes,
warnings,
diffs,
effects,
};
}
/** Prompts and rewrites legacy plugin manifest contract fields when doctor repair is enabled. */
export async function maybeRepairLegacyPluginManifestContracts(params: {
config?: OpenClawConfig;
@@ -69,10 +69,6 @@ const mocks = vi.hoisted(() => ({
}),
),
maybeRepairLegacyPluginManifestContracts: vi.fn().mockResolvedValue(undefined),
repairLegacyPluginManifestContractFindings: vi.fn(async () => ({
changes: [] as string[],
effects: [] as unknown[],
})),
detectLegacyClawdBrowserProfileResidue: vi.fn(),
maybeArchiveLegacyClawdBrowserProfileResidue: vi.fn(),
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/openclaw-workspace"),
@@ -179,7 +175,6 @@ vi.mock("../commands/doctor-plugin-manifests.js", () => ({
legacyPluginManifestContractMigrationToHealthFinding:
mocks.legacyPluginManifestContractMigrationToHealthFinding,
maybeRepairLegacyPluginManifestContracts: mocks.maybeRepairLegacyPluginManifestContracts,
repairLegacyPluginManifestContractFindings: mocks.repairLegacyPluginManifestContractFindings,
}));
vi.mock("../commands/doctor-auth-oauth-sidecar.js", () => ({
@@ -390,11 +385,6 @@ describe("doctor health contributions", () => {
mocks.legacyPluginManifestContractMigrationToHealthFinding.mockClear();
mocks.maybeRepairLegacyPluginManifestContracts.mockClear();
mocks.maybeRepairLegacyPluginManifestContracts.mockResolvedValue(undefined);
mocks.repairLegacyPluginManifestContractFindings.mockClear();
mocks.repairLegacyPluginManifestContractFindings.mockResolvedValue({
changes: [],
effects: [],
});
mocks.maybeRepairLegacyOAuthSidecarProfiles.mockClear();
mocks.maybeRepairLegacyOAuthSidecarProfiles.mockResolvedValue(undefined);
mocks.collectAuthProfileHealthFindings.mockClear();
@@ -566,56 +556,6 @@ describe("doctor health contributions", () => {
);
});
it("threads dry-run legacy plugin manifest repairs through the structured check", async () => {
const contribution = requireDoctorContribution("doctor:legacy-plugin-manifests");
const check = contribution.healthChecks[0] as HealthCheck;
const findings = [
{
checkId: "core/doctor/legacy-plugin-manifests",
severity: "warning" as const,
message: "Plugin manifest legacy-plugin uses legacy top-level capability keys.",
path: "/tmp/openclaw-plugin/openclaw.plugin.json",
target: "legacy-plugin",
requirement: "contracts-capability-keys",
},
];
mocks.repairLegacyPluginManifestContractFindings.mockResolvedValueOnce({
changes: ["Would rewrite legacy manifest."],
effects: [
{
kind: "file",
action: "would-rewrite-legacy-plugin-manifest-contracts",
target: "/tmp/openclaw-plugin/openclaw.plugin.json",
dryRunSafe: false,
},
],
});
const result = await check.repair?.(
{
cfg: { plugins: { load: { paths: ["/tmp/openclaw-plugin"] } } },
mode: "fix",
dryRun: true,
diff: true,
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
},
findings,
);
expect(mocks.repairLegacyPluginManifestContractFindings).toHaveBeenCalledWith({
config: { plugins: { load: { paths: ["/tmp/openclaw-plugin"] } } },
env: process.env,
findings,
dryRun: true,
diff: true,
});
expect(result?.effects).toContainEqual(
expect.objectContaining({
action: "would-rewrite-legacy-plugin-manifest-contracts",
}),
);
});
it("runs release configured plugin install repair before plugin registry and final config writes", () => {
const ids = resolveDoctorHealthContributions().map((entry) => entry.id);
-11
View File
@@ -1353,17 +1353,6 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] {
env: process.env,
}).map(legacyPluginManifestContractMigrationToHealthFinding);
},
async repair(ctx, findings) {
const { repairLegacyPluginManifestContractFindings } =
await import("../commands/doctor-plugin-manifests.js");
return await repairLegacyPluginManifestContractFindings({
config: ctx.cfg,
env: process.env,
findings,
dryRun: ctx.dryRun,
diff: ctx.diff,
});
},
},
run: runLegacyPluginManifestHealth,
}),