refactor: share docker e2e harness runner

This commit is contained in:
Peter Steinberger
2026-04-29 08:28:49 +01:00
parent f6a2cf15c0
commit 34bd962a20
11 changed files with 157 additions and 159 deletions

View File

@@ -7,7 +7,6 @@ IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-bundled-plugin-install-uninstal
docker_e2e_build_or_reuse "$IMAGE_NAME" bundled-plugin-install-uninstall
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 bundled-plugin-install-uninstall empty)"
docker_e2e_harness_mount_args
DOCKER_ENV_ARGS=(
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0
@@ -25,9 +24,8 @@ done
echo "Running bundled plugin install/uninstall Docker E2E..."
RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-bundled-plugin-install-uninstall.XXXXXX")"
if ! docker run --rm \
if ! docker_e2e_run_with_harness \
"${DOCKER_ENV_ARGS[@]}" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh >"$RUN_LOG" 2>&1
then

View File

@@ -16,16 +16,14 @@ cleanup() {
trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-first-run
docker_e2e_harness_mount_args
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 crestodian-first-run empty)"
echo "Running in-container Crestodian first-run smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
docker_e2e_run_with_harness \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"

View File

@@ -16,16 +16,14 @@ cleanup() {
trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-planner
docker_e2e_harness_mount_args
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 crestodian-planner empty)"
echo "Running in-container Crestodian planner fallback smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
docker_e2e_run_with_harness \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"

View File

@@ -16,16 +16,14 @@ cleanup() {
trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-rescue
docker_e2e_harness_mount_args
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 crestodian-rescue empty)"
echo "Running in-container Crestodian rescue smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
docker_e2e_run_with_harness \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"

View File

@@ -0,0 +1,137 @@
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
const home = os.homedir();
const readJson = (file) => {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return {};
}
};
const pluginRecordSnapshot = () => {
const config = readJson(openclawPath("openclaw.json"));
const index = readJson(openclawPath("plugins", "installs.json"));
const records = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const record = records["lossless-claw"] ?? records["@example/lossless-claw"];
if (!record) {
throw new Error("missing plugin install record");
}
const { source, spec, resolvedName, resolvedVersion, resolvedSpec, integrity, shasum } = record;
return { source, spec, resolvedName, resolvedVersion, resolvedSpec, integrity, shasum };
};
function legacyCompat(version) {
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?/.exec(version);
const [year, month, day] = match?.slice(1, 4).map(Number) ?? [];
return (
Boolean(match) && (year < 2026 || (year === 2026 && (month < 4 || (month === 4 && day <= 25))))
);
}
function openclawPath(...parts) {
return path.join(home, ".openclaw", ...parts);
}
function writeJson(file, value) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
function seedInstallState() {
writeJson(openclawPath("extensions", "lossless-claw", "package.json"), {
name: "@example/lossless-claw",
version: "0.9.0",
});
writeJson(process.env.OPENCLAW_CONFIG_PATH, { plugins: {} });
writeJson(openclawPath("plugins", "installs.json"), {
version: 1,
warning: "DO NOT EDIT. This file is generated by OpenClaw plugin registry commands.",
hostContractVersion: "docker-e2e",
compatRegistryVersion: "docker-e2e",
migrationVersion: 1,
policyHash: "docker-e2e",
generatedAtMs: 1777118400000,
installRecords: {
"lossless-claw": {
source: "npm",
spec: "@example/lossless-claw@0.9.0",
installPath: "~/.openclaw/extensions/lossless-claw",
resolvedName: "@example/lossless-claw",
resolvedVersion: "0.9.0",
resolvedSpec: "@example/lossless-claw@0.9.0",
integrity: "sha512-same",
shasum: "same",
},
},
plugins: [],
diagnostics: [],
});
}
async function waitRegistry() {
for (let attempt = 0; attempt < 50; attempt += 1) {
if (await registryHealthy()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error("Local npm metadata registry failed to start");
}
function registryHealthy() {
return new Promise((resolve) => {
const req = http.get("http://127.0.0.1:4873/@example%2flossless-claw", (res) => {
resolve(res.statusCode === 200);
res.resume();
});
req.on("error", () => resolve(false));
req.setTimeout(200, () => {
req.destroy();
resolve(false);
});
});
}
function assertSnapshot(beforePath) {
const before = readJson(beforePath);
const after = pluginRecordSnapshot();
if (JSON.stringify(before) !== JSON.stringify(after)) {
throw new Error(
`plugin install record changed unexpectedly: ${JSON.stringify({ before, after })}`,
);
}
}
function assertOutput(logPath) {
const output = fs.readFileSync(logPath, "utf8");
const failure = output.includes("Downloading @example/lossless-claw")
? "Unexpected npm download/reinstall path"
: !output.includes("lossless-claw is up to date (0.9.0).")
? "Expected up-to-date output missing"
: "";
if (failure) {
throw new Error(`${failure}\n${output}`);
}
}
const [command, arg] = process.argv.slice(2);
const commands = {
"legacy-compat": () => console.log(legacyCompat(arg || "") ? "1" : "0"),
seed: seedInstallState,
"wait-registry": waitRegistry,
snapshot: () => process.stdout.write(JSON.stringify(pluginRecordSnapshot(), null, 2)),
"assert-snapshot": () => assertSnapshot(arg),
"assert-output": () => assertOutput(arg),
};
const run = commands[command];
await (
run ??
(() => {
throw new Error(`Unknown plugin update probe command: ${command || "(missing)"}`);
})
)();

View File

@@ -8,76 +8,20 @@ openclaw_e2e_install_package /tmp/openclaw-install.log "mounted OpenClaw package
package_root="$(openclaw_e2e_package_root /tmp/npm-prefix)"
entry="$(openclaw_e2e_package_entrypoint "$package_root")"
probe="scripts/e2e/lib/plugin-update/probe.mjs"
package_version="$(node -p "require('$package_root/package.json').version")"
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
PACKAGE_VERSION="$package_version" node -e 'const version = process.env.PACKAGE_VERSION || ""; const match = new RegExp("^(\\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");'
)"
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(node "$probe" legacy-compat "$package_version")"
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
export NPM_CONFIG_REGISTRY=http://127.0.0.1:4873
export PATH="/tmp/npm-prefix/bin:$PATH"
mkdir -p "$HOME/.openclaw/extensions/lossless-claw"
cat > "$HOME/.openclaw/extensions/lossless-claw/package.json" <<'JSON'
{
"name": "@example/lossless-claw",
"version": "0.9.0"
}
JSON
cat > "$OPENCLAW_CONFIG_PATH" <<'JSON'
{
"plugins": {}
}
JSON
mkdir -p "$HOME/.openclaw/plugins"
cat > "$HOME/.openclaw/plugins/installs.json" <<'JSON'
{
"version": 1,
"warning": "DO NOT EDIT. This file is generated by OpenClaw plugin registry commands.",
"hostContractVersion": "docker-e2e",
"compatRegistryVersion": "docker-e2e",
"migrationVersion": 1,
"policyHash": "docker-e2e",
"generatedAtMs": 1777118400000,
"installRecords": {
"lossless-claw": {
"source": "npm",
"spec": "@example/lossless-claw@0.9.0",
"installPath": "~/.openclaw/extensions/lossless-claw",
"resolvedName": "@example/lossless-claw",
"resolvedVersion": "0.9.0",
"resolvedSpec": "@example/lossless-claw@0.9.0",
"integrity": "sha512-same",
"shasum": "same"
}
},
"plugins": [],
"diagnostics": []
}
JSON
node "$probe" seed
node scripts/e2e/lib/plugin-update/registry-server.mjs >/tmp/openclaw-e2e-registry.log 2>&1 &
registry_pid=$!
trap 'kill "$registry_pid" >/dev/null 2>&1 || true' EXIT
registry_ready=0
for _ in $(seq 1 50); do
if node --input-type=module -e '
import http from "node:http";
const req = http.get("http://127.0.0.1:4873/@example%2flossless-claw", (res) => {
process.exit(res.statusCode === 200 ? 0 : 1);
});
req.on("error", () => process.exit(1));
req.setTimeout(200, () => {
req.destroy();
process.exit(1);
});
'; then
registry_ready=1
break
fi
sleep 0.1
done
if [ "$registry_ready" -ne 1 ]; then
if ! node "$probe" wait-registry; then
echo "Local npm metadata registry failed to start"
cat /tmp/openclaw-e2e-registry.log || true
exit 1
@@ -89,37 +33,7 @@ if [ "$OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT" != "1" ]; then
fi
plugin_update_timeout_seconds="${OPENCLAW_PLUGIN_UPDATE_TIMEOUT_SECONDS:-180}"
node --input-type=module > /tmp/plugin-update-before.json <<'NODE'
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const readJson = (file) => {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return {};
}
};
const home = os.homedir();
const config = readJson(path.join(home, ".openclaw", "openclaw.json"));
const index = readJson(path.join(home, ".openclaw", "plugins", "installs.json"));
const records = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const record = records["lossless-claw"] ?? records["@example/lossless-claw"];
if (!record) {
throw new Error("missing seeded plugin install record");
}
const snapshot = {
source: record.source,
spec: record.spec,
resolvedName: record.resolvedName,
resolvedVersion: record.resolvedVersion,
resolvedSpec: record.resolvedSpec,
integrity: record.integrity,
shasum: record.shasum,
};
process.stdout.write(JSON.stringify(snapshot, null, 2));
NODE
node "$probe" snapshot > /tmp/plugin-update-before.json
set +e
timeout "${plugin_update_timeout_seconds}s" node "$entry" plugins update @example/lossless-claw > /tmp/plugin-update-output.log 2>&1
@@ -143,48 +57,6 @@ if [ -n "$before_config_hash" ]; then
fi
fi
node --input-type=module <<'NODE'
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const readJson = (file) => {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return {};
}
};
const home = os.homedir();
const before = readJson("/tmp/plugin-update-before.json");
const config = readJson(path.join(home, ".openclaw", "openclaw.json"));
const index = readJson(path.join(home, ".openclaw", "plugins", "installs.json"));
const records = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const record = records["lossless-claw"] ?? records["@example/lossless-claw"];
if (!record) {
throw new Error("missing plugin install record after update");
}
const after = {
source: record.source,
spec: record.spec,
resolvedName: record.resolvedName,
resolvedVersion: record.resolvedVersion,
resolvedSpec: record.resolvedSpec,
integrity: record.integrity,
shasum: record.shasum,
};
if (JSON.stringify(before) !== JSON.stringify(after)) {
throw new Error(`plugin install record changed unexpectedly: ${JSON.stringify({ before, after })}`);
}
NODE
if grep -q "Downloading @example/lossless-claw" /tmp/plugin-update-output.log; then
echo "Unexpected npm download/reinstall path"
cat /tmp/plugin-update-output.log
exit 1
fi
if ! grep -q "lossless-claw is up to date (0.9.0)." /tmp/plugin-update-output.log; then
echo "Expected up-to-date output missing"
cat /tmp/plugin-update-output.log
exit 1
fi
node "$probe" assert-snapshot /tmp/plugin-update-before.json
node "$probe" assert-output /tmp/plugin-update-output.log
cat /tmp/plugin-update-output.log

