mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
1d446ad2cc
* test(qa): prove marketplace plugin updates * fix(plugins): clean exact install load paths on uninstall * fix(ci): register marketplace lifecycle assertions
241 lines
7.4 KiB
TypeScript
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);
|
|
});
|
|
});
|