mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 12:36:55 +02:00
761 lines
26 KiB
Bash
Executable File
761 lines
26 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
|
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugins-e2e" OPENCLAW_PLUGINS_E2E_IMAGE)"
|
|
|
|
docker_e2e_build_or_reuse "$IMAGE_NAME" plugins
|
|
|
|
DOCKER_ENV_ARGS=(-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0)
|
|
for env_name in \
|
|
OPENCLAW_PLUGINS_E2E_CLAWHUB \
|
|
OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC \
|
|
OPENCLAW_PLUGINS_E2E_CLAWHUB_ID \
|
|
OPENCLAW_CLAWHUB_URL \
|
|
CLAWHUB_URL \
|
|
OPENCLAW_CLAWHUB_TOKEN \
|
|
CLAWHUB_TOKEN \
|
|
CLAWHUB_AUTH_TOKEN; do
|
|
env_value="${!env_name:-}"
|
|
if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then
|
|
DOCKER_ENV_ARGS+=(-e "$env_name")
|
|
fi
|
|
done
|
|
|
|
echo "Running plugins Docker E2E..."
|
|
RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-plugins-run.XXXXXX")"
|
|
if ! docker run --rm "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash -s >"$RUN_LOG" 2>&1 <<'EOF'
|
|
set -euo pipefail
|
|
|
|
if [ -f dist/index.mjs ]; then
|
|
OPENCLAW_ENTRY="dist/index.mjs"
|
|
elif [ -f dist/index.js ]; then
|
|
OPENCLAW_ENTRY="dist/index.js"
|
|
else
|
|
echo "Missing dist/index.(m)js (build output):"
|
|
ls -la dist || true
|
|
exit 1
|
|
fi
|
|
export OPENCLAW_ENTRY
|
|
|
|
home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX")
|
|
export HOME="$home_dir"
|
|
BUNDLED_PLUGIN_ROOT_DIR="extensions"
|
|
OPENCLAW_PLUGIN_HOME="$HOME/.openclaw/$BUNDLED_PLUGIN_ROOT_DIR"
|
|
|
|
record_fixture_plugin_trust() {
|
|
local plugin_id="$1"
|
|
local plugin_root="$2"
|
|
local enabled="$3"
|
|
node - <<'NODE' "$plugin_id" "$plugin_root" "$enabled"
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const pluginId = process.argv[2];
|
|
const pluginRoot = process.argv[3];
|
|
const enabled = process.argv[4] === "1";
|
|
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
|
const config = fs.existsSync(configPath)
|
|
? JSON.parse(fs.readFileSync(configPath, "utf8"))
|
|
: {};
|
|
const plugins = (config.plugins ??= {});
|
|
const entries = (plugins.entries ??= {});
|
|
entries[pluginId] = { ...(entries[pluginId] ?? {}), enabled };
|
|
delete plugins.installs;
|
|
plugins.allow = Array.from(new Set([...(plugins.allow ?? []), pluginId])).sort();
|
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
|
|
const ledgerPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
|
const ledger = fs.existsSync(ledgerPath)
|
|
? JSON.parse(fs.readFileSync(ledgerPath, "utf8"))
|
|
: {
|
|
version: 1,
|
|
warning:
|
|
"DO NOT EDIT. This file is generated by OpenClaw plugin install/update/uninstall commands. Use `openclaw plugins install/update/uninstall` instead.",
|
|
records: {},
|
|
};
|
|
ledger.updatedAtMs = Date.now();
|
|
ledger.records ??= {};
|
|
ledger.records[pluginId] = {
|
|
...(ledger.records[pluginId] ?? {}),
|
|
source: "path",
|
|
installPath: pluginRoot,
|
|
sourcePath: pluginRoot,
|
|
};
|
|
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
|
|
NODE
|
|
}
|
|
|
|
run_logged() {
|
|
local label="$1"
|
|
shift
|
|
local log_file="/tmp/openclaw-plugins-e2e-${label}.log"
|
|
if ! "$@" >"$log_file" 2>&1; then
|
|
cat "$log_file"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
write_fixture_plugin() {
|
|
local dir="$1"
|
|
local id="$2"
|
|
local version="$3"
|
|
local method="$4"
|
|
local name="$5"
|
|
|
|
mkdir -p "$dir"
|
|
cat > "$dir/package.json" <<JSON
|
|
{
|
|
"name": "@openclaw/$id",
|
|
"version": "$version",
|
|
"openclaw": { "extensions": ["./index.js"] }
|
|
}
|
|
JSON
|
|
cat > "$dir/index.js" <<JS
|
|
module.exports = {
|
|
id: "$id",
|
|
name: "$name",
|
|
register(api) {
|
|
api.registerGatewayMethod("$method", async () => ({ ok: true }));
|
|
},
|
|
};
|
|
JS
|
|
cat > "$dir/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "placeholder",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
node - <<'NODE' "$dir/openclaw.plugin.json" "$id"
|
|
const fs = require("node:fs");
|
|
const file = process.argv[2];
|
|
const id = process.argv[3];
|
|
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
parsed.id = id;
|
|
fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
NODE
|
|
}
|
|
|
|
demo_plugin_id="demo-plugin"
|
|
demo_plugin_root="$OPENCLAW_PLUGIN_HOME/$demo_plugin_id"
|
|
mkdir -p "$demo_plugin_root"
|
|
|
|
cat > "$demo_plugin_root/index.js" <<'JS'
|
|
module.exports = {
|
|
id: "demo-plugin",
|
|
name: "Demo Plugin",
|
|
description: "Docker E2E demo plugin",
|
|
register(api) {
|
|
api.registerTool(() => null, { name: "demo_tool" });
|
|
api.registerGatewayMethod("demo.ping", async () => ({ ok: true }));
|
|
api.registerCli(() => {}, { commands: ["demo"] });
|
|
api.registerService({ id: "demo-service", start: () => {} });
|
|
},
|
|
};
|
|
JS
|
|
cat > "$demo_plugin_root/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "demo-plugin",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
record_fixture_plugin_trust "$demo_plugin_id" "$demo_plugin_root" 1
|
|
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin --json > /tmp/plugins-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins.json", "utf8"));
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-inspect.json", "utf8"));
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin");
|
|
if (!plugin) throw new Error("plugin not found");
|
|
if (plugin.status !== "loaded") {
|
|
throw new Error(`unexpected status: ${plugin.status}`);
|
|
}
|
|
|
|
const assertIncludes = (list, value, label) => {
|
|
if (!Array.isArray(list) || !list.includes(value)) {
|
|
throw new Error(`${label} missing: ${value}`);
|
|
}
|
|
};
|
|
|
|
const inspectToolNames = Array.isArray(inspect.tools)
|
|
? inspect.tools.flatMap((entry) => (Array.isArray(entry?.names) ? entry.names : []))
|
|
: [];
|
|
assertIncludes(inspectToolNames, "demo_tool", "tool");
|
|
assertIncludes(inspect.gatewayMethods, "demo.ping", "gateway method");
|
|
assertIncludes(inspect.cliCommands, "demo", "cli command");
|
|
assertIncludes(inspect.services, "demo-service", "service");
|
|
|
|
const diagErrors = (data.diagnostics || []).filter((diag) => diag.level === "error");
|
|
if (diagErrors.length > 0) {
|
|
throw new Error(`diagnostics errors: ${diagErrors.map((diag) => diag.message).join("; ")}`);
|
|
}
|
|
|
|
console.log("ok");
|
|
NODE
|
|
|
|
echo "Testing tgz install flow..."
|
|
pack_dir="$(mktemp -d "/tmp/openclaw-plugin-pack.XXXXXX")"
|
|
mkdir -p "$pack_dir/package"
|
|
cat > "$pack_dir/package/package.json" <<'JSON'
|
|
{
|
|
"name": "@openclaw/demo-plugin-tgz",
|
|
"version": "0.0.1",
|
|
"openclaw": { "extensions": ["./index.js"] }
|
|
}
|
|
JSON
|
|
cat > "$pack_dir/package/index.js" <<'JS'
|
|
module.exports = {
|
|
id: "demo-plugin-tgz",
|
|
name: "Demo Plugin TGZ",
|
|
register(api) {
|
|
api.registerGatewayMethod("demo.tgz", async () => ({ ok: true }));
|
|
},
|
|
};
|
|
JS
|
|
cat > "$pack_dir/package/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "demo-plugin-tgz",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
tar -czf /tmp/demo-plugin-tgz.tgz -C "$pack_dir" package
|
|
|
|
run_logged install-tgz node "$OPENCLAW_ENTRY" plugins install /tmp/demo-plugin-tgz.tgz
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins2.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-tgz --json > /tmp/plugins2-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins2.json", "utf8"));
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins2-inspect.json", "utf8"));
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin-tgz");
|
|
if (!plugin) throw new Error("tgz plugin not found");
|
|
if (plugin.status !== "loaded") {
|
|
throw new Error(`unexpected status: ${plugin.status}`);
|
|
}
|
|
if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes("demo.tgz")) {
|
|
throw new Error("expected gateway method demo.tgz");
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
echo "Testing install from local folder (plugins.load.paths)..."
|
|
dir_plugin="$(mktemp -d "/tmp/openclaw-plugin-dir.XXXXXX")"
|
|
cat > "$dir_plugin/package.json" <<'JSON'
|
|
{
|
|
"name": "@openclaw/demo-plugin-dir",
|
|
"version": "0.0.1",
|
|
"openclaw": { "extensions": ["./index.js"] }
|
|
}
|
|
JSON
|
|
cat > "$dir_plugin/index.js" <<'JS'
|
|
module.exports = {
|
|
id: "demo-plugin-dir",
|
|
name: "Demo Plugin DIR",
|
|
register(api) {
|
|
api.registerGatewayMethod("demo.dir", async () => ({ ok: true }));
|
|
},
|
|
};
|
|
JS
|
|
cat > "$dir_plugin/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "demo-plugin-dir",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
run_logged install-dir node "$OPENCLAW_ENTRY" plugins install "$dir_plugin"
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins3.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-dir --json > /tmp/plugins3-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins3.json", "utf8"));
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins3-inspect.json", "utf8"));
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin-dir");
|
|
if (!plugin) throw new Error("dir plugin not found");
|
|
if (plugin.status !== "loaded") {
|
|
throw new Error(`unexpected status: ${plugin.status}`);
|
|
}
|
|
if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes("demo.dir")) {
|
|
throw new Error("expected gateway method demo.dir");
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
echo "Testing install from npm spec (file:)..."
|
|
file_pack_dir="$(mktemp -d "/tmp/openclaw-plugin-filepack.XXXXXX")"
|
|
mkdir -p "$file_pack_dir/package"
|
|
cat > "$file_pack_dir/package/package.json" <<'JSON'
|
|
{
|
|
"name": "@openclaw/demo-plugin-file",
|
|
"version": "0.0.1",
|
|
"openclaw": { "extensions": ["./index.js"] }
|
|
}
|
|
JSON
|
|
cat > "$file_pack_dir/package/index.js" <<'JS'
|
|
module.exports = {
|
|
id: "demo-plugin-file",
|
|
name: "Demo Plugin FILE",
|
|
register(api) {
|
|
api.registerGatewayMethod("demo.file", async () => ({ ok: true }));
|
|
},
|
|
};
|
|
JS
|
|
cat > "$file_pack_dir/package/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "demo-plugin-file",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
run_logged install-file node "$OPENCLAW_ENTRY" plugins install "file:$file_pack_dir/package"
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins4.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect demo-plugin-file --json > /tmp/plugins4-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins4.json", "utf8"));
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins4-inspect.json", "utf8"));
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin-file");
|
|
if (!plugin) throw new Error("file plugin not found");
|
|
if (plugin.status !== "loaded") {
|
|
throw new Error(`unexpected status: ${plugin.status}`);
|
|
}
|
|
if (!Array.isArray(inspect.gatewayMethods) || !inspect.gatewayMethods.includes("demo.file")) {
|
|
throw new Error("expected gateway method demo.file");
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
echo "Testing Claude bundle enable and inspect flow..."
|
|
bundle_plugin_id="claude-bundle-e2e"
|
|
bundle_root="$OPENCLAW_PLUGIN_HOME/$bundle_plugin_id"
|
|
mkdir -p "$bundle_root/.claude-plugin" "$bundle_root/commands"
|
|
cat > "$bundle_root/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "claude-bundle-e2e"
|
|
}
|
|
JSON
|
|
cat > "$bundle_root/commands/office-hours.md" <<'MD'
|
|
---
|
|
description: Help with architecture and rollout planning
|
|
---
|
|
Act as an engineering advisor.
|
|
|
|
Focus on:
|
|
$ARGUMENTS
|
|
MD
|
|
record_fixture_plugin_trust "$bundle_plugin_id" "$bundle_root" 0
|
|
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-bundle-disabled.json
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins-bundle-disabled.json", "utf8"));
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === "claude-bundle-e2e");
|
|
if (!plugin) throw new Error("Claude bundle plugin not found");
|
|
if (plugin.status !== "disabled") {
|
|
throw new Error(`expected disabled bundle before enable, got ${plugin.status}`);
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
run_logged enable-claude-bundle node "$OPENCLAW_ENTRY" plugins enable claude-bundle-e2e
|
|
node "$OPENCLAW_ENTRY" plugins inspect claude-bundle-e2e --json > /tmp/plugins-bundle-inspect.json
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-bundle-inspect.json", "utf8"));
|
|
if (inspect.plugin?.bundleFormat !== "claude") {
|
|
throw new Error(`expected Claude bundle format, got ${inspect.plugin?.bundleFormat}`);
|
|
}
|
|
if (inspect.plugin?.enabled !== true || inspect.plugin?.status !== "loaded") {
|
|
throw new Error(
|
|
`expected enabled loaded Claude bundle, got enabled=${inspect.plugin?.enabled} status=${inspect.plugin?.status}`,
|
|
);
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
echo "Testing plugin install visible after explicit restart..."
|
|
slash_install_dir="$(mktemp -d "/tmp/openclaw-plugin-slash-install.XXXXXX")"
|
|
cat > "$slash_install_dir/package.json" <<'JSON'
|
|
{
|
|
"name": "@openclaw/slash-install-plugin",
|
|
"version": "0.0.1",
|
|
"openclaw": { "extensions": ["./index.js"] }
|
|
}
|
|
JSON
|
|
cat > "$slash_install_dir/index.js" <<'JS'
|
|
module.exports = {
|
|
id: "slash-install-plugin",
|
|
name: "Slash Install Plugin",
|
|
register(api) {
|
|
api.registerGatewayMethod("demo.slash.install", async () => ({ ok: true }));
|
|
},
|
|
};
|
|
JS
|
|
cat > "$slash_install_dir/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "slash-install-plugin",
|
|
"configSchema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
run_logged install-slash-plugin node "$OPENCLAW_ENTRY" plugins install "$slash_install_dir"
|
|
node "$OPENCLAW_ENTRY" plugins inspect slash-install-plugin --json > /tmp/plugin-command-install-show.json
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugin-command-install-show.json", "utf8"));
|
|
if (inspect.plugin?.status !== "loaded") {
|
|
throw new Error(`expected loaded status after install, got ${inspect.plugin?.status}`);
|
|
}
|
|
if (inspect.plugin?.enabled !== true) {
|
|
throw new Error(`expected enabled status after install, got ${inspect.plugin?.enabled}`);
|
|
}
|
|
if (!inspect.gatewayMethods.includes("demo.slash.install")) {
|
|
throw new Error(`expected installed gateway method, got ${inspect.gatewayMethods.join(", ")}`);
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
echo "Testing marketplace install and update flows..."
|
|
marketplace_root="$HOME/.claude/plugins/marketplaces/fixture-marketplace"
|
|
mkdir -p "$HOME/.claude/plugins" "$marketplace_root/.claude-plugin"
|
|
write_fixture_plugin \
|
|
"$marketplace_root/plugins/marketplace-shortcut" \
|
|
"marketplace-shortcut" \
|
|
"0.0.1" \
|
|
"demo.marketplace.shortcut.v1" \
|
|
"Marketplace Shortcut"
|
|
write_fixture_plugin \
|
|
"$marketplace_root/plugins/marketplace-direct" \
|
|
"marketplace-direct" \
|
|
"0.0.1" \
|
|
"demo.marketplace.direct.v1" \
|
|
"Marketplace Direct"
|
|
cat > "$marketplace_root/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "Fixture Marketplace",
|
|
"version": "1.0.0",
|
|
"plugins": [
|
|
{
|
|
"name": "marketplace-shortcut",
|
|
"version": "0.0.1",
|
|
"description": "Shortcut install fixture",
|
|
"source": "./plugins/marketplace-shortcut"
|
|
},
|
|
{
|
|
"name": "marketplace-direct",
|
|
"version": "0.0.1",
|
|
"description": "Explicit marketplace fixture",
|
|
"source": {
|
|
"type": "path",
|
|
"path": "./plugins/marketplace-direct"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$HOME/.claude/plugins/known_marketplaces.json" <<JSON
|
|
{
|
|
"claude-fixtures": {
|
|
"installLocation": "$marketplace_root",
|
|
"source": {
|
|
"type": "github",
|
|
"repo": "openclaw/fixture-marketplace"
|
|
}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
node "$OPENCLAW_ENTRY" plugins marketplace list claude-fixtures --json > /tmp/marketplace-list.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/marketplace-list.json", "utf8"));
|
|
const names = (data.plugins || []).map((entry) => entry.name).sort();
|
|
if (data.name !== "Fixture Marketplace") {
|
|
throw new Error(`unexpected marketplace name: ${data.name}`);
|
|
}
|
|
if (!names.includes("marketplace-shortcut") || !names.includes("marketplace-direct")) {
|
|
throw new Error(`unexpected marketplace plugins: ${names.join(", ")}`);
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
run_logged install-marketplace-shortcut node "$OPENCLAW_ENTRY" plugins install marketplace-shortcut@claude-fixtures
|
|
run_logged install-marketplace-direct node "$OPENCLAW_ENTRY" plugins install marketplace-direct --marketplace claude-fixtures
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-marketplace.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --json > /tmp/plugins-marketplace-shortcut-inspect.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect marketplace-direct --json > /tmp/plugins-marketplace-direct-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace.json", "utf8"));
|
|
const shortcutInspect = JSON.parse(
|
|
fs.readFileSync("/tmp/plugins-marketplace-shortcut-inspect.json", "utf8"),
|
|
);
|
|
const directInspect = JSON.parse(
|
|
fs.readFileSync("/tmp/plugins-marketplace-direct-inspect.json", "utf8"),
|
|
);
|
|
const getPlugin = (id) => {
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === id);
|
|
if (!plugin) throw new Error(`plugin not found: ${id}`);
|
|
if (plugin.status !== "loaded") {
|
|
throw new Error(`unexpected status for ${id}: ${plugin.status}`);
|
|
}
|
|
return plugin;
|
|
};
|
|
|
|
const shortcut = getPlugin("marketplace-shortcut");
|
|
const direct = getPlugin("marketplace-direct");
|
|
if (shortcut.version !== "0.0.1") {
|
|
throw new Error(`unexpected shortcut version: ${shortcut.version}`);
|
|
}
|
|
if (direct.version !== "0.0.1") {
|
|
throw new Error(`unexpected direct version: ${direct.version}`);
|
|
}
|
|
if (!shortcutInspect.gatewayMethods.includes("demo.marketplace.shortcut.v1")) {
|
|
throw new Error("expected marketplace shortcut gateway method");
|
|
}
|
|
if (!directInspect.gatewayMethods.includes("demo.marketplace.direct.v1")) {
|
|
throw new Error("expected marketplace direct gateway method");
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
|
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
|
for (const id of ["marketplace-shortcut", "marketplace-direct"]) {
|
|
const record = index.installRecords?.[id];
|
|
if (!record) throw new Error(`missing install record for ${id}`);
|
|
if (record.source !== "marketplace") {
|
|
throw new Error(`unexpected source for ${id}: ${record.source}`);
|
|
}
|
|
if (record.marketplaceSource !== "claude-fixtures") {
|
|
throw new Error(`unexpected marketplace source for ${id}: ${record.marketplaceSource}`);
|
|
}
|
|
if (record.marketplacePlugin !== id) {
|
|
throw new Error(`unexpected marketplace plugin for ${id}: ${record.marketplacePlugin}`);
|
|
}
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
write_fixture_plugin \
|
|
"$marketplace_root/plugins/marketplace-shortcut" \
|
|
"marketplace-shortcut" \
|
|
"0.0.2" \
|
|
"demo.marketplace.shortcut.v2" \
|
|
"Marketplace Shortcut"
|
|
run_logged update-marketplace-shortcut-dry-run node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut --dry-run
|
|
run_logged update-marketplace-shortcut node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-marketplace-updated.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect marketplace-shortcut --json > /tmp/plugins-marketplace-updated-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
|
|
const data = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace-updated.json", "utf8"));
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace-updated-inspect.json", "utf8"));
|
|
const plugin = (data.plugins || []).find((entry) => entry.id === "marketplace-shortcut");
|
|
if (!plugin) throw new Error("updated marketplace plugin not found");
|
|
if (plugin.version !== "0.0.2") {
|
|
throw new Error(`unexpected updated version: ${plugin.version}`);
|
|
}
|
|
if (!inspect.gatewayMethods.includes("demo.marketplace.shortcut.v2")) {
|
|
throw new Error(`expected updated gateway method, got ${inspect.gatewayMethods.join(", ")}`);
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
|
|
if [ "${OPENCLAW_PLUGINS_E2E_CLAWHUB:-1}" = "0" ]; then
|
|
echo "Skipping ClawHub plugin install and uninstall (OPENCLAW_PLUGINS_E2E_CLAWHUB=0)."
|
|
else
|
|
echo "Testing ClawHub plugin install and uninstall..."
|
|
CLAWHUB_PLUGIN_SPEC="${OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC:-clawhub:openclaw-now4real}"
|
|
CLAWHUB_PLUGIN_ID="${OPENCLAW_PLUGINS_E2E_CLAWHUB_ID:-now4real}"
|
|
export CLAWHUB_PLUGIN_SPEC CLAWHUB_PLUGIN_ID
|
|
|
|
node - <<'NODE'
|
|
const spec = process.env.CLAWHUB_PLUGIN_SPEC;
|
|
if (!spec?.startsWith("clawhub:")) {
|
|
throw new Error(`expected clawhub: spec, got ${spec}`);
|
|
}
|
|
|
|
const parsePackageName = (rawSpec) => {
|
|
const value = rawSpec.slice("clawhub:".length).trim();
|
|
const slashIndex = value.lastIndexOf("/");
|
|
const atIndex = value.lastIndexOf("@");
|
|
return atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value;
|
|
};
|
|
|
|
const packageName = parsePackageName(spec);
|
|
const baseUrl = (process.env.OPENCLAW_CLAWHUB_URL || process.env.CLAWHUB_URL || "https://clawhub.ai")
|
|
.replace(/\/+$/, "");
|
|
const token =
|
|
process.env.OPENCLAW_CLAWHUB_TOKEN ||
|
|
process.env.CLAWHUB_TOKEN ||
|
|
process.env.CLAWHUB_AUTH_TOKEN ||
|
|
"";
|
|
const response = await fetch(`${baseUrl}/api/v1/packages/${encodeURIComponent(packageName)}`, {
|
|
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.text().catch(() => "");
|
|
throw new Error(`ClawHub package preflight failed for ${packageName}: ${response.status} ${body}`);
|
|
}
|
|
const detail = await response.json();
|
|
const family = detail.package?.family;
|
|
if (family !== "code-plugin" && family !== "bundle-plugin") {
|
|
throw new Error(`ClawHub package ${packageName} is not installable as a plugin: ${family}`);
|
|
}
|
|
if (detail.package?.runtimeId && detail.package.runtimeId !== process.env.CLAWHUB_PLUGIN_ID) {
|
|
throw new Error(
|
|
`ClawHub package ${packageName} runtimeId ${detail.package.runtimeId} does not match expected ${process.env.CLAWHUB_PLUGIN_ID}`,
|
|
);
|
|
}
|
|
console.log(`Using ClawHub package ${packageName} (${family}).`);
|
|
NODE
|
|
|
|
run_logged install-clawhub node "$OPENCLAW_ENTRY" plugins install "$CLAWHUB_PLUGIN_SPEC"
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-clawhub-installed.json
|
|
node "$OPENCLAW_ENTRY" plugins inspect "$CLAWHUB_PLUGIN_ID" --json > /tmp/plugins-clawhub-inspect.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const pluginId = process.env.CLAWHUB_PLUGIN_ID;
|
|
const spec = process.env.CLAWHUB_PLUGIN_SPEC;
|
|
const parsePackageName = (rawSpec) => {
|
|
const value = rawSpec.slice("clawhub:".length).trim();
|
|
const slashIndex = value.lastIndexOf("/");
|
|
const atIndex = value.lastIndexOf("@");
|
|
return atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value;
|
|
};
|
|
const packageName = parsePackageName(spec);
|
|
const list = JSON.parse(fs.readFileSync("/tmp/plugins-clawhub-installed.json", "utf8"));
|
|
const inspect = JSON.parse(fs.readFileSync("/tmp/plugins-clawhub-inspect.json", "utf8"));
|
|
const plugin = (list.plugins || []).find((entry) => entry.id === pluginId);
|
|
if (!plugin) throw new Error(`ClawHub plugin not found after install: ${pluginId}`);
|
|
if (plugin.status !== "loaded") {
|
|
throw new Error(`unexpected ClawHub plugin status for ${pluginId}: ${plugin.status}`);
|
|
}
|
|
if (inspect.plugin?.id !== pluginId) {
|
|
throw new Error(`unexpected ClawHub inspect plugin id: ${inspect.plugin?.id}`);
|
|
}
|
|
|
|
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
|
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
|
const record = index.installRecords?.[pluginId];
|
|
if (!record) throw new Error(`missing ClawHub install record for ${pluginId}`);
|
|
if (record.source !== "clawhub") {
|
|
throw new Error(`unexpected ClawHub install source for ${pluginId}: ${record.source}`);
|
|
}
|
|
if (record.clawhubPackage !== packageName) {
|
|
throw new Error(
|
|
`unexpected ClawHub package for ${pluginId}: ${record.clawhubPackage}, expected ${packageName}`,
|
|
);
|
|
}
|
|
if (record.clawhubFamily !== "code-plugin" && record.clawhubFamily !== "bundle-plugin") {
|
|
throw new Error(`unexpected ClawHub family for ${pluginId}: ${record.clawhubFamily}`);
|
|
}
|
|
if (typeof record.installPath !== "string" || record.installPath.length === 0) {
|
|
throw new Error(`missing ClawHub install path for ${pluginId}`);
|
|
}
|
|
|
|
const installPath = record.installPath.replace(/^~(?=$|\/)/, process.env.HOME);
|
|
const extensionsRoot = path.join(process.env.HOME, ".openclaw", "extensions");
|
|
if (!installPath.startsWith(`${extensionsRoot}${path.sep}`)) {
|
|
throw new Error(`ClawHub install path is outside managed extensions root: ${installPath}`);
|
|
}
|
|
if (!fs.existsSync(installPath)) {
|
|
throw new Error(`ClawHub install path missing on disk: ${installPath}`);
|
|
}
|
|
fs.writeFileSync("/tmp/plugins-clawhub-install-path.txt", installPath, "utf8");
|
|
console.log("ok");
|
|
NODE
|
|
|
|
run_logged uninstall-clawhub node "$OPENCLAW_ENTRY" plugins uninstall "$CLAWHUB_PLUGIN_SPEC" --force
|
|
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-clawhub-uninstalled.json
|
|
|
|
node - <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const pluginId = process.env.CLAWHUB_PLUGIN_ID;
|
|
const installPath = fs.readFileSync("/tmp/plugins-clawhub-install-path.txt", "utf8").trim();
|
|
const list = JSON.parse(fs.readFileSync("/tmp/plugins-clawhub-uninstalled.json", "utf8"));
|
|
if ((list.plugins || []).some((entry) => entry.id === pluginId)) {
|
|
throw new Error(`ClawHub plugin still listed after uninstall: ${pluginId}`);
|
|
}
|
|
|
|
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
|
const index = fs.existsSync(indexPath) ? JSON.parse(fs.readFileSync(indexPath, "utf8")) : {};
|
|
if (index.installRecords?.[pluginId]) {
|
|
throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`);
|
|
}
|
|
|
|
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
|
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
|
if (config.plugins?.entries?.[pluginId]) {
|
|
throw new Error(`ClawHub config entry still present after uninstall: ${pluginId}`);
|
|
}
|
|
if ((config.plugins?.allow || []).includes(pluginId)) {
|
|
throw new Error(`ClawHub allowlist entry still present after uninstall: ${pluginId}`);
|
|
}
|
|
if ((config.plugins?.deny || []).includes(pluginId)) {
|
|
throw new Error(`ClawHub denylist entry still present after uninstall: ${pluginId}`);
|
|
}
|
|
if (fs.existsSync(installPath)) {
|
|
throw new Error(`ClawHub managed install directory still exists after uninstall: ${installPath}`);
|
|
}
|
|
console.log("ok");
|
|
NODE
|
|
fi
|
|
|
|
EOF
|
|
then
|
|
cat "$RUN_LOG"
|
|
rm -f "$RUN_LOG"
|
|
exit 1
|
|
fi
|
|
rm -f "$RUN_LOG"
|
|
|
|
echo "OK"
|