fix(update): prune stale local bundled plugin shadows

Summary:\n- prune stale local bundled plugin path records during update/doctor repair\n- keep current, same-version, versionless, source-checkout, and arbitrary local path records preserved\n- add changelog and deterministic sort comparator cleanup\n\nVerification:\n- node scripts/run-vitest.mjs src/plugins/contracts/boundary-invariants.test.ts src/plugins/stale-local-bundled-plugin-install-records.test.ts src/cli/update-cli/post-core-plugin-convergence.test.ts src/commands/doctor-plugin-registry.test.ts\n- node scripts/run-oxlint-shards.mjs --threads=8\n- ./node_modules/.bin/oxfmt --check --threads=1 CHANGELOG.md src/plugins/stale-local-bundled-plugin-install-records.ts src/commands/doctor-plugin-registry.ts\n- git diff --check\n- GitHub exact-SHA: Real behavior proof, build-artifacts, checks-fast-contracts-plugins-a, check-prod-types, check-lint, check-test-types green on 8bcbf681ec
This commit is contained in:
Jason (Json)
2026-05-21 01:49:19 -06:00
committed by GitHub
parent 3eb2d64392
commit 4a360ac1cc
8 changed files with 609 additions and 8 deletions

View File

@@ -1,4 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
repairMissingConfiguredPluginInstalls: vi.fn(),
@@ -21,6 +24,8 @@ import {
} from "./post-core-plugin-convergence.js";
describe("runPostCorePluginConvergence", () => {
const tempDirs: string[] = [];
beforeEach(() => {
vi.clearAllMocks();
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
@@ -31,6 +36,43 @@ describe("runPostCorePluginConvergence", () => {
mocks.runPluginPayloadSmokeCheck.mockResolvedValue({ checked: [], failures: [] });
});
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
function makeTempDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-post-core-convergence-"));
tempDirs.push(dir);
return dir;
}
function writeBundledPlugin(rootDir: string, pluginId: string): string {
const pluginDir = path.join(rootDir, pluginId);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(path.join(pluginDir, "index.js"), "export default {};\n", "utf8");
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify({
id: pluginId,
name: pluginId,
version: "2026.5.20-beta.1",
configSchema: { type: "object" },
}),
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: `@openclaw/${pluginId}`,
version: "2026.5.20-beta.1",
}),
"utf8",
);
return pluginDir;
}
it("calls repair with OPENCLAW_UPDATE_POST_CORE_CONVERGENCE=1 set", async () => {
const cfg = { plugins: { entries: {} } } as unknown as OpenClawConfig;
await runPostCorePluginConvergence({
@@ -121,6 +163,55 @@ describe("runPostCorePluginConvergence", () => {
});
});
it("prunes stale local bundled plugin shadows from baseline records before repair", async () => {
const bundledRoot = makeTempDir();
writeBundledPlugin(bundledRoot, "discord");
const baseline = {
discord: {
source: "path" as const,
installPath: path.join(makeTempDir(), "dist", "extensions", "discord"),
version: "2026.5.4-beta.3",
},
brave: { source: "npm" as const, installPath: "/p/brave" },
};
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
changes: [],
warnings: [],
records: { brave: baseline.brave },
});
const cfg = {
plugins: { entries: { discord: { enabled: true }, brave: { enabled: true } } },
} as unknown as OpenClawConfig;
const result = await runPostCorePluginConvergence({
cfg,
env: {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
VITEST: "true",
},
baselineInstallRecords: baseline,
});
expect(mocks.repairMissingConfiguredPluginInstalls).toHaveBeenCalledWith({
cfg,
env: {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
VITEST: "true",
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
},
baselineRecords: {
brave: baseline.brave,
},
});
expect(result.changes).toEqual([
'Removed stale local bundled plugin install record "discord".',
]);
expect(result.installRecords).toEqual({ brave: baseline.brave });
});
it("flags errored=true and surfaces actionable guidance when repair warns", async () => {
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
changes: [],

View File

@@ -3,6 +3,7 @@ import { UPDATE_POST_CORE_CONVERGENCE_ENV } from "../../commands/doctor/shared/u
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginInstallRecord } from "../../config/types.plugins.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
import { pruneStaleLocalBundledPluginInstallRecords } from "../../plugins/stale-local-bundled-plugin-install-records.js";
import {
resolveTrustedSourceLinkedOfficialClawHubSpec,
resolveTrustedSourceLinkedOfficialNpmSpec,
@@ -66,11 +67,17 @@ export async function runPostCorePluginConvergence(params: {
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
[UPDATE_POST_CORE_CONVERGENCE_ENV]: "1",
};
const prunedBaseline = params.baselineInstallRecords
? pruneStaleLocalBundledPluginInstallRecords({
installRecords: params.baselineInstallRecords,
env,
})
: null;
const repair = await repairMissingConfiguredPluginInstalls({
cfg: params.cfg,
env,
...(params.baselineInstallRecords ? { baselineRecords: params.baselineInstallRecords } : {}),
...(prunedBaseline ? { baselineRecords: prunedBaseline.records } : {}),
});
const warnings: PostCoreConvergenceWarning[] = repair.warnings.map((message) => ({
@@ -99,7 +106,12 @@ export async function runPostCorePluginConvergence(params: {
}
return {
changes: repair.changes,
changes: [
...(prunedBaseline?.stale.map(
(record) => `Removed stale local bundled plugin install record "${record.pluginId}".`,
) ?? []),
...repair.changes,
],
warnings,
errored: warnings.length > 0,
smokeFailures: smoke.failures,