Files
openclaw/src/plugins/install-record-commit.retention.test.ts
T
Peter Steinberger 0fe49c5731 fix(plugins): prevent uninstalled npm plugins from coming back (#113902)
* fix(plugins): preserve retained uninstall files

* test(plugins): track retention temp dirs

* fix(plugins): retain rollback marker snapshots
2026-08-09 08:12:05 -07:00

142 lines
5.2 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
import { commitPluginInstallRecordsWithConfig } from "./install-record-commit.js";
import { listRecoveredManagedNpmInstallCandidates } from "./installed-plugin-index-record-reader.js";
import {
cleanupRetainedManagedNpmInstallGenerations,
hasRetainedManagedNpmInstallMarker,
} from "./managed-npm-retention.js";
import { writeManagedNpmPlugin } from "./test-helpers/managed-npm-plugin.js";
function npmRecord(packageName: string, installPath: string): PluginInstallRecord {
return { source: "npm", spec: `${packageName}@1.0.0`, installPath };
}
describe("retained managed npm record commits", () => {
it("suppresses recovery when a retained install record is removed", async () => {
await withOpenClawTestState({ label: "retained-record-removal" }, async (state) => {
const packageName = "@openclaw/retained-demo";
const installPath = writeManagedNpmPlugin({
stateDir: state.stateDir,
packageName,
pluginId: "retained-demo",
version: "1.0.0",
});
expect(
listRecoveredManagedNpmInstallCandidates({ stateDir: state.stateDir }).map(
(candidate) => candidate.pluginId,
),
).toContain("retained-demo");
await commitPluginInstallRecordsWithConfig({
previousInstallRecords: { "retained-demo": npmRecord(packageName, installPath) },
nextInstallRecords: {},
nextConfig: {},
});
expect(hasRetainedManagedNpmInstallMarker(installPath)).toBe(true);
expect(
listRecoveredManagedNpmInstallCandidates({ stateDir: state.stateDir }).map(
(candidate) => candidate.pluginId,
),
).not.toContain("retained-demo");
});
});
it.each(["direct", "symlink"] as const)(
"does not retire a package still used by a %s active install path",
async (activePathKind) => {
await withOpenClawTestState({ label: `retained-active-${activePathKind}` }, async (state) => {
const packageName = "@openclaw/retained-active";
const installPath = writeManagedNpmPlugin({
stateDir: state.stateDir,
packageName,
pluginId: "retained-active",
version: "1.0.0",
});
let activePath = installPath;
if (activePathKind === "symlink") {
activePath = state.statePath("active", "retained-active");
fs.mkdirSync(path.dirname(activePath), { recursive: true });
fs.symlinkSync(installPath, activePath, "dir");
}
await commitPluginInstallRecordsWithConfig({
previousInstallRecords: { "retained-active": npmRecord(packageName, installPath) },
nextInstallRecords: {
"active-alias": {
source: "path",
sourcePath: activePath,
installPath: activePath,
},
},
nextConfig: {},
});
expect(hasRetainedManagedNpmInstallMarker(installPath)).toBe(false);
});
},
);
it("does not retire a removed npm record outside the managed npm root", async () => {
await withOpenClawTestState({ label: "retained-outside-root" }, async (state) => {
const outsideRoot = state.path("outside-root");
try {
const packageName = "@openclaw/outside-retained";
const installPath = writeManagedNpmPlugin({
stateDir: outsideRoot,
packageName,
pluginId: "outside-retained",
version: "1.0.0",
});
await commitPluginInstallRecordsWithConfig({
previousInstallRecords: { "outside-retained": npmRecord(packageName, installPath) },
nextInstallRecords: {},
nextConfig: {},
});
expect(hasRetainedManagedNpmInstallMarker(installPath)).toBe(false);
} finally {
fs.rmSync(outsideRoot, { recursive: true, force: true });
}
});
});
it("keeps npm-to-local source changes cleanup-eligible", async () => {
await withOpenClawTestState({ label: "retained-source-change" }, async (state) => {
const packageName = "@openclaw/moved-local";
const installPath = writeManagedNpmPlugin({
stateDir: state.stateDir,
packageName,
pluginId: "moved-local",
version: "1.0.0",
});
const localInstallPath = state.statePath("extensions", "moved-local");
fs.mkdirSync(localInstallPath, { recursive: true });
await commitPluginInstallRecordsWithConfig({
previousInstallRecords: { "moved-local": npmRecord(packageName, installPath) },
nextInstallRecords: {
"moved-local": {
source: "path",
sourcePath: localInstallPath,
installPath: localInstallPath,
},
},
nextConfig: {},
});
expect(hasRetainedManagedNpmInstallMarker(installPath)).toBe(true);
await expect(
cleanupRetainedManagedNpmInstallGenerations({
activeInstallPaths: [localInstallPath],
}),
).resolves.toBe(1);
expect(fs.existsSync(installPath)).toBe(false);
expect(fs.existsSync(localInstallPath)).toBe(true);
});
});
});