View File

@@ -38,17 +38,15 @@ prepare_package_tgz() {
prepare_package_tgz
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_harness_mount_args
run_log="$(docker_e2e_run_log npm-onboard-channel-agent)"
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 npm-onboard-channel-agent empty)"
echo "Running npm tarball onboard/channel/agent Docker E2E ($CHANNEL)..."
if ! docker run --rm \
if ! docker_e2e_run_with_harness \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENCLAW_NPM_ONBOARD_CHANNEL="$CHANNEL" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
-i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'
set -euo pipefail

View File

@@ -16,16 +16,14 @@ cleanup() {
trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" pi-bundle-mcp-tools
docker_e2e_harness_mount_args
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 pi-bundle-mcp-tools empty)"
echo "Running in-container Pi bundle MCP tool availability smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
docker_e2e_run_with_harness \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
eval \"\$(printf '%s' \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" | base64 -d)\"

View File

@@ -12,19 +12,17 @@ SKIP_BUILD="${OPENCLAW_PLUGIN_UPDATE_E2E_SKIP_BUILD:-0}"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz plugin-update "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
# Bare lanes mount the package artifact instead of baking app sources into the image.
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_harness_mount_args
docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 plugin-update empty)"
echo "Running unchanged plugin update smoke..."
docker run --rm \
docker_e2e_run_with_harness \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENCLAW_SKIP_CHANNELS=1 \
-e OPENCLAW_SKIP_PROVIDERS=1 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash scripts/e2e/lib/plugin-update/unchanged-scenario.sh

View File

@@ -17,15 +17,13 @@ cleanup() {
trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" session-runtime-context
docker_e2e_harness_mount_args
echo "Running session runtime context Docker E2E..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
docker_e2e_run_with_harness \
--name "$CONTAINER_NAME" \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
"${DOCKER_E2E_HARNESS_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc 'set -euo pipefail; tsx scripts/e2e/session-runtime-context-docker-client.ts' \
>"$RUN_LOG" 2>&1

View File

@@ -62,3 +62,8 @@ docker_e2e_package_mount_args() {
docker_e2e_harness_mount_args() {
DOCKER_E2E_HARNESS_ARGS=(-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" -v "$ROOT_DIR/scripts/lib:/app/scripts/lib:ro")
}
docker_e2e_run_with_harness() {
docker_e2e_harness_mount_args
docker run --rm "${DOCKER_E2E_HARNESS_ARGS[@]}" "$@"
}