mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:50:46 +00:00
Tolerate corrupt plugins during update (#77706)
* fix(update): tolerate corrupt plugin state * fix(update): preserve corrupt plugin proof state * fix(update): narrow corrupt plugin warnings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
89
scripts/e2e/lib/plugin-update/corrupt-update-scenario.sh
Normal file
89
scripts/e2e/lib/plugin-update/corrupt-update-scenario.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
source scripts/e2e/lib/plugins/fixtures.sh
|
||||
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
|
||||
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
export npm_config_prefix=/tmp/npm-prefix
|
||||
export NPM_CONFIG_PREFIX=/tmp/npm-prefix
|
||||
export PATH="/tmp/npm-prefix/bin:$PATH"
|
||||
export CI=true
|
||||
export OPENCLAW_DISABLE_BUNDLED_PLUGINS=1
|
||||
export OPENCLAW_NO_ONBOARD=1
|
||||
export OPENCLAW_NO_PROMPT=1
|
||||
|
||||
baseline="${OPENCLAW_UPDATE_CORRUPT_PLUGIN_BASELINE:-openclaw@latest}"
|
||||
echo "Installing baseline OpenClaw package: $baseline"
|
||||
if ! npm install -g --prefix /tmp/npm-prefix --omit=optional "$baseline" >/tmp/openclaw-update-corrupt-baseline-install.log 2>&1; then
|
||||
cat /tmp/openclaw-update-corrupt-baseline-install.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
package_root="$(openclaw_e2e_package_root /tmp/npm-prefix)"
|
||||
entry="$(openclaw_e2e_package_entrypoint "$package_root")"
|
||||
export OPENCLAW_ENTRY="$entry"
|
||||
|
||||
npm_pack_dir="$(mktemp -d "/tmp/openclaw-corrupt-plugin-pack.XXXXXX")"
|
||||
npm_registry_dir="$(mktemp -d "/tmp/openclaw-corrupt-plugin-registry.XXXXXX")"
|
||||
pack_fixture_plugin "$npm_pack_dir" /tmp/demo-corrupt-plugin.tgz demo-corrupt-plugin 0.0.1 demo.corrupt "Demo Corrupt Plugin"
|
||||
start_npm_fixture_registry "@openclaw/demo-corrupt-plugin" "0.0.1" /tmp/demo-corrupt-plugin.tgz "$npm_registry_dir"
|
||||
|
||||
echo "Installing managed external plugin..."
|
||||
node "$entry" plugins install "npm:@openclaw/demo-corrupt-plugin@0.0.1" >/tmp/openclaw-corrupt-plugin-install.log 2>&1
|
||||
node "$entry" plugins inspect demo-corrupt-plugin --runtime --json >/tmp/openclaw-corrupt-plugin-before.json
|
||||
unset NPM_CONFIG_REGISTRY npm_config_registry
|
||||
|
||||
plugin_dir="$(
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
const installPath = payload.install?.installPath ?? payload.plugin?.rootDir;
|
||||
if (!installPath) {
|
||||
throw new Error("missing plugin install path in inspect output");
|
||||
}
|
||||
process.stdout.write(installPath);
|
||||
' /tmp/openclaw-corrupt-plugin-before.json
|
||||
)"
|
||||
rm -f "$plugin_dir/package.json"
|
||||
if [ -f "$plugin_dir/package.json" ]; then
|
||||
echo "Expected corrupt plugin package.json to be removed before update." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updating OpenClaw with corrupt plugin present..."
|
||||
set +e
|
||||
node "$entry" update --channel beta --tag "${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}" --yes --no-restart --json >/tmp/openclaw-update-corrupt-plugin.json 2>/tmp/openclaw-update-corrupt-plugin.err
|
||||
update_status=$?
|
||||
set -e
|
||||
if [ "$update_status" -ne 0 ]; then
|
||||
if ! node scripts/e2e/lib/plugin-update/probe.mjs assert-legacy-post-update-plugin-failure /tmp/openclaw-update-corrupt-plugin.json; then
|
||||
echo "openclaw update failed with corrupt plugin present" >&2
|
||||
cat /tmp/openclaw-update-corrupt-plugin.err >&2 || true
|
||||
cat /tmp/openclaw-update-corrupt-plugin.json >&2 || true
|
||||
exit "$update_status"
|
||||
fi
|
||||
echo "Legacy updater reported post-update plugin failure after installing the new core; verifying updated entrypoint..."
|
||||
set +e
|
||||
OPENCLAW_UPDATE_POST_CORE=1 \
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL=beta \
|
||||
OPENCLAW_UPDATE_POST_CORE_RESULT_PATH=/tmp/openclaw-update-corrupt-plugin-post-core.json \
|
||||
node "$entry" update --yes --no-restart --json >/tmp/openclaw-update-corrupt-plugin-post-core.stdout 2>/tmp/openclaw-update-corrupt-plugin-post-core.err
|
||||
post_core_status=$?
|
||||
set -e
|
||||
if [ "$post_core_status" -ne 0 ]; then
|
||||
echo "updated OpenClaw entry failed post-core plugin verification" >&2
|
||||
cat /tmp/openclaw-update-corrupt-plugin-post-core.err >&2 || true
|
||||
cat /tmp/openclaw-update-corrupt-plugin-post-core.stdout >&2 || true
|
||||
cat /tmp/openclaw-update-corrupt-plugin-post-core.json >&2 || true
|
||||
exit "$post_core_status"
|
||||
fi
|
||||
node scripts/e2e/lib/plugin-update/probe.mjs assert-corrupt-plugin-result /tmp/openclaw-update-corrupt-plugin-post-core.json demo-corrupt-plugin
|
||||
exit 0
|
||||
fi
|
||||
|
||||
node scripts/e2e/lib/plugin-update/probe.mjs assert-corrupt-update /tmp/openclaw-update-corrupt-plugin.json demo-corrupt-plugin
|
||||
@@ -112,7 +112,79 @@ function assertOutput(logPath) {
|
||||
}
|
||||
}
|
||||
|
||||
const [command, arg] = process.argv.slice(2);
|
||||
function assertCorruptUpdate(updateJsonPath, pluginId) {
|
||||
const payload = readJson(updateJsonPath);
|
||||
if (payload.status !== "ok") {
|
||||
throw new Error(`expected core update status ok, got ${JSON.stringify(payload.status)}`);
|
||||
}
|
||||
const plugins = payload.postUpdate?.plugins;
|
||||
if (!plugins) {
|
||||
throw new Error(`missing postUpdate.plugins in update output: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
if (plugins.status !== "warning") {
|
||||
throw new Error(
|
||||
`expected post-update plugin status warning, got ${JSON.stringify(plugins.status)}`,
|
||||
);
|
||||
}
|
||||
assertCorruptPluginDetails(plugins, pluginId);
|
||||
}
|
||||
|
||||
function assertCorruptPluginResult(pluginJsonPath, pluginId) {
|
||||
const plugins = readJson(pluginJsonPath);
|
||||
if (plugins.status !== "warning") {
|
||||
throw new Error(
|
||||
`expected post-update plugin status warning, got ${JSON.stringify(plugins.status)}`,
|
||||
);
|
||||
}
|
||||
assertCorruptPluginDetails(plugins, pluginId);
|
||||
}
|
||||
|
||||
function assertCorruptPluginDetails(plugins, pluginId) {
|
||||
const outcomes = plugins.npm?.outcomes ?? [];
|
||||
const outcome = outcomes.find((entry) => entry?.pluginId === pluginId);
|
||||
if (!outcome || outcome.status !== "error") {
|
||||
throw new Error(
|
||||
`expected error outcome for ${pluginId}, got ${JSON.stringify({
|
||||
outcomes,
|
||||
warnings: plugins.warnings ?? [],
|
||||
sync: plugins.sync,
|
||||
integrityDrifts: plugins.integrityDrifts ?? [],
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
const warnings = plugins.warnings ?? [];
|
||||
const warning = warnings.find((entry) => entry?.pluginId === pluginId);
|
||||
if (!warning) {
|
||||
throw new Error(`expected warning for ${pluginId}, got ${JSON.stringify(warnings)}`);
|
||||
}
|
||||
const text = JSON.stringify({ outcome, warning });
|
||||
for (const expected of [
|
||||
"package.json is missing",
|
||||
"Run openclaw doctor --fix to attempt automatic repair.",
|
||||
`Run openclaw plugins inspect ${pluginId} --runtime --json for details.`,
|
||||
]) {
|
||||
if (!text.includes(expected)) {
|
||||
throw new Error(`expected update output to include ${expected}: ${text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertLegacyPostUpdatePluginFailure(updateJsonPath) {
|
||||
const payload = readJson(updateJsonPath);
|
||||
if (payload.status !== "error" || payload.reason !== "post-update-plugins") {
|
||||
throw new Error(
|
||||
`expected legacy post-update plugin failure, got ${JSON.stringify({
|
||||
status: payload.status,
|
||||
reason: payload.reason,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
if (!payload.after?.version) {
|
||||
throw new Error(`expected core update to install a new version: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const [command, arg, arg2] = process.argv.slice(2);
|
||||
const commands = {
|
||||
"legacy-compat": () => console.log(legacyPackageAcceptanceCompat(arg || "") ? "1" : "0"),
|
||||
seed: seedInstallState,
|
||||
@@ -120,6 +192,9 @@ const commands = {
|
||||
snapshot: () => process.stdout.write(JSON.stringify(pluginRecordSnapshot(), null, 2)),
|
||||
"assert-snapshot": () => assertSnapshot(arg),
|
||||
"assert-output": () => assertOutput(arg),
|
||||
"assert-corrupt-update": () => assertCorruptUpdate(arg, arg2),
|
||||
"assert-corrupt-plugin-result": () => assertCorruptPluginResult(arg, arg2),
|
||||
"assert-legacy-post-update-plugin-failure": () => assertLegacyPostUpdatePluginFailure(arg),
|
||||
};
|
||||
const run = commands[command];
|
||||
await (
|
||||
|
||||
30
scripts/e2e/update-corrupt-plugin-docker.sh
Normal file
30
scripts/e2e/update-corrupt-plugin-docker.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verifies `openclaw update` succeeds when a managed external plugin is corrupt.
|
||||
# The lane installs an older published OpenClaw package, corrupts an npm-managed
|
||||
# plugin payload, then updates to the prepared package artifact.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-update-corrupt-plugin-e2e" OPENCLAW_UPDATE_CORRUPT_PLUGIN_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_UPDATE_CORRUPT_PLUGIN_E2E_SKIP_BUILD:-0}"
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz update-corrupt-plugin "${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_build_or_reuse "$IMAGE_NAME" update-corrupt-plugin "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 update-corrupt-plugin empty)"
|
||||
|
||||
echo "Running corrupt plugin update tolerance E2E..."
|
||||
docker_e2e_run_with_harness \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENCLAW_SKIP_CHANNELS=1 \
|
||||
-e OPENCLAW_SKIP_PROVIDERS=1 \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
bash scripts/e2e/lib/plugin-update/corrupt-update-scenario.sh
|
||||
|
||||
echo "Corrupt plugin update tolerance Docker E2E passed."
|
||||
@@ -273,6 +273,15 @@ export const mainLanes = [
|
||||
npmLane("plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update", {
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
npmLane(
|
||||
"update-corrupt-plugin",
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-corrupt-plugin",
|
||||
{
|
||||
stateScenario: "empty",
|
||||
timeoutMs: 30 * 60 * 1000,
|
||||
weight: 3,
|
||||
},
|
||||
),
|
||||
npmLane(
|
||||
"plugin-lifecycle-matrix",
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-lifecycle-matrix",
|
||||
|
||||
Reference in New Issue
Block a user