Files
openclaw/test/scripts/release-plugin-marketplace-lifecycle.test.ts
Vincent Koc 1d446ad2cc fix(plugins): remove stale load paths during uninstall (#118930)
* test(qa): prove marketplace plugin updates

* fix(plugins): clean exact install load paths on uninstall

* fix(ci): register marketplace lifecycle assertions
2026-08-03 21:57:54 +00:00

241 lines
7.4 KiB
TypeScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const HELPER = path.resolve("scripts/e2e/lib/release-plugin-marketplace/lifecycle-assertions.mjs");
const tempDirs = new Set<string>();
afterEach(() => {
cleanupTempDirs(tempDirs);
});
function writeIndex(
stateDir: string,
pluginId: string,
record: Record<string, unknown>,
packageVersion: string,
) {
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
const db = new DatabaseSync(databasePath);
try {
db.exec(`
CREATE TABLE IF NOT EXISTS installed_plugin_index (
index_key TEXT NOT NULL PRIMARY KEY,
version INTEGER NOT NULL,
host_contract_version TEXT NOT NULL,
compat_registry_version TEXT NOT NULL,
migration_version INTEGER NOT NULL,
policy_hash TEXT NOT NULL,
generated_at_ms INTEGER NOT NULL,
refresh_reason TEXT,
install_records_json TEXT NOT NULL,
plugins_json TEXT NOT NULL,
diagnostics_json TEXT NOT NULL,
warning TEXT,
updated_at_ms INTEGER NOT NULL
);
`);
const now = Date.now();
db.prepare(
`
INSERT OR REPLACE INTO installed_plugin_index (
index_key, version, host_contract_version, compat_registry_version,
migration_version, policy_hash, generated_at_ms, refresh_reason,
install_records_json, plugins_json, diagnostics_json, warning, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
"installed-plugin-index",
1,
"test",
"test",
1,
"test",
now,
"source-changed",
JSON.stringify({ [pluginId]: record }),
JSON.stringify([{ pluginId, packageVersion }]),
"[]",
null,
now,
);
} finally {
db.close();
}
}
function runHelper(home: string, args: string[]) {
const stateDir = path.join(home, ".openclaw");
return spawnSync(process.execPath, [HELPER, ...args], {
encoding: "utf8",
env: {
...process.env,
HOME: home,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
OPENCLAW_STATE_DIR: stateDir,
},
});
}
function writeMarketplaceState(home: string, version: string) {
const pluginId = "release-marketplace-plugin";
const stateDir = path.join(home, ".openclaw");
const installPath = path.join(stateDir, "extensions", pluginId);
fs.mkdirSync(installPath, { recursive: true });
fs.writeFileSync(
path.join(installPath, "package.json"),
`${JSON.stringify({ name: `@openclaw/${pluginId}`, version })}\n`,
"utf8",
);
fs.writeFileSync(
path.join(stateDir, "openclaw.json"),
`${JSON.stringify({
plugins: { entries: { [pluginId]: { enabled: true } } },
})}\n`,
"utf8",
);
writeIndex(
stateDir,
pluginId,
{
source: "marketplace",
installPath,
version,
marketplaceName: "Release Fixture Marketplace",
marketplaceSource: "release-fixtures",
marketplacePlugin: pluginId,
},
version,
);
return { installPath, pluginId };
}
function clearMarketplaceIndex(home: string) {
const databasePath = path.join(home, ".openclaw", "state", "openclaw.sqlite");
const db = new DatabaseSync(databasePath);
try {
db.prepare(
`
UPDATE installed_plugin_index
SET install_records_json = ?,
plugins_json = ?,
updated_at_ms = ?
WHERE index_key = ?
`,
).run("{}", "[]", Date.now(), "installed-plugin-index");
} finally {
db.close();
}
}
describe("release plugin marketplace lifecycle assertions", () => {
it("checks canonical marketplace metadata and stable managed install paths", () => {
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
const { installPath, pluginId } = writeMarketplaceState(home, "0.0.1");
const installPathFile = path.join(home, "install-path.txt");
const initial = runHelper(home, [
"assert-marketplace-state",
pluginId,
"0.0.1",
"release-fixtures",
pluginId,
installPathFile,
]);
expect(initial.stderr).toBe("");
expect(initial.status).toBe(0);
expect(fs.readFileSync(installPathFile, "utf8").trim()).toBe(installPath);
writeMarketplaceState(home, "0.0.2");
const updated = runHelper(home, [
"assert-marketplace-state",
pluginId,
"0.0.2",
"release-fixtures",
pluginId,
installPathFile,
]);
expect(updated.stderr).toBe("");
expect(updated.status).toBe(0);
});
it("rejects marketplace state with the wrong persisted version", () => {
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
const { pluginId } = writeMarketplaceState(home, "0.0.1");
const result = runHelper(home, [
"assert-marketplace-state",
pluginId,
"0.0.2",
"release-fixtures",
pluginId,
path.join(home, "install-path.txt"),
]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("expected install record version 0.0.2, got 0.0.1");
});
it("seeds uninstall state and verifies complete cleanup with sentinels preserved", () => {
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
const { installPath, pluginId } = writeMarketplaceState(home, "0.0.2");
const sentinelPluginId = "release-marketplace-other";
const sentinelPath = path.join(home, "marketplace", sentinelPluginId);
const installPathFile = path.join(home, "install-path.txt");
fs.mkdirSync(sentinelPath, { recursive: true });
const seeded = runHelper(home, [
"seed-marketplace-uninstall-state",
pluginId,
sentinelPluginId,
sentinelPath,
installPathFile,
]);
expect(seeded.stderr).toBe("");
expect(seeded.status).toBe(0);
const configPath = path.join(home, ".openclaw", "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
expect(config.plugins.allow).toEqual([pluginId, sentinelPluginId]);
expect(config.plugins.deny).toEqual([pluginId, sentinelPluginId]);
expect(config.plugins.load.paths).toEqual([installPath, sentinelPath]);
delete config.plugins.entries[pluginId];
config.plugins.allow = [sentinelPluginId];
config.plugins.deny = [sentinelPluginId];
config.plugins.load.paths = [sentinelPath];
fs.writeFileSync(configPath, `${JSON.stringify(config)}\n`, "utf8");
fs.rmSync(installPath, { recursive: true });
clearMarketplaceIndex(home);
const verified = runHelper(home, [
"assert-marketplace-uninstalled",
pluginId,
sentinelPluginId,
sentinelPath,
installPathFile,
]);
expect(verified.stderr).toBe("");
expect(verified.status).toBe(0);
});
it("checks the exact dry-run and update outcome text", () => {
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
const logPath = path.join(home, "update.log");
fs.writeFileSync(logPath, "Would update release-marketplace-plugin: 0.0.1 -> 0.0.2.\n", "utf8");
const result = runHelper(home, [
"assert-update-log",
logPath,
"Would update release-marketplace-plugin: 0.0.1 -> 0.0.2.",
]);
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
});
});