mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
refactor: simplify docker e2e helpers
This commit is contained in:
@@ -9,17 +9,11 @@ SKIP_BUILD="${OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD:-0}"
|
||||
DOCKER_COMMAND_TIMEOUT="${OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_DOCKER_COMMAND_TIMEOUT:-300s}"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 agents-delete-shared-workspace empty)"
|
||||
|
||||
docker_cmd() {
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$DOCKER_COMMAND_TIMEOUT" "$@"
|
||||
return
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" agents-delete-shared-workspace "$ROOT_DIR/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
|
||||
docker_e2e_harness_mount_args
|
||||
|
||||
run_logged agents-delete-shared-workspace docker_cmd docker run --rm \
|
||||
run_logged agents-delete-shared-workspace docker_e2e_docker_cmd run --rm \
|
||||
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
|
||||
--entrypoint bash \
|
||||
-e OPENCLAW_SKIP_CHANNELS=1 \
|
||||
-e OPENCLAW_SKIP_PROVIDERS=1 \
|
||||
@@ -33,6 +27,7 @@ run_logged agents-delete-shared-workspace docker_cmd docker run --rm \
|
||||
"$IMAGE_NAME" \
|
||||
-lc '
|
||||
set -euo pipefail
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
|
||||
run_openclaw() {
|
||||
if command -v openclaw >/dev/null 2>&1; then
|
||||
@@ -47,7 +42,7 @@ run_openclaw() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
|
||||
export SHARED_WORKSPACE="$HOME/workspace-shared"
|
||||
output_file="$HOME/delete.json"
|
||||
trap '\''rm -rf "$HOME"'\'' EXIT
|
||||
|
||||
@@ -14,18 +14,14 @@ TOKEN="browser-cdp-e2e-token"
|
||||
CONTAINER_NAME="openclaw-browser-cdp-e2e-$$"
|
||||
DOCKER_COMMAND_TIMEOUT="${OPENCLAW_BROWSER_CDP_SNAPSHOT_DOCKER_COMMAND_TIMEOUT:-900s}"
|
||||
|
||||
docker_cmd() {
|
||||
timeout "$DOCKER_COMMAND_TIMEOUT" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
docker_cmd docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" = "1" ] || [ "$SKIP_BUILD" = "1" ]; then
|
||||
echo "Reusing Docker image: $IMAGE_NAME"
|
||||
docker_cmd docker image inspect "$IMAGE_NAME" >/dev/null
|
||||
docker_e2e_docker_cmd image inspect "$IMAGE_NAME" >/dev/null
|
||||
else
|
||||
docker_e2e_build_or_reuse "$BASE_IMAGE" browser-cdp-base "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "0"
|
||||
build_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-browser-cdp-build.XXXXXX")"
|
||||
@@ -45,7 +41,7 @@ OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 browser-cdp-sn
|
||||
|
||||
echo "Starting browser CDP snapshot container..."
|
||||
docker_e2e_harness_mount_args
|
||||
docker_cmd docker run -d \
|
||||
docker_e2e_docker_cmd run -d \
|
||||
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
@@ -95,26 +91,7 @@ chromium --headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage \\
|
||||
--remote-debugging-port=$CDP_PORT \\
|
||||
--user-data-dir=/tmp/openclaw-browser-cdp/chrome \\
|
||||
about:blank >/tmp/browser-cdp-chromium.log 2>&1 &
|
||||
node --input-type=module - <<'NODE' >/tmp/browser-cdp-fixture.log 2>&1 &
|
||||
import http from 'node:http';
|
||||
const html = \`<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<main>
|
||||
<button>Save</button>
|
||||
<a href=\"https://docs.openclaw.ai/browser-cdp-live\">Docs</a>
|
||||
<div id=\"card\" onclick=\"window.__clicked = true\" style=\"cursor: pointer\">Clickable Card</div>
|
||||
<iframe title=\"Child\" srcdoc='<button>Inside</button>'></iframe>
|
||||
</main>
|
||||
</body>
|
||||
</html>\`;
|
||||
http
|
||||
.createServer((_req, res) => {
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
})
|
||||
.listen($FIXTURE_PORT, '127.0.0.1');
|
||||
NODE
|
||||
FIXTURE_PORT=$FIXTURE_PORT node scripts/e2e/lib/browser-cdp-snapshot/fixture-server.mjs >/tmp/browser-cdp-fixture.log 2>&1 &
|
||||
openclaw_e2e_exec_gateway \"\$entry\" $PORT loopback /tmp/browser-cdp-gateway.log" >/dev/null
|
||||
|
||||
echo "Waiting for Chromium and Gateway..."
|
||||
@@ -129,7 +106,7 @@ if ! docker_e2e_wait_container_bash "$CONTAINER_NAME" 180 0.5 "
|
||||
fi
|
||||
|
||||
echo "Running browser CDP snapshot smoke..."
|
||||
docker_cmd docker exec "$CONTAINER_NAME" bash -lc "
|
||||
docker_e2e_docker_cmd exec "$CONTAINER_NAME" bash -lc "
|
||||
set -euo pipefail
|
||||
source /tmp/openclaw-test-state-env
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
@@ -139,25 +116,7 @@ node \"\$entry\" browser \"\${base_args[@]}\" --browser-profile docker-cdp docto
|
||||
grep -q 'OK live-snapshot' /tmp/browser-cdp-doctor.txt
|
||||
node \"\$entry\" browser \"\${base_args[@]}\" --browser-profile docker-cdp open http://127.0.0.1:$FIXTURE_PORT/ >/tmp/browser-cdp-open.txt
|
||||
node \"\$entry\" browser \"\${base_args[@]}\" --browser-profile docker-cdp snapshot --interactive --urls --out /tmp/browser-cdp-snapshot.txt >/tmp/browser-cdp-snapshot.out
|
||||
node --input-type=module - <<'NODE'
|
||||
import fs from 'node:fs';
|
||||
const snapshot = fs.readFileSync('/tmp/browser-cdp-snapshot.txt', 'utf8');
|
||||
for (const needle of [
|
||||
'button \"Save\"',
|
||||
'link \"Docs\"',
|
||||
'https://docs.openclaw.ai/browser-cdp-live',
|
||||
'generic \"Clickable Card\"',
|
||||
'cursor:pointer',
|
||||
'Iframe \"Child\"',
|
||||
'button \"Inside\"',
|
||||
]) {
|
||||
if (!snapshot.includes(needle)) {
|
||||
console.error(snapshot);
|
||||
throw new Error('missing snapshot needle: ' + needle);
|
||||
}
|
||||
}
|
||||
console.log('ok');
|
||||
NODE
|
||||
node scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs /tmp/browser-cdp-snapshot.txt
|
||||
"
|
||||
|
||||
echo "Browser CDP snapshot Docker E2E passed."
|
||||
|
||||
@@ -26,7 +26,8 @@ docker_e2e_run_with_harness \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "set -euo pipefail
|
||||
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"
|
||||
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}\"
|
||||
tsx scripts/e2e/crestodian-first-run-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
|
||||
@@ -26,7 +26,8 @@ docker_e2e_run_with_harness \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "set -euo pipefail
|
||||
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"
|
||||
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}\"
|
||||
tsx scripts/e2e/crestodian-planner-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
|
||||
@@ -26,7 +26,8 @@ docker_e2e_run_with_harness \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "set -euo pipefail
|
||||
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"
|
||||
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}\"
|
||||
tsx scripts/e2e/crestodian-rescue-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
|
||||
@@ -13,24 +13,20 @@ GW_NAME="openclaw-gateway-e2e-$$"
|
||||
DOCKER_COMMAND_TIMEOUT="${OPENCLAW_GATEWAY_NETWORK_DOCKER_COMMAND_TIMEOUT:-600s}"
|
||||
CLIENT_TIMEOUT="${OPENCLAW_GATEWAY_NETWORK_CLIENT_TIMEOUT:-90s}"
|
||||
|
||||
docker_cmd() {
|
||||
timeout "$DOCKER_COMMAND_TIMEOUT" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
docker_cmd docker rm -f "$GW_NAME" >/dev/null 2>&1 || true
|
||||
docker_cmd docker network rm "$NET_NAME" >/dev/null 2>&1 || true
|
||||
docker_e2e_docker_cmd rm -f "$GW_NAME" >/dev/null 2>&1 || true
|
||||
docker_e2e_docker_cmd network rm "$NET_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" gateway-network "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
|
||||
|
||||
echo "Creating Docker network..."
|
||||
docker_cmd docker network create "$NET_NAME" >/dev/null
|
||||
docker_e2e_docker_cmd network create "$NET_NAME" >/dev/null
|
||||
|
||||
echo "Starting gateway container..."
|
||||
docker_e2e_harness_mount_args
|
||||
docker_cmd docker run -d \
|
||||
docker_e2e_docker_cmd run -d \
|
||||
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
|
||||
--name "$GW_NAME" \
|
||||
--network "$NET_NAME" \
|
||||
@@ -51,67 +47,11 @@ fi
|
||||
|
||||
echo "Running client container (connect + health)..."
|
||||
run_logged gateway-network-client timeout "$CLIENT_TIMEOUT" docker run --rm \
|
||||
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
|
||||
--network "$NET_NAME" \
|
||||
-e "GW_URL=ws://$GW_NAME:$PORT" \
|
||||
-e "GW_TOKEN=$TOKEN" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "node --input-type=module - <<'NODE'
|
||||
import { WebSocket } from \"ws\";
|
||||
|
||||
const PROTOCOL_VERSION = 3;
|
||||
|
||||
const url = process.env.GW_URL;
|
||||
const token = process.env.GW_TOKEN;
|
||||
if (!url || !token) throw new Error(\"missing GW_URL/GW_TOKEN\");
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
await new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error(\"ws open timeout\")), 30000);
|
||||
ws.once(\"open\", () => {
|
||||
clearTimeout(t);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
function onceFrame(filter, timeoutMs = 30000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error(\"timeout\")), timeoutMs);
|
||||
const handler = (data) => {
|
||||
const obj = JSON.parse(String(data));
|
||||
if (!filter(obj)) return;
|
||||
clearTimeout(t);
|
||||
ws.off(\"message\", handler);
|
||||
resolve(obj);
|
||||
};
|
||||
ws.on(\"message\", handler);
|
||||
});
|
||||
}
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: \"req\",
|
||||
id: \"c1\",
|
||||
method: \"connect\",
|
||||
params: {
|
||||
minProtocol: PROTOCOL_VERSION,
|
||||
maxProtocol: PROTOCOL_VERSION,
|
||||
client: {
|
||||
id: \"test\",
|
||||
displayName: \"docker-net-e2e\",
|
||||
version: \"dev\",
|
||||
platform: process.platform,
|
||||
mode: \"test\",
|
||||
},
|
||||
caps: [],
|
||||
auth: { token },
|
||||
},
|
||||
}),
|
||||
);
|
||||
const connectRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"c1\");
|
||||
if (!connectRes.ok) throw new Error(\"connect failed: \" + (connectRes.error?.message ?? \"unknown\"));
|
||||
|
||||
ws.close();
|
||||
console.log(\"ok\");
|
||||
NODE"
|
||||
node scripts/e2e/lib/gateway-network/client.mjs
|
||||
|
||||
echo "OK"
|
||||
|
||||
21
scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs
Normal file
21
scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
const snapshotPath = process.argv[2] ?? "/tmp/browser-cdp-snapshot.txt";
|
||||
const snapshot = fs.readFileSync(snapshotPath, "utf8");
|
||||
|
||||
for (const needle of [
|
||||
'button "Save"',
|
||||
'link "Docs"',
|
||||
"https://docs.openclaw.ai/browser-cdp-live",
|
||||
'generic "Clickable Card"',
|
||||
"cursor:pointer",
|
||||
'Iframe "Child"',
|
||||
'button "Inside"',
|
||||
]) {
|
||||
if (!snapshot.includes(needle)) {
|
||||
console.error(snapshot);
|
||||
throw new Error(`missing snapshot needle: ${needle}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("ok");
|
||||
25
scripts/e2e/lib/browser-cdp-snapshot/fixture-server.mjs
Normal file
25
scripts/e2e/lib/browser-cdp-snapshot/fixture-server.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import http from "node:http";
|
||||
|
||||
const port = Number(process.env.FIXTURE_PORT);
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
throw new Error(`invalid FIXTURE_PORT: ${process.env.FIXTURE_PORT ?? "unset"}`);
|
||||
}
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<main>
|
||||
<button>Save</button>
|
||||
<a href="https://docs.openclaw.ai/browser-cdp-live">Docs</a>
|
||||
<div id="card" onclick="window.__clicked = true" style="cursor: pointer">Clickable Card</div>
|
||||
<iframe title="Child" srcdoc='<button>Inside</button>'></iframe>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
http
|
||||
.createServer((_req, res) => {
|
||||
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
res.end(html);
|
||||
})
|
||||
.listen(port, "127.0.0.1");
|
||||
63
scripts/e2e/lib/gateway-network/client.mjs
Normal file
63
scripts/e2e/lib/gateway-network/client.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
const PROTOCOL_VERSION = 3;
|
||||
|
||||
const url = process.env.GW_URL;
|
||||
const token = process.env.GW_TOKEN;
|
||||
if (!url || !token) {
|
||||
throw new Error("missing GW_URL/GW_TOKEN");
|
||||
}
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("ws open timeout")), 30_000);
|
||||
ws.once("open", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
function onceFrame(filter, timeoutMs = 30_000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
||||
const handler = (data) => {
|
||||
const obj = JSON.parse(String(data));
|
||||
if (!filter(obj)) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
ws.off("message", handler);
|
||||
resolve(obj);
|
||||
};
|
||||
ws.on("message", handler);
|
||||
});
|
||||
}
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
id: "c1",
|
||||
method: "connect",
|
||||
params: {
|
||||
minProtocol: PROTOCOL_VERSION,
|
||||
maxProtocol: PROTOCOL_VERSION,
|
||||
client: {
|
||||
id: "test",
|
||||
displayName: "docker-net-e2e",
|
||||
version: "dev",
|
||||
platform: process.platform,
|
||||
mode: "test",
|
||||
},
|
||||
caps: [],
|
||||
auth: { token },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const connectRes = await onceFrame((frame) => frame?.type === "res" && frame?.id === "c1");
|
||||
if (!connectRes.ok) {
|
||||
throw new Error(`connect failed: ${connectRes.error?.message ?? "unknown"}`);
|
||||
}
|
||||
|
||||
ws.close();
|
||||
console.log("ok");
|
||||
183
scripts/e2e/lib/kitchen-sink-plugin/clawhub-fixture-server.cjs
Normal file
183
scripts/e2e/lib/kitchen-sink-plugin/clawhub-fixture-server.cjs
Normal file
@@ -0,0 +1,183 @@
|
||||
const crypto = require("node:crypto");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
const portFile = process.argv[2];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const packageName = "openclaw-kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
const version = "0.1.3";
|
||||
|
||||
async function main() {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"package/package.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/index.js",
|
||||
`module.exports = {
|
||||
id: "${pluginId}",
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "kitchen-sink-provider",
|
||||
label: "Kitchen Sink Provider",
|
||||
docsPath: "/providers/kitchen-sink",
|
||||
auth: [],
|
||||
});
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "kitchen-sink-channel",
|
||||
meta: {
|
||||
id: "kitchen-sink-channel",
|
||||
label: "Kitchen Sink Channel",
|
||||
selectionLabel: "Kitchen Sink",
|
||||
docsPath: "/channels/kitchen-sink",
|
||||
blurb: "Kitchen sink ClawHub fixture channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/openclaw.plugin.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
const packageDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
runtimeId: pluginId,
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
summary: "Kitchen sink plugin fixture for prerelease CI.",
|
||||
ownerHandle: "openclaw",
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
latestVersion: version,
|
||||
tags: { latest: version },
|
||||
capabilityTags: ["test-fixture"],
|
||||
executesCode: true,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.11",
|
||||
minGatewayVersion: "2026.4.11",
|
||||
},
|
||||
capabilities: {
|
||||
executesCode: true,
|
||||
runtimeId: pluginId,
|
||||
capabilityTags: ["test-fixture"],
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
},
|
||||
verification: {
|
||||
tier: "source-linked",
|
||||
sourceRepo: "https://github.com/openclaw/kitchen-sink",
|
||||
hasProvenance: false,
|
||||
scanStatus: "passed",
|
||||
},
|
||||
},
|
||||
};
|
||||
const versionDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
},
|
||||
version: {
|
||||
version,
|
||||
createdAt: 0,
|
||||
changelog: "Fixture package for kitchen-sink plugin prerelease CI.",
|
||||
distTags: ["latest"],
|
||||
sha256hash,
|
||||
compatibility: packageDetail.package.compatibility,
|
||||
capabilities: packageDetail.package.capabilities,
|
||||
verification: packageDetail.package.verification,
|
||||
},
|
||||
};
|
||||
|
||||
const json = (response, value, status = 200) => {
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405);
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}`) {
|
||||
json(response, packageDetail);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}`
|
||||
) {
|
||||
json(response, versionDetail);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/beta`) {
|
||||
json(response, { error: "version not found" }, 404);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/download`) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
});
|
||||
response.end(archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
require("node:fs").writeFileSync(portFile, String(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -49,191 +49,7 @@ start_kitchen_sink_clawhub_fixture_server() {
|
||||
local server_port_file="$fixture_dir/clawhub-fixture-port"
|
||||
local server_pid_file="$fixture_dir/clawhub-fixture-pid"
|
||||
|
||||
node - "$server_port_file" <<'NODE' >"$server_log" 2>&1 &
|
||||
const crypto = require("node:crypto");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
const portFile = process.argv[2];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const packageName = "openclaw-kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
const version = "0.1.3";
|
||||
|
||||
async function main() {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"package/package.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/index.js",
|
||||
`module.exports = {
|
||||
id: "${pluginId}",
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "kitchen-sink-provider",
|
||||
label: "Kitchen Sink Provider",
|
||||
docsPath: "/providers/kitchen-sink",
|
||||
auth: [],
|
||||
});
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "kitchen-sink-channel",
|
||||
meta: {
|
||||
id: "kitchen-sink-channel",
|
||||
label: "Kitchen Sink Channel",
|
||||
selectionLabel: "Kitchen Sink",
|
||||
docsPath: "/channels/kitchen-sink",
|
||||
blurb: "Kitchen sink ClawHub fixture channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/openclaw.plugin.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
const packageDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
runtimeId: pluginId,
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
summary: "Kitchen sink plugin fixture for prerelease CI.",
|
||||
ownerHandle: "openclaw",
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
latestVersion: version,
|
||||
tags: { latest: version },
|
||||
capabilityTags: ["test-fixture"],
|
||||
executesCode: true,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.11",
|
||||
minGatewayVersion: "2026.4.11",
|
||||
},
|
||||
capabilities: {
|
||||
executesCode: true,
|
||||
runtimeId: pluginId,
|
||||
capabilityTags: ["test-fixture"],
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
},
|
||||
verification: {
|
||||
tier: "source-linked",
|
||||
sourceRepo: "https://github.com/openclaw/kitchen-sink",
|
||||
hasProvenance: false,
|
||||
scanStatus: "passed",
|
||||
},
|
||||
},
|
||||
};
|
||||
const versionDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
},
|
||||
version: {
|
||||
version,
|
||||
createdAt: 0,
|
||||
changelog: "Fixture package for kitchen-sink plugin prerelease CI.",
|
||||
distTags: ["latest"],
|
||||
sha256hash,
|
||||
compatibility: packageDetail.package.compatibility,
|
||||
capabilities: packageDetail.package.capabilities,
|
||||
verification: packageDetail.package.verification,
|
||||
},
|
||||
};
|
||||
|
||||
const json = (response, value, status = 200) => {
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405);
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}`) {
|
||||
json(response, packageDetail);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}`
|
||||
) {
|
||||
json(response, versionDetail);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/beta`) {
|
||||
json(response, { error: "version not found" }, 404);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/download`) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
});
|
||||
response.end(archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
require("node:fs").writeFileSync(portFile, String(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
node scripts/e2e/lib/kitchen-sink-plugin/clawhub-fixture-server.cjs "$server_port_file" >"$server_log" 2>&1 &
|
||||
local server_pid="$!"
|
||||
echo "$server_pid" >"$server_pid_file"
|
||||
|
||||
|
||||
23
scripts/e2e/lib/openwebui/http-probe.mjs
Normal file
23
scripts/e2e/lib/openwebui/http-probe.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
const [url, expectedRaw = "200"] = process.argv.slice(2);
|
||||
if (!url) {
|
||||
throw new Error("usage: http-probe.mjs <url> [status|lt500]");
|
||||
}
|
||||
|
||||
const timeoutMs = Number(process.env.OPENCLAW_HTTP_PROBE_TIMEOUT_MS ?? 30_000);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const headers = {};
|
||||
if (process.env.OPENCLAW_HTTP_PROBE_BEARER) {
|
||||
headers.authorization = `Bearer ${process.env.OPENCLAW_HTTP_PROBE_BEARER}`;
|
||||
}
|
||||
const res = await fetch(url, { headers, signal: controller.signal }).catch(() => null);
|
||||
const ok =
|
||||
expectedRaw === "lt500"
|
||||
? Boolean(res && res.status < 500)
|
||||
: res?.status === Number(expectedRaw);
|
||||
process.exit(ok ? 0 : 1);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
137
scripts/e2e/lib/plugins/clawhub-fixture-server.cjs
Normal file
137
scripts/e2e/lib/plugins/clawhub-fixture-server.cjs
Normal file
@@ -0,0 +1,137 @@
|
||||
const crypto = require("node:crypto");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
const portFile = process.argv[2];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const packageName = "openclaw-kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
const version = "0.1.0";
|
||||
|
||||
async function main() {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"package/package.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/index.js",
|
||||
`module.exports = {
|
||||
id: "${pluginId}",
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
description: "Docker E2E kitchen-sink plugin fixture",
|
||||
register(api) {
|
||||
api.on("before_agent_start", async (event, context) => ({
|
||||
kitchenSink: true,
|
||||
observedEventKeys: Object.keys(event || {}),
|
||||
observedContextKeys: Object.keys(context || {}),
|
||||
}));
|
||||
api.registerTool(() => null, { name: "kitchen_sink_tool" });
|
||||
api.registerGatewayMethod("kitchen-sink.ping", async () => ({ ok: true }));
|
||||
api.registerCli(() => {}, { commands: ["kitchen-sink"] });
|
||||
api.registerService({ id: "kitchen-sink-service", start: () => {} });
|
||||
},
|
||||
};
|
||||
`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/openclaw.plugin.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
|
||||
const json = (response, value) => {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405);
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}`) {
|
||||
json(response, {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
runtimeId: pluginId,
|
||||
latestVersion: version,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.26",
|
||||
minGatewayVersion: "2026.4.26",
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}`
|
||||
) {
|
||||
json(response, {
|
||||
version: {
|
||||
version,
|
||||
createdAt: 0,
|
||||
changelog: "Kitchen-sink fixture package for Docker plugin E2E.",
|
||||
sha256hash,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.26",
|
||||
minGatewayVersion: "2026.4.26",
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/download`) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
});
|
||||
response.end(archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
require("node:fs").writeFileSync(portFile, String(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
195
scripts/e2e/lib/plugins/clawhub.sh
Normal file
195
scripts/e2e/lib/plugins/clawhub.sh
Normal file
@@ -0,0 +1,195 @@
|
||||
run_plugins_clawhub_scenario() {
|
||||
if [ "${OPENCLAW_PLUGINS_E2E_CLAWHUB:-1}" = "0" ]; then
|
||||
echo "Skipping ClawHub plugin install and uninstall (OPENCLAW_PLUGINS_E2E_CLAWHUB=0)."
|
||||
else
|
||||
echo "Testing ClawHub kitchen-sink plugin install and uninstall..."
|
||||
CLAWHUB_PLUGIN_SPEC="${OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC:-clawhub:openclaw-kitchen-sink}"
|
||||
CLAWHUB_PLUGIN_ID="${OPENCLAW_PLUGINS_E2E_CLAWHUB_ID:-openclaw-kitchen-sink-fixture}"
|
||||
export CLAWHUB_PLUGIN_SPEC CLAWHUB_PLUGIN_ID
|
||||
|
||||
start_clawhub_fixture_server() {
|
||||
local fixture_dir="$1"
|
||||
local server_log="$fixture_dir/clawhub-fixture.log"
|
||||
local server_port_file="$fixture_dir/clawhub-fixture-port"
|
||||
local server_pid_file="$fixture_dir/clawhub-fixture-pid"
|
||||
|
||||
node scripts/e2e/lib/plugins/clawhub-fixture-server.cjs "$server_port_file" >"$server_log" 2>&1 &
|
||||
local server_pid="$!"
|
||||
echo "$server_pid" >"$server_pid_file"
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
trap 'if [[ -f "'"$server_pid_file"'" ]]; then kill "$(cat "'"$server_pid_file"'")" 2>/dev/null || true; fi' EXIT
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
cat "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
cat "$server_log"
|
||||
echo "Timed out waiting for ClawHub fixture server." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]; then
|
||||
# Keep the release-path smoke hermetic; live ClawHub can rate-limit CI.
|
||||
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-clawhub-fixture.XXXXXX")"
|
||||
start_clawhub_fixture_server "$clawhub_fixture_dir"
|
||||
fi
|
||||
|
||||
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 configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
|
||||
if (!allowLegacyCompat && !index.installRecords) {
|
||||
throw new Error("expected modern installRecords in installed plugin index");
|
||||
}
|
||||
const installRecords = allowLegacyCompat
|
||||
? index.installRecords ?? index.records ?? config.plugins?.installs ?? {}
|
||||
: index.installRecords ?? {};
|
||||
const record = 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")) : {};
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
const installRecords = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
|
||||
if (installRecords[pluginId]) {
|
||||
throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
|
||||
const configAfterUninstallPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const configAfterUninstall = fs.existsSync(configAfterUninstallPath)
|
||||
? JSON.parse(fs.readFileSync(configAfterUninstallPath, "utf8"))
|
||||
: {};
|
||||
if (configAfterUninstall.plugins?.entries?.[pluginId]) {
|
||||
throw new Error(`ClawHub config entry still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
if ((configAfterUninstall.plugins?.allow || []).includes(pluginId)) {
|
||||
throw new Error(`ClawHub allowlist entry still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
if ((configAfterUninstall.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
|
||||
}
|
||||
87
scripts/e2e/lib/plugins/fixtures.sh
Normal file
87
scripts/e2e/lib/plugins/fixtures.sh
Normal file
@@ -0,0 +1,87 @@
|
||||
record_fixture_plugin_trust() {
|
||||
local plugin_id="$1"
|
||||
local plugin_root="$2"
|
||||
local enabled="$3"
|
||||
node - "$plugin_id" "$plugin_root" "$enabled" <<'NODE'
|
||||
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
|
||||
}
|
||||
|
||||
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 - "$dir/openclaw.plugin.json" "$id" <<'NODE'
|
||||
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
|
||||
}
|
||||
173
scripts/e2e/lib/plugins/marketplace.sh
Normal file
173
scripts/e2e/lib/plugins/marketplace.sh
Normal file
@@ -0,0 +1,173 @@
|
||||
run_plugins_marketplace_scenario() {
|
||||
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"));
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
|
||||
if (!allowLegacyCompat && !index.installRecords) {
|
||||
throw new Error("expected modern installRecords in installed plugin index");
|
||||
}
|
||||
const installRecords = allowLegacyCompat
|
||||
? index.installRecords ?? index.records ?? config.plugins?.installs ?? {}
|
||||
: index.installRecords ?? {};
|
||||
for (const id of ["marketplace-shortcut", "marketplace-direct"]) {
|
||||
const record = installRecords[id];
|
||||
if (!record) {
|
||||
if (allowLegacyCompat) {
|
||||
console.log(`legacy package did not persist marketplace install record for ${id}`);
|
||||
continue;
|
||||
}
|
||||
throw new Error(`missing marketplace 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
|
||||
}
|
||||
@@ -13,94 +13,9 @@ openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing
|
||||
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 - "$plugin_id" "$plugin_root" "$enabled" <<'NODE'
|
||||
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
|
||||
}
|
||||
|
||||
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 - "$dir/openclaw.plugin.json" "$id" <<'NODE'
|
||||
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
|
||||
}
|
||||
|
||||
source scripts/e2e/lib/plugins/fixtures.sh
|
||||
source scripts/e2e/lib/plugins/marketplace.sh
|
||||
source scripts/e2e/lib/plugins/clawhub.sh
|
||||
demo_plugin_id="demo-plugin"
|
||||
demo_plugin_root="$OPENCLAW_PLUGIN_HOME/$demo_plugin_id"
|
||||
mkdir -p "$demo_plugin_root"
|
||||
@@ -405,506 +320,6 @@ if (!inspect.gatewayMethods.includes("demo.slash.install")) {
|
||||
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
|
||||
run_plugins_marketplace_scenario
|
||||
|
||||
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"));
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
|
||||
if (!allowLegacyCompat && !index.installRecords) {
|
||||
throw new Error("expected modern installRecords in installed plugin index");
|
||||
}
|
||||
const installRecords = allowLegacyCompat
|
||||
? index.installRecords ?? index.records ?? config.plugins?.installs ?? {}
|
||||
: index.installRecords ?? {};
|
||||
for (const id of ["marketplace-shortcut", "marketplace-direct"]) {
|
||||
const record = installRecords[id];
|
||||
if (!record) {
|
||||
if (allowLegacyCompat) {
|
||||
console.log(`legacy package did not persist marketplace install record for ${id}`);
|
||||
continue;
|
||||
}
|
||||
throw new Error(`missing marketplace 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 kitchen-sink plugin install and uninstall..."
|
||||
CLAWHUB_PLUGIN_SPEC="${OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC:-clawhub:openclaw-kitchen-sink}"
|
||||
CLAWHUB_PLUGIN_ID="${OPENCLAW_PLUGINS_E2E_CLAWHUB_ID:-openclaw-kitchen-sink-fixture}"
|
||||
export CLAWHUB_PLUGIN_SPEC CLAWHUB_PLUGIN_ID
|
||||
|
||||
start_clawhub_fixture_server() {
|
||||
local fixture_dir="$1"
|
||||
local server_log="$fixture_dir/clawhub-fixture.log"
|
||||
local server_port_file="$fixture_dir/clawhub-fixture-port"
|
||||
local server_pid_file="$fixture_dir/clawhub-fixture-pid"
|
||||
|
||||
node - "$server_port_file" <<'NODE' >"$server_log" 2>&1 &
|
||||
const crypto = require("node:crypto");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
const portFile = process.argv[2];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const packageName = "openclaw-kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
const version = "0.1.0";
|
||||
|
||||
async function main() {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"package/package.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/index.js",
|
||||
`module.exports = {
|
||||
id: "${pluginId}",
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
description: "Docker E2E kitchen-sink plugin fixture",
|
||||
register(api) {
|
||||
api.on("before_agent_start", async (event, context) => ({
|
||||
kitchenSink: true,
|
||||
observedEventKeys: Object.keys(event || {}),
|
||||
observedContextKeys: Object.keys(context || {}),
|
||||
}));
|
||||
api.registerTool(() => null, { name: "kitchen_sink_tool" });
|
||||
api.registerGatewayMethod("kitchen-sink.ping", async () => ({ ok: true }));
|
||||
api.registerCli(() => {}, { commands: ["kitchen-sink"] });
|
||||
api.registerService({ id: "kitchen-sink-service", start: () => {} });
|
||||
},
|
||||
};
|
||||
`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/openclaw.plugin.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
|
||||
const json = (response, value) => {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405);
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}`) {
|
||||
json(response, {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
runtimeId: pluginId,
|
||||
latestVersion: version,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.26",
|
||||
minGatewayVersion: "2026.4.26",
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}`
|
||||
) {
|
||||
json(response, {
|
||||
version: {
|
||||
version,
|
||||
createdAt: 0,
|
||||
changelog: "Kitchen-sink fixture package for Docker plugin E2E.",
|
||||
sha256hash,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.26",
|
||||
minGatewayVersion: "2026.4.26",
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/download`) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
});
|
||||
response.end(archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
require("node:fs").writeFileSync(portFile, String(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
local server_pid="$!"
|
||||
echo "$server_pid" >"$server_pid_file"
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
trap 'if [[ -f "'"$server_pid_file"'" ]]; then kill "$(cat "'"$server_pid_file"'")" 2>/dev/null || true; fi' EXIT
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
cat "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
cat "$server_log"
|
||||
echo "Timed out waiting for ClawHub fixture server." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]; then
|
||||
# Keep the release-path smoke hermetic; live ClawHub can rate-limit CI.
|
||||
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-clawhub-fixture.XXXXXX")"
|
||||
start_clawhub_fixture_server "$clawhub_fixture_dir"
|
||||
fi
|
||||
|
||||
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 configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
|
||||
if (!allowLegacyCompat && !index.installRecords) {
|
||||
throw new Error("expected modern installRecords in installed plugin index");
|
||||
}
|
||||
const installRecords = allowLegacyCompat
|
||||
? index.installRecords ?? index.records ?? config.plugins?.installs ?? {}
|
||||
: index.installRecords ?? {};
|
||||
const record = 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")) : {};
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
const installRecords = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
|
||||
if (installRecords[pluginId]) {
|
||||
throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
|
||||
const configAfterUninstallPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const configAfterUninstall = fs.existsSync(configAfterUninstallPath)
|
||||
? JSON.parse(fs.readFileSync(configAfterUninstallPath, "utf8"))
|
||||
: {};
|
||||
if (configAfterUninstall.plugins?.entries?.[pluginId]) {
|
||||
throw new Error(`ClawHub config entry still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
if ((configAfterUninstall.plugins?.allow || []).includes(pluginId)) {
|
||||
throw new Error(`ClawHub allowlist entry still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
if ((configAfterUninstall.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
|
||||
run_plugins_clawhub_scenario
|
||||
|
||||
@@ -20,7 +20,8 @@ docker_e2e_run_logged_with_harness openai-image-auth \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
-i "$IMAGE_NAME" bash -lc '
|
||||
set -euo pipefail
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
|
||||
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}"
|
||||
export OPENCLAW_SKIP_CHANNELS=1
|
||||
export OPENCLAW_SKIP_GMAIL_WATCHER=1
|
||||
export OPENCLAW_SKIP_CRON=1
|
||||
|
||||
@@ -24,10 +24,6 @@ OW_NAME="openclaw-openwebui-$$"
|
||||
DOCKER_COMMAND_TIMEOUT="${OPENCLAW_OPENWEBUI_DOCKER_COMMAND_TIMEOUT:-600s}"
|
||||
DOCKER_PULL_TIMEOUT="${OPENCLAW_OPENWEBUI_DOCKER_PULL_TIMEOUT:-600s}"
|
||||
|
||||
docker_cmd() {
|
||||
timeout "$DOCKER_COMMAND_TIMEOUT" "$@"
|
||||
}
|
||||
|
||||
OPENAI_API_KEY_VALUE="${OPENAI_API_KEY:-}"
|
||||
if [[ "$OPENAI_API_KEY_VALUE" == "undefined" || "$OPENAI_API_KEY_VALUE" == "null" ]]; then
|
||||
OPENAI_API_KEY_VALUE=""
|
||||
@@ -42,9 +38,9 @@ if [[ -z "$OPENAI_API_KEY_VALUE" ]]; then
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
docker_cmd docker rm -f "$OW_NAME" >/dev/null 2>&1 || true
|
||||
docker_cmd docker rm -f "$GW_NAME" >/dev/null 2>&1 || true
|
||||
docker_cmd docker network rm "$NET_NAME" >/dev/null 2>&1 || true
|
||||
docker_e2e_docker_cmd rm -f "$OW_NAME" >/dev/null 2>&1 || true
|
||||
docker_e2e_docker_cmd rm -f "$GW_NAME" >/dev/null 2>&1 || true
|
||||
docker_e2e_docker_cmd network rm "$NET_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
@@ -54,12 +50,12 @@ echo "Pulling Open WebUI image: $OPENWEBUI_IMAGE"
|
||||
timeout "$DOCKER_PULL_TIMEOUT" docker pull "$OPENWEBUI_IMAGE" >/dev/null
|
||||
|
||||
echo "Creating Docker network..."
|
||||
docker_cmd docker network create "$NET_NAME" >/dev/null
|
||||
docker_e2e_docker_cmd network create "$NET_NAME" >/dev/null
|
||||
|
||||
echo "Starting gateway container..."
|
||||
# Harness files are mounted read-only; the app under test comes from /app/dist.
|
||||
docker_e2e_harness_mount_args
|
||||
docker_cmd docker run -d \
|
||||
docker_e2e_docker_cmd run -d \
|
||||
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
|
||||
--name "$GW_NAME" \
|
||||
--network "$NET_NAME" \
|
||||
@@ -124,12 +120,7 @@ EOF
|
||||
' >/dev/null
|
||||
|
||||
echo "Waiting for gateway HTTP surface..."
|
||||
if ! docker_e2e_wait_container_bash_while_running "$OW_NAME" "$GW_NAME" 240 1 "node --input-type=module -e '
|
||||
const res = await fetch(\"http://127.0.0.1:$PORT/v1/models\", {
|
||||
headers: { authorization: \"Bearer $TOKEN\" },
|
||||
}).catch(() => null);
|
||||
process.exit(res?.status === 200 ? 0 : 1);
|
||||
'"; then
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 240 1 "OPENCLAW_HTTP_PROBE_BEARER='$TOKEN' node scripts/e2e/lib/openwebui/http-probe.mjs 'http://127.0.0.1:$PORT/v1/models' 200"; then
|
||||
echo "Gateway failed to start"
|
||||
docker_e2e_docker_cmd inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
docker_e2e_tail_container_file_if_running "$GW_NAME" /tmp/openwebui-gateway.log 200
|
||||
@@ -137,7 +128,7 @@ if ! docker_e2e_wait_container_bash_while_running "$OW_NAME" "$GW_NAME" 240 1 "n
|
||||
fi
|
||||
|
||||
echo "Starting Open WebUI container..."
|
||||
docker_cmd docker run -d \
|
||||
docker_e2e_docker_cmd run -d \
|
||||
--name "$OW_NAME" \
|
||||
--network "$NET_NAME" \
|
||||
-e ENV=prod \
|
||||
@@ -161,31 +152,14 @@ docker_cmd docker run -d \
|
||||
"$OPENWEBUI_IMAGE" >/dev/null
|
||||
|
||||
echo "Waiting for Open WebUI..."
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 240 1 "node --input-type=module -e '
|
||||
const res = await fetch(\"http://$OW_NAME:$WEBUI_PORT/\").catch(() => null);
|
||||
process.exit(res && res.status < 500 ? 0 : 1);
|
||||
'"; then
|
||||
if ! docker_e2e_wait_container_bash_while_running "$OW_NAME" "$GW_NAME" 240 1 "node scripts/e2e/lib/openwebui/http-probe.mjs 'http://$OW_NAME:$WEBUI_PORT/' lt500"; then
|
||||
echo "Open WebUI failed to start"
|
||||
docker_e2e_docker_cmd logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for gateway model endpoint after Open WebUI startup..."
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 90 5 "node --input-type=module -e '
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
const res = await fetch(\"http://$GW_NAME:$PORT/v1/models\", {
|
||||
headers: { authorization: \"Bearer $TOKEN\" },
|
||||
signal: controller.signal,
|
||||
});
|
||||
process.exit(res.status === 200 ? 0 : 1);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
'"; then
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 90 5 "OPENCLAW_HTTP_PROBE_BEARER='$TOKEN' OPENCLAW_HTTP_PROBE_TIMEOUT_MS=8000 node scripts/e2e/lib/openwebui/http-probe.mjs 'http://$GW_NAME:$PORT/v1/models' 200"; then
|
||||
echo "Gateway model endpoint did not stay reachable after Open WebUI startup"
|
||||
docker_e2e_docker_cmd inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
docker_e2e_tail_container_file_if_running "$GW_NAME" /tmp/openwebui-gateway.log 200
|
||||
@@ -194,7 +168,7 @@ if ! docker_e2e_wait_container_bash "$GW_NAME" 90 5 "node --input-type=module -e
|
||||
fi
|
||||
|
||||
echo "Running Open WebUI -> OpenClaw smoke..."
|
||||
if ! docker_cmd docker exec \
|
||||
if ! docker_e2e_docker_cmd exec \
|
||||
-e "OPENWEBUI_BASE_URL=http://$OW_NAME:$WEBUI_PORT" \
|
||||
-e "OPENWEBUI_ADMIN_EMAIL=$ADMIN_EMAIL" \
|
||||
-e "OPENWEBUI_ADMIN_PASSWORD=$ADMIN_PASSWORD" \
|
||||
|
||||
@@ -26,7 +26,8 @@ docker_e2e_run_with_harness \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "set -euo pipefail
|
||||
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"
|
||||
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}\"
|
||||
tsx scripts/e2e/pi-bundle-mcp-tools-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
|
||||
@@ -15,15 +15,15 @@ docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(
|
||||
node "$ROOT_DIR/scripts/lib/openclaw-test-state.mjs" shell \
|
||||
--label update-channel-switch \
|
||||
--scenario update-stable \
|
||||
| base64 \
|
||||
| tr -d '\n'
|
||||
--scenario update-stable |
|
||||
base64 |
|
||||
tr -d '\n'
|
||||
)"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" update-channel-switch "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
|
||||
echo "Running update channel switch E2E..."
|
||||
docker run --rm \
|
||||
docker_e2e_run_with_harness \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENCLAW_SKIP_CHANNELS=1 \
|
||||
-e OPENCLAW_SKIP_PROVIDERS=1 \
|
||||
@@ -31,6 +31,7 @@ docker run --rm \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc 'set -euo pipefail
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
@@ -140,31 +141,11 @@ pkg_tgz_path="$package_tgz"
|
||||
npm install -g --prefix /tmp/npm-prefix --omit=optional "$pkg_tgz_path"
|
||||
package_version="$(node -p "JSON.parse(require(\"node:fs\").readFileSync(\"/tmp/npm-prefix/lib/node_modules/openclaw/package.json\", \"utf8\")).version")"
|
||||
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
|
||||
node - "$package_version" <<"NODE"
|
||||
const version = process.argv[2] || "";
|
||||
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/.exec(version);
|
||||
if (!match) {
|
||||
console.log("0");
|
||||
process.exit(0);
|
||||
}
|
||||
const value = [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||
const max = [2026, 4, 25];
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
if (value[i] < max[i]) {
|
||||
console.log("1");
|
||||
process.exit(0);
|
||||
}
|
||||
if (value[i] > max[i]) {
|
||||
console.log("0");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
console.log("1");
|
||||
NODE
|
||||
node scripts/e2e/lib/package-compat.mjs "$package_version"
|
||||
)"
|
||||
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
|
||||
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
|
||||
|
||||
export OPENCLAW_GIT_DIR="$git_root"
|
||||
export OPENCLAW_UPDATE_DEV_TARGET_REF="$fixture_sha"
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
# while polling readiness from the host.
|
||||
|
||||
docker_e2e_docker_cmd() {
|
||||
timeout "${DOCKER_COMMAND_TIMEOUT:-600s}" docker "$@"
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "${DOCKER_COMMAND_TIMEOUT:-600s}" docker "$@"
|
||||
return
|
||||
fi
|
||||
docker "$@"
|
||||
}
|
||||
|
||||
docker_e2e_container_running() {
|
||||
|
||||
Reference in New Issue
Block a user