openclaw/scripts/e2e/container-image-upgrade-docker.sh
Peter Steinberger 2a1402cd3e
fix(startup): keep legacy state repair in Doctor (#151025)
* fix(startup): keep legacy state repair in doctor

Separate current-state startup readiness from explicit Doctor migrations.
Preserve current config recovery, quarantine, device identity checks,
lease fencing and updater completion ownership.

Remove automatic startup migration/checkpoint and node-host import paths.
Keep shipped read-only context and roster projections unchanged.

* test(doctor): align proof callers with repair ownership

Keep survivor fixture setup in caller order and select the shared Doctor
flow separately from the channel-specific proof. Include the complete
isolated diagnostics dependency closure.

Prove ordinary startup preserves legacy directories before explicit
Doctor repair, and await SQLite worker closure before test cleanup.

* test(startup): verify index repair through gateway maintenance

* test(doctor): align cutover fixtures with state owners

Reacquire the database after Doctor retires its generation, retain real session history for startup refusal, and keep pending reads at their actual owners. Distinguish the reused readonly reader from independent snapshot-token children and verify every child settles.

* fix(doctor): preserve image activation and published migration receipts

Run the shared noninteractive Doctor owner before default and Compose Gateway
activation so retained Docker volumes keep their published upgrade path while
ordinary Gateway startup stays readiness-only. Preserve root selectors and
settle interrupted repair before executing the original command.

Retain the path-wide tombstones emitted by the published restart-sentinel
importer, including consumed notices, and validate completed source decisions
before retiring recreated inputs. Add the root-image activation lane and causal
receipt coverage without introducing a schema, option, or second importer.

* test(docker): preserve release state pairs in upgrade proof

Use the existing synthetic v2026.9.2 corpus instead of combining a July shared database with a later agent schema. Preserve its deletion journal, registry, both transcript payloads, paired backups, and unsafe-state controls. Register the two shell-launched helpers with Knip.

* test(docker): normalize persisted schema snapshot rows

Compare node:sqlite schema rows using the same plain-object representation as the saved JSON preimage. Keep strict schema, row, and value-type assertions intact.

* fix(models): retain discovered models after refresh failures

Record successful legacy catalog results at the producer boundary so unavailable refreshes retain the accepted inventory. Preserve explicit outcomes, advisory SDK fallback behavior, and first-discovery starter policy.

* fix(models): preserve skipped catalog outcome semantics

Mark bundled static, configured, and advisory catalog projections with
explicit empty outcomes so legacy success inference cannot promote them
to observed account inventory. Preserve live outcomes and helper types.

Keep exact auth provenance histories and move existing fixture/policy
code into focused owners where required by the line-cap ratchet.

Validation: 447 producer and sibling cases, 56 shared self-hosted cases,
95 auth/policy cases, causal missing-outcome failures, maintained checks,
and independent review.

* test(plugin-sdk): keep discovery loader types acyclic

Move the shared loader type into a leaf consumed by both discovery
contract helpers. Preserve its public provider-test-contracts export
without a child-to-parent type import cycle.

Validation: maintained Madge check reports zero cycles; core, all core
test graphs, extension test types, lint, formatting and independent
review pass. Runtime behavior and previous catalog proof are unchanged.

* fix(plugin-sdk): mark generated static catalogs explicitly

Keep the generated non-live, non-strict catalog adapter from claiming
successful acquisition for manifest or configured rows. Preserve null,
errors, strict and custom callbacks, static catalogs, and public types.

Validation: three existing controls fail before the correction; all49
owner and sibling cases pass afterward, with types, lint, line caps and
fresh independent review clean.

* test(gateway): cover restart import during state retirement

Exercise canonical database close while a legacy notice read is paused. Verify admission rejection, retained canonical and source bytes, no migration receipt, and joined maintenance custody without changing the accepted sidecar-stop drain contract.

* fix(test): drain sharing fixtures before removing state

* test: bind retirement regression to its own worker

* docs(docker): clarify automatic Doctor activation

* test(doctor): keep readiness fixture runtime private

* test: stabilize shared skill watcher fixture roots

(cherry picked from commit 15606be10ed61be99931fcf3bb300ebdb475ad89)

* perf(tooling): share indexed scope parsing for artifact scans

(cherry picked from commit 6e6eef9f5b)

* fix(team-reports): use source owners in scheduler tests

The source barrel retired in #157819, but the scheduler tests still imported
it, breaking the extension test typecheck on main. Import the Discord and
GitHub owners directly, matching the production caller.

Validated the original TS2307/TS7006 failure, the corrected extension type
graph, all 36 scheduler tests, changed checks, and independent P2 review.

(cherry picked from commit 7c4866c73b)

* test(install): isolate global npm configuration in version fixtures

Use a controlled absent global config inside the fixture home so the
release helper does not query the deliberately narrow npm stub. Keep
predecessor selection, fresh-install behavior, and expected exits intact.

(cherry picked from commit 0a9eefc76c82d81bd134add3ac3d269ae1b607bd)

* test(docker): verify the complete image activation entrypoint
2026-09-25 13:43:40 -07:00

142 lines
6.8 KiB
Bash

#!/usr/bin/env bash
# Bash 5.3+ can deadlock writing heredoc pipes on macOS before the reader starts.
if [[ ${OSTYPE:-} == darwin* && $BASH != /bin/bash ]] && ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3))); then
exec /bin/bash "$0" "$@"
fi
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SOURCE_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
RUN_ID="$(node -p 'require("node:crypto").randomUUID()')"
IMAGE_NAME="${OPENCLAW_CONTAINER_IMAGE_UPGRADE_IMAGE:-openclaw-container-image-upgrade:local}"
ARTIFACTS="$ROOT_DIR/.artifacts/docker-tests/container-image-upgrade-$RUN_ID"
mkdir -p "$ARTIFACTS"
CONTAINERS=()
ACTIVE_HELPER=""
VOLUMES=()
LABEL="org.openclaw.e2e.run"
cleanup() {
local result=$?
trap - EXIT
local name owner
if [ -n "$ACTIVE_HELPER" ]; then CONTAINERS+=("$ACTIVE_HELPER"); fi
for name in "${CONTAINERS[@]}"; do
if owner="$(docker_e2e_docker_cmd inspect -f "{{index .Config.Labels \"$LABEL\"}}" "$name" 2>/dev/null)"; then
if [ "$owner" != "$RUN_ID" ]; then
echo "Refusing cleanup of changed container $name" >&2
result=1
continue
fi
docker_e2e_docker_cmd logs "$name" >"$ARTIFACTS/$name.log" 2>&1 || true
docker_e2e_docker_cmd inspect "$name" >"$ARTIFACTS/$name.inspect.json" || true
docker_e2e_docker_cmd rm -f "$name" >/dev/null || result=1
else
echo "Container cleanup could not verify $name" >&2
result=1
fi
done
for name in "${VOLUMES[@]}"; do
owner="$(docker_e2e_docker_cmd volume inspect -f "{{index .Labels \"$LABEL\"}}" "$name")" || { result=1; continue; }
if [ "$owner" != "$RUN_ID" ]; then
echo "Refusing cleanup of changed volume $name" >&2
result=1
continue
fi
docker_e2e_docker_cmd volume rm "$name" >/dev/null || result=1
done
echo "Container image activation evidence: $ARTIFACTS"
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
# Root Dockerfile, not the bare/package E2E image: activation is the contract under test.
docker_e2e_build_or_reuse "$IMAGE_NAME" container-image-upgrade "$SOURCE_ROOT/Dockerfile" "$SOURCE_ROOT"
docker_e2e_docker_cmd image inspect "$IMAGE_NAME" >"$ARTIFACTS/image.json"
node "$ROOT_DIR/scripts/e2e/lib/container-image-upgrade/assert-launch.mjs" "$ARTIFACTS/image.json"
IMAGE_ID="$(docker_e2e_docker_cmd image inspect -f '{{.Id}}' "$IMAGE_NAME")"
for cell in default compose old-shape unsafe; do
name="openclaw-image-upgrade-$RUN_ID-$cell"
volume="$name-state"
if docker_e2e_docker_cmd volume inspect "$volume" >/dev/null 2>&1; then
echo "Refusing preexisting fixture volume $volume" >&2
exit 1
fi
docker_e2e_docker_cmd volume create --label "$LABEL=$RUN_ID" "$volume" >/dev/null
VOLUMES+=("$volume")
mounts=(
-v "$volume:/home/node/.openclaw"
-v "$ROOT_DIR/scripts/e2e/lib/container-image-upgrade/fixture.mjs:/proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs:ro"
-v "$ROOT_DIR/scripts/lib/sqlite-transcript-payload.mjs:/proof/scripts/lib/sqlite-transcript-payload.mjs:ro"
-v "$SOURCE_ROOT/test/fixtures/state-corpus/2026.9.2:/proof/state-corpus/2026.9.2:ro"
)
environment=(
-e HOME=/home/node -e OPENCLAW_STATE_DIR=/home/node/.openclaw
-e OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json
-e "OPENCLAW_GATEWAY_TOKEN=synthetic-container-$RUN_ID"
-e OPENCLAW_DISABLE_BONJOUR=1 -e OPENCLAW_DISABLE_BUNDLED_PLUGINS=1
)
# Generic helper commands keep the image entrypoint but do not invoke Doctor.
helper="$name-seed"
ACTIVE_HELPER="$helper"
docker_e2e_docker_cmd run --rm --name "$helper" --label "$LABEL=$RUN_ID" \
"${mounts[@]}" "${environment[@]}" "$IMAGE_ID" node /proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs seed "$cell" \
>"$ARTIFACTS/$cell-seed.json"
ACTIVE_HELPER=""
CONTAINERS+=("$name")
case "$cell" in
default | unsafe)
docker_e2e_docker_cmd run -d --name "$name" --label "$LABEL=$RUN_ID" \
"${mounts[@]}" "${environment[@]}" "$IMAGE_ID" >/dev/null ;;
compose)
docker_e2e_docker_cmd run -d --name "$name" --label "$LABEL=$RUN_ID" \
"${mounts[@]}" "${environment[@]}" "$IMAGE_ID" \
node dist/index.js gateway --bind lan --port 18789 >/dev/null ;;
old-shape)
# Counterfactual original04d0 activation shape on the final candidate image.
# This is the sole entrypoint override, not a separately built baseline image.
docker_e2e_docker_cmd run -d --name "$name" --label "$LABEL=$RUN_ID" \
"${mounts[@]}" "${environment[@]}" --entrypoint tini "$IMAGE_ID" \
-s -- node openclaw.mjs gateway >/dev/null ;;
esac
if [ "$cell" = default ] || [ "$cell" = compose ]; then
docker_e2e_wait_container_bash "$name" 180 1 'node /proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs ready'
docker_e2e_docker_cmd exec "$name" node /proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs ready >"$ARTIFACTS/$cell-ready.json"
docker_e2e_docker_cmd exec "$name" node /proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs history >"$ARTIFACTS/$cell-history.json"
docker_e2e_docker_cmd inspect "$name" >"$ARTIFACTS/$cell-launch.json"
node "$ROOT_DIR/scripts/e2e/lib/container-image-upgrade/assert-launch.mjs" \
"$ARTIFACTS/image.json" "$ARTIFACTS/$cell-launch.json" "$cell"
docker_e2e_docker_cmd stop --time 60 "$name" >/dev/null
docker_e2e_docker_cmd inspect "$name" >"$ARTIFACTS/$cell-stop.json"
node "$ROOT_DIR/scripts/e2e/lib/container-image-upgrade/assert-launch.mjs" \
"$ARTIFACTS/image.json" "$ARTIFACTS/$cell-stop.json" "$cell" "" stopped \
>"$ARTIFACTS/$cell-stop-assertion.json"
verify=migrated
else
# Require a natural refusal, not a timeout kill. Poll readiness while it can run.
for attempt in $(seq 1 180); do
if ! docker_e2e_container_running "$name"; then break; fi
if docker_e2e_docker_cmd exec "$name" node /proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs ready >/dev/null 2>&1; then
echo "Refusal control unexpectedly became ready: $cell" >&2
exit 1
fi
sleep 1
done
docker_e2e_docker_cmd logs "$name" >"$ARTIFACTS/$cell-refusal.log" 2>&1
docker_e2e_docker_cmd inspect "$name" >"$ARTIFACTS/$cell-launch.json"
node "$ROOT_DIR/scripts/e2e/lib/container-image-upgrade/assert-launch.mjs" \
"$ARTIFACTS/image.json" "$ARTIFACTS/$cell-launch.json" "$cell" "$ARTIFACTS/$cell-refusal.log"
verify="$cell"
fi
helper="$name-verify"
ACTIVE_HELPER="$helper"
docker_e2e_docker_cmd run --rm --name "$helper" --label "$LABEL=$RUN_ID" \
"${mounts[@]}" "${environment[@]}" "$IMAGE_ID" node /proof/scripts/e2e/lib/container-image-upgrade/fixture.mjs verify "$verify" \
>"$ARTIFACTS/$cell-state.json"
ACTIVE_HELPER=""
echo "Container image activation passed: $cell"
done