diff --git a/scripts/e2e/plugins-docker.sh b/scripts/e2e/plugins-docker.sh index 5c34508a6cb..238185fa3a3 100755 --- a/scripts/e2e/plugins-docker.sh +++ b/scripts/e2e/plugins-docker.sh @@ -611,6 +611,169 @@ CLAWHUB_PLUGIN_SPEC="${OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC:-clawhub:openclaw-now4r CLAWHUB_PLUGIN_ID="${OPENCLAW_PLUGINS_E2E_CLAWHUB_ID:-now4real}" 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 - <<'NODE' "$server_port_file" >"$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-now4real"; +const pluginId = "now4real"; +const version = "0.1.2"; + +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: "Now 4 Real", + register(api) { + api.registerGatewayMethod("now4real.ping", async () => ({ ok: true })); + }, +}; +`, + { 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: "Now 4 Real", + family: "code-plugin", + channel: "official", + isOfficial: true, + runtimeId: pluginId, + latestVersion: version, + createdAt: 0, + updatedAt: 0, + compatibility: { + pluginApiRange: ">=2026.4.11", + minGatewayVersion: "2026.4.11", + }, + }, + }); + return; + } + if ( + url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}` + ) { + json(response, { + version: { + version, + createdAt: 0, + changelog: "Fixture package for Docker plugin E2E.", + sha256hash, + compatibility: { + pluginApiRange: ">=2026.4.11", + minGatewayVersion: "2026.4.11", + }, + }, + }); + 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:")) { diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 7784160df91..92f463ef61b 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -6,6 +6,7 @@ const DOCKER_ALL_SCHEDULER_PATH = "scripts/test-docker-all.mjs"; const DOCKER_E2E_SCENARIOS_PATH = "scripts/lib/docker-e2e-scenarios.mjs"; const INSTALL_E2E_RUNNER_PATH = "scripts/docker/install-sh-e2e/run.sh"; const OPENAI_WEB_SEARCH_MINIMAL_E2E_PATH = "scripts/e2e/openai-web-search-minimal-docker.sh"; +const PLUGINS_DOCKER_E2E_PATH = "scripts/e2e/plugins-docker.sh"; const CENTRALIZED_BUILD_SCRIPTS = [ "scripts/docker/setup.sh", "scripts/e2e/browser-cdp-snapshot-docker.sh", @@ -95,4 +96,13 @@ describe("docker build helper", () => { expect(runner).toContain('[...gatewayArgs, "agent", "--params"'); expect(runner).not.toContain('"agent.wait"'); }); + + it("keeps ClawHub plugin Docker smoke hermetic by default", () => { + const runner = readFileSync(PLUGINS_DOCKER_E2E_PATH, "utf8"); + + expect(runner).toContain("start_clawhub_fixture_server()"); + expect(runner).toContain('OPENCLAW_CLAWHUB_URL="http://127.0.0.1:'); + expect(runner).toContain("live ClawHub can rate-limit CI"); + expect(runner).toContain('[[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]'); + }); });