mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
test(plugins): add lifecycle matrix coverage
Add plugin lifecycle matrix Docker E2E coverage, resource metrics, fixture registry version support, and gauntlet handling for bundled plugin ids / required config.
This commit is contained in:
@@ -74,6 +74,7 @@ Use focused lanes while iterating:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:plugins
|
||||
pnpm test:docker:plugin-lifecycle-matrix
|
||||
pnpm test:docker:plugin-update
|
||||
pnpm test:docker:upgrade-survivor
|
||||
pnpm test:docker:published-upgrade-survivor
|
||||
@@ -89,6 +90,10 @@ Important lanes:
|
||||
dependencies, npm update no-ops, local ClawHub fixture installs and update
|
||||
no-ops, marketplace update behavior, and Claude-bundle enable/inspect. Set
|
||||
`OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to keep the ClawHub block hermetic/offline.
|
||||
- `test:docker:plugin-lifecycle-matrix` installs the candidate package in a bare
|
||||
container, runs an npm plugin through install, inspect, disable, enable,
|
||||
explicit upgrade, explicit downgrade, and uninstall after deleting the plugin
|
||||
code. It logs RSS and CPU metrics for each phase.
|
||||
- `test:docker:plugin-update` validates that an unchanged installed plugin does
|
||||
not reinstall or lose install metadata during `openclaw plugins update`.
|
||||
- `test:docker:upgrade-survivor` installs the candidate tarball over a dirty
|
||||
|
||||
@@ -615,7 +615,7 @@ These Docker runners split into two buckets:
|
||||
- `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. Profiles are ordered by breadth: `smoke`, `package`, `product`, and `full`. See [Testing updates and plugins](/help/testing-updates-plugins) for the package/update/plugin contract, published-upgrade survivor matrix, release defaults, and failure triage.
|
||||
- Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if pre-dispatch startup imports package dependencies such as Commander, prompt UI, undici, or logging before command dispatch; it also keeps the bundled gateway run chunk under budget and rejects static imports of known cold gateway paths. Packaged CLI smoke also covers root help, onboard help, doctor help, status, config schema, and a model-list command.
|
||||
- Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
|
||||
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
|
||||
|
||||
@@ -645,8 +645,9 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
|
||||
- Plugins (install/update smoke for local path, `file:`, npm registry with hoisted dependencies, git moving refs, ClawHub kitchen-sink, marketplace updates, and Claude-bundle enable/inspect): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
|
||||
Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the ClawHub block, or override the default kitchen-sink package/runtime pair with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`. Without `OPENCLAW_CLAWHUB_URL`/`CLAWHUB_URL`, the test uses a hermetic local ClawHub fixture server.
|
||||
- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`)
|
||||
- Plugin lifecycle matrix smoke: `pnpm test:docker:plugin-lifecycle-matrix` installs the packed OpenClaw tarball in a bare container, installs an npm plugin, toggles enable/disable, upgrades and downgrades it through a local npm registry, deletes the installed code, then verifies uninstall still removes stale state while logging RSS/CPU metrics for each lifecycle phase.
|
||||
- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`)
|
||||
- Plugins: `pnpm test:docker:plugins` covers install/update smoke for local path, `file:`, npm registry with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect. `pnpm test:docker:plugin-update` covers unchanged update behavior for installed plugins.
|
||||
- Plugins: `pnpm test:docker:plugins` covers install/update smoke for local path, `file:`, npm registry with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect. `pnpm test:docker:plugin-update` covers unchanged update behavior for installed plugins. `pnpm test:docker:plugin-lifecycle-matrix` covers resource-tracked npm plugin install, enable, disable, upgrade, downgrade, and missing-code uninstall.
|
||||
|
||||
To prebuild and reuse the shared functional image manually:
|
||||
|
||||
|
||||
@@ -1549,6 +1549,7 @@
|
||||
"test:docker:openai-web-search-minimal": "bash scripts/e2e/openai-web-search-minimal-docker.sh",
|
||||
"test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh",
|
||||
"test:docker:pi-bundle-mcp-tools": "bash scripts/e2e/pi-bundle-mcp-tools-docker.sh",
|
||||
"test:docker:plugin-lifecycle-matrix": "bash scripts/e2e/plugin-lifecycle-matrix-docker.sh",
|
||||
"test:docker:plugin-update": "bash scripts/e2e/plugin-update-unchanged-docker.sh",
|
||||
"test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
|
||||
"test:docker:published-upgrade-survivor": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest} bash scripts/e2e/upgrade-survivor-docker.sh",
|
||||
|
||||
@@ -389,11 +389,11 @@ function runPluginLifecycle(params) {
|
||||
const commands = [
|
||||
{
|
||||
phase: "install",
|
||||
args: ["install", plugin.dir, "--link", "--dangerously-force-unsafe-install"],
|
||||
args: ["install", plugin.id],
|
||||
},
|
||||
{ phase: "inspect", args: ["inspect", plugin.id, "--json"] },
|
||||
{ phase: "disable", args: ["disable", plugin.id] },
|
||||
{ phase: "enable", args: ["enable", plugin.id] },
|
||||
...(plugin.hasRequiredConfigFields ? [] : [{ phase: "enable", args: ["enable", plugin.id] }]),
|
||||
{ phase: "doctor", args: ["doctor"] },
|
||||
{ phase: "uninstall", args: ["uninstall", plugin.id, "--force"] },
|
||||
];
|
||||
|
||||
138
scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs
Normal file
138
scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const [summaryPath, phase, separator, command, ...args] = process.argv.slice(2);
|
||||
if (!summaryPath || !phase || separator !== "--" || !command) {
|
||||
console.error("usage: measure.mjs <summary.tsv> <phase> -- <command> [args...]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const pageSize = Number.parseInt(process.env.OPENCLAW_PROC_PAGE_SIZE || "4096", 10);
|
||||
const clockTicks = Number.parseInt(process.env.OPENCLAW_PROC_CLK_TCK || "100", 10);
|
||||
const pollMs = Number.parseInt(process.env.OPENCLAW_PLUGIN_LIFECYCLE_METRIC_POLL_MS || "100", 10);
|
||||
|
||||
if (!fs.existsSync("/proc")) {
|
||||
console.error("plugin lifecycle resource sampler requires Linux /proc");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function readProcSnapshot() {
|
||||
const stats = new Map();
|
||||
for (const entry of fs.readdirSync("/proc", { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
const pid = Number.parseInt(entry.name, 10);
|
||||
const statPath = path.join("/proc", entry.name, "stat");
|
||||
try {
|
||||
const raw = fs.readFileSync(statPath, "utf8");
|
||||
const closeParen = raw.lastIndexOf(")");
|
||||
if (closeParen === -1) {
|
||||
continue;
|
||||
}
|
||||
const fields = raw
|
||||
.slice(closeParen + 2)
|
||||
.trim()
|
||||
.split(/\s+/u);
|
||||
const ppid = Number.parseInt(fields[1] ?? "", 10);
|
||||
const userTicks = Number.parseInt(fields[11] ?? "", 10);
|
||||
const systemTicks = Number.parseInt(fields[12] ?? "", 10);
|
||||
const rssPages = Number.parseInt(fields[21] ?? "", 10);
|
||||
if (
|
||||
!Number.isFinite(ppid) ||
|
||||
!Number.isFinite(userTicks) ||
|
||||
!Number.isFinite(systemTicks) ||
|
||||
!Number.isFinite(rssPages)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
stats.set(pid, {
|
||||
ppid,
|
||||
cpuTicks: userTicks + systemTicks,
|
||||
rssBytes: Math.max(0, rssPages) * pageSize,
|
||||
});
|
||||
} catch {
|
||||
// Processes can exit while /proc is being scanned.
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
function descendantsOf(rootPid, stats) {
|
||||
const children = new Map();
|
||||
for (const [pid, stat] of stats.entries()) {
|
||||
const siblings = children.get(stat.ppid) ?? [];
|
||||
siblings.push(pid);
|
||||
children.set(stat.ppid, siblings);
|
||||
}
|
||||
const seen = new Set([rootPid]);
|
||||
const queue = [rootPid];
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
for (const child of children.get(queue[index]) ?? []) {
|
||||
if (!seen.has(child)) {
|
||||
seen.add(child);
|
||||
queue.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
function sample(rootPid) {
|
||||
const stats = readProcSnapshot();
|
||||
const pids = descendantsOf(rootPid, stats);
|
||||
let rssBytes = 0;
|
||||
let cpuTicks = 0;
|
||||
for (const pid of pids) {
|
||||
const stat = stats.get(pid);
|
||||
if (!stat) {
|
||||
continue;
|
||||
}
|
||||
rssBytes += stat.rssBytes;
|
||||
cpuTicks += stat.cpuTicks;
|
||||
}
|
||||
return { rssBytes, cpuTicks };
|
||||
}
|
||||
|
||||
const started = performance.now();
|
||||
const child = spawn(command, args, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
let maxRssBytes = 0;
|
||||
let maxCpuTicks = 0;
|
||||
const updateMetrics = () => {
|
||||
if (!child.pid) {
|
||||
return;
|
||||
}
|
||||
const current = sample(child.pid);
|
||||
maxRssBytes = Math.max(maxRssBytes, current.rssBytes);
|
||||
maxCpuTicks = Math.max(maxCpuTicks, current.cpuTicks);
|
||||
};
|
||||
|
||||
updateMetrics();
|
||||
const interval = setInterval(updateMetrics, pollMs);
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
updateMetrics();
|
||||
clearInterval(interval);
|
||||
const wallMs = performance.now() - started;
|
||||
const cpuSeconds = maxCpuTicks / clockTicks;
|
||||
const maxRssKb = Math.round(maxRssBytes / 1024);
|
||||
const cpuCoreRatio = wallMs > 0 ? cpuSeconds / (wallMs / 1000) : 0;
|
||||
fs.appendFileSync(
|
||||
summaryPath,
|
||||
`${phase}\t${maxRssKb}\t${cpuSeconds.toFixed(3)}\t${wallMs.toFixed(0)}\t${cpuCoreRatio.toFixed(3)}\t${signal ?? ""}\n`,
|
||||
);
|
||||
console.log(
|
||||
`plugin lifecycle resource: phase=${phase} max_rss_kb=${maxRssKb} cpu_s=${cpuSeconds.toFixed(3)} wall_ms=${wallMs.toFixed(0)} cpu_core_ratio=${cpuCoreRatio.toFixed(3)}`,
|
||||
);
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
96
scripts/e2e/lib/plugin-lifecycle-matrix/probe.mjs
Normal file
96
scripts/e2e/lib/plugin-lifecycle-matrix/probe.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const home = os.homedir();
|
||||
|
||||
function openclawPath(...parts) {
|
||||
return path.join(home, ".openclaw", ...parts);
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function records() {
|
||||
const index = readJson(openclawPath("plugins", "installs.json"));
|
||||
return index.installRecords ?? index.records ?? {};
|
||||
}
|
||||
|
||||
function recordFor(pluginId) {
|
||||
return records()[pluginId];
|
||||
}
|
||||
|
||||
function config() {
|
||||
return readJson(process.env.OPENCLAW_CONFIG_PATH ?? openclawPath("openclaw.json"));
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function assertVersion(pluginId, version) {
|
||||
const record = recordFor(pluginId);
|
||||
assert(record, `install record missing for ${pluginId}`);
|
||||
assert(record.source === "npm", `expected npm source for ${pluginId}, got ${record.source}`);
|
||||
assert(
|
||||
record.resolvedVersion === version || record.version === version,
|
||||
`expected ${pluginId} record version ${version}, got ${JSON.stringify(record)}`,
|
||||
);
|
||||
assert(record.installPath, `install path missing for ${pluginId}`);
|
||||
const packageJson = readJson(path.join(record.installPath, "package.json"));
|
||||
assert(
|
||||
packageJson.version === version,
|
||||
`expected installed package version ${version}, got ${packageJson.version}`,
|
||||
);
|
||||
}
|
||||
|
||||
function assertEnabled(pluginId, expectedRaw) {
|
||||
const expected = expectedRaw === "true";
|
||||
const entry = config().plugins?.entries?.[pluginId];
|
||||
assert(entry?.enabled === expected, `expected ${pluginId} enabled=${expected}`);
|
||||
}
|
||||
|
||||
function printInstallPath(pluginId) {
|
||||
const record = recordFor(pluginId);
|
||||
assert(record?.installPath, `install path missing for ${pluginId}`);
|
||||
process.stdout.write(record.installPath);
|
||||
}
|
||||
|
||||
function assertUninstalled(pluginId) {
|
||||
const cfg = config();
|
||||
const record = recordFor(pluginId);
|
||||
assert(!record, `install record still present for ${pluginId}`);
|
||||
assert(!cfg.plugins?.entries?.[pluginId], `plugin config entry still present for ${pluginId}`);
|
||||
assert(!(cfg.plugins?.allow ?? []).includes(pluginId), `allowlist still contains ${pluginId}`);
|
||||
assert(!(cfg.plugins?.deny ?? []).includes(pluginId), `denylist still contains ${pluginId}`);
|
||||
const loadPaths = cfg.plugins?.load?.paths ?? [];
|
||||
assert(
|
||||
!loadPaths.some((entry) => String(entry).includes(pluginId)),
|
||||
`load path still references ${pluginId}: ${loadPaths.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const [command, pluginId, arg] = process.argv.slice(2);
|
||||
switch (command) {
|
||||
case "assert-version":
|
||||
assertVersion(pluginId, arg);
|
||||
break;
|
||||
case "assert-enabled":
|
||||
assertEnabled(pluginId, arg);
|
||||
break;
|
||||
case "install-path":
|
||||
printInstallPath(pluginId);
|
||||
break;
|
||||
case "assert-uninstalled":
|
||||
assertUninstalled(pluginId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown plugin lifecycle matrix probe command: ${command ?? "<missing>"}`);
|
||||
}
|
||||
70
scripts/e2e/lib/plugin-lifecycle-matrix/sweep.sh
Normal file
70
scripts/e2e/lib/plugin-lifecycle-matrix/sweep.sh
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
|
||||
openclaw_e2e_install_package /tmp/openclaw-plugin-lifecycle-install.log "mounted OpenClaw package" /tmp/npm-prefix
|
||||
|
||||
package_root="$(openclaw_e2e_package_root /tmp/npm-prefix)"
|
||||
entry="$(openclaw_e2e_package_entrypoint "$package_root")"
|
||||
export PATH="/tmp/npm-prefix/bin:$PATH"
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
source scripts/e2e/lib/plugins/fixtures.sh
|
||||
|
||||
plugin_id="lifecycle-claw"
|
||||
package_name="@openclaw/lifecycle-claw"
|
||||
probe="scripts/e2e/lib/plugin-lifecycle-matrix/probe.mjs"
|
||||
measure="scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs"
|
||||
resource_dir="/tmp/openclaw-plugin-lifecycle-matrix"
|
||||
mkdir -p "$resource_dir"
|
||||
summary_tsv="$resource_dir/resource-summary.tsv"
|
||||
printf "phase\tmax_rss_kb\tcpu_seconds\twall_ms\tcpu_core_ratio\tsignal\n" >"$summary_tsv"
|
||||
|
||||
run_measured() {
|
||||
local phase="$1"
|
||||
shift
|
||||
|
||||
echo "Running plugin lifecycle phase: $phase"
|
||||
node "$measure" "$summary_tsv" "$phase" -- "$@"
|
||||
}
|
||||
|
||||
pack_root="$(mktemp -d "/tmp/openclaw-plugin-lifecycle-pack.XXXXXX")"
|
||||
registry_root="$(mktemp -d "/tmp/openclaw-plugin-lifecycle-registry.XXXXXX")"
|
||||
pack_fixture_plugin "$pack_root/v1" /tmp/lifecycle-claw-1.0.0.tgz "$plugin_id" 1.0.0 lifecycle.v1 "Lifecycle Claw"
|
||||
pack_fixture_plugin "$pack_root/v2" /tmp/lifecycle-claw-2.0.0.tgz "$plugin_id" 2.0.0 lifecycle.v2 "Lifecycle Claw"
|
||||
start_npm_fixture_registry "$package_name" 1.0.0 /tmp/lifecycle-claw-1.0.0.tgz "$registry_root" "$package_name" 2.0.0 /tmp/lifecycle-claw-2.0.0.tgz
|
||||
|
||||
run_measured install-v1 node "$entry" plugins install "npm:$package_name@1.0.0"
|
||||
node "$probe" assert-version "$plugin_id" 1.0.0
|
||||
|
||||
run_measured inspect-v1 bash -c 'node "$1" plugins inspect "$2" --runtime --json >/tmp/plugin-lifecycle-inspect-v1.json' bash "$entry" "$plugin_id"
|
||||
|
||||
run_measured disable node "$entry" plugins disable "$plugin_id"
|
||||
node "$probe" assert-enabled "$plugin_id" false
|
||||
|
||||
run_measured enable node "$entry" plugins enable "$plugin_id"
|
||||
node "$probe" assert-enabled "$plugin_id" true
|
||||
|
||||
run_measured upgrade-v2 node "$entry" plugins update "$package_name@2.0.0"
|
||||
node "$probe" assert-version "$plugin_id" 2.0.0
|
||||
|
||||
run_measured downgrade-v1 node "$entry" plugins update "$package_name@1.0.0"
|
||||
node "$probe" assert-version "$plugin_id" 1.0.0
|
||||
|
||||
install_path="$(node "$probe" install-path "$plugin_id")"
|
||||
rm -rf "$install_path"
|
||||
if [[ -e "$install_path" ]]; then
|
||||
echo "Failed to remove plugin code before missing-code uninstall: $install_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_measured missing-code-uninstall node "$entry" plugins uninstall "$plugin_id" --force
|
||||
node "$probe" assert-uninstalled "$plugin_id"
|
||||
|
||||
echo "Plugin lifecycle resource summary:"
|
||||
cat "$summary_tsv"
|
||||
echo "Plugin lifecycle matrix passed."
|
||||
@@ -18,33 +18,42 @@ for (let index = 0; index < packageArgs.length; index += 3) {
|
||||
const version = packageArgs[index + 1];
|
||||
const tarballPath = packageArgs[index + 2];
|
||||
const archive = fs.readFileSync(tarballPath);
|
||||
packages.set(packageName, {
|
||||
const existing = packages.get(packageName) ?? {
|
||||
encodedPackageName: encodeURIComponent(packageName).replace("%40", "@"),
|
||||
packageName,
|
||||
latestVersion: version,
|
||||
versions: new Map(),
|
||||
};
|
||||
existing.latestVersion = version;
|
||||
existing.versions.set(version, {
|
||||
archive,
|
||||
dependencies: packageName === "@openclaw/demo-plugin-npm" ? { "is-number": "7.0.0" } : {},
|
||||
encodedPackageName: encodeURIComponent(packageName).replace("%40", "@"),
|
||||
integrity: `sha512-${crypto.createHash("sha512").update(archive).digest("base64")}`,
|
||||
packageName,
|
||||
shasum: crypto.createHash("sha1").update(archive).digest("hex"),
|
||||
tarballName: path.basename(tarballPath),
|
||||
version,
|
||||
});
|
||||
packages.set(packageName, existing);
|
||||
}
|
||||
|
||||
const metadataFor = (entry, baseUrl) => ({
|
||||
name: entry.packageName,
|
||||
"dist-tags": { latest: entry.version },
|
||||
versions: {
|
||||
[entry.version]: {
|
||||
dependencies: entry.dependencies,
|
||||
name: entry.packageName,
|
||||
version: entry.version,
|
||||
dist: {
|
||||
integrity: entry.integrity,
|
||||
shasum: entry.shasum,
|
||||
tarball: `${baseUrl}/${entry.encodedPackageName}/-/${entry.tarballName}`,
|
||||
"dist-tags": { latest: entry.latestVersion },
|
||||
versions: Object.fromEntries(
|
||||
[...entry.versions.entries()].map(([version, versionEntry]) => [
|
||||
version,
|
||||
{
|
||||
dependencies: versionEntry.dependencies,
|
||||
name: entry.packageName,
|
||||
version,
|
||||
dist: {
|
||||
integrity: versionEntry.integrity,
|
||||
shasum: versionEntry.shasum,
|
||||
tarball: `${baseUrl}/${entry.encodedPackageName}/-/${versionEntry.tarballName}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
function findPackageForPath(pathname) {
|
||||
@@ -54,11 +63,13 @@ function findPackageForPath(pathname) {
|
||||
function findTarballForPath(pathname) {
|
||||
for (const entry of packages.values()) {
|
||||
const prefix = `/${entry.encodedPackageName}/-/`;
|
||||
if (
|
||||
pathname.toLowerCase().startsWith(prefix.toLowerCase()) &&
|
||||
pathname.endsWith(`/${entry.tarballName}`)
|
||||
) {
|
||||
return entry;
|
||||
if (!pathname.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
for (const versionEntry of entry.versions.values()) {
|
||||
if (pathname.endsWith(`/${versionEntry.tarballName}`)) {
|
||||
return versionEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
27
scripts/e2e/plugin-lifecycle-matrix-docker.sh
Normal file
27
scripts/e2e/plugin-lifecycle-matrix-docker.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bare package-level plugin lifecycle matrix with resource metrics.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugin-lifecycle-matrix-e2e" OPENCLAW_PLUGIN_LIFECYCLE_MATRIX_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_PLUGIN_LIFECYCLE_MATRIX_E2E_SKIP_BUILD:-0}"
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz plugin-lifecycle-matrix "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-lifecycle-matrix "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 plugin-lifecycle-matrix empty)"
|
||||
|
||||
echo "Running plugin lifecycle matrix Docker E2E..."
|
||||
docker_e2e_run_with_harness \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENCLAW_SKIP_CHANNELS=1 \
|
||||
-e OPENCLAW_SKIP_PROVIDERS=1 \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
bash scripts/e2e/lib/plugin-lifecycle-matrix/sweep.sh
|
||||
|
||||
echo "Plugin lifecycle matrix Docker E2E passed."
|
||||
@@ -255,6 +255,14 @@ export const mainLanes = [
|
||||
npmLane("plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update", {
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
npmLane(
|
||||
"plugin-lifecycle-matrix",
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-lifecycle-matrix",
|
||||
{
|
||||
stateScenario: "empty",
|
||||
timeoutMs: 12 * 60 * 1000,
|
||||
},
|
||||
),
|
||||
serviceLane("config-reload", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:config-reload", {
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user