Files
ukmesh/scripts/test-replace-container.sh
T
b40793f36e Fix analysis state, worker observability, feed layout and maintenance (re-land onto main) (#114)
* Fix analysis state, worker observability, feed layout and maintenance (#113)

* fix(analysis): restore tested retired workload policy

* fix(owner): prewarm last-hop cache with bounded progress reporting

* fix(metrics): use real link worker heartbeat for queue alerts

* fix(feed): measure virtual rows and preserve scroll anchors

* fix(ops): reject Compose metadata drift before replacement

* fix(maintenance): archive inactive nodes across roles and bridge states

* docs(review): record validation and heartbeat audit findings

* fix(maintenance): retain private identities during stale cleanup

* fix(ops): consume Compose service output before adoption checks

* test(owner): cover prewarm scheduling recovery and shutdown

* test(e2e): isolate server ports and Vite dependency caches

* test(feed): cover empty filters and arrivals at the top

* test(map): budget two cold bootstraps in mode persistence check

* docs(review): record continuation regressions and final verification

* fix(alerts): match queue heartbeats to their scrape reporter

* test(feed): preserve packet anchors across scroll-mode changes

* test(owner): verify incremental prewarm cache refreshes

* fix(maintenance): preserve packet privacy fence during public cleanup

* fix(maintenance): recheck inactive sightings before archive and delete

* fix(owner): serve fresh cache hits during background prewarm

* test(map): give each viewport its own startup budget

* test(map): load valid terrain and coverage tiles in local fixtures

* fix(metrics): export unknown heartbeat when Redis initialization fails

* test(path): control worker-pool deadlines independently of thread startup

* test(e2e): await owner and map readiness before state assertions

* test(feed): check fractional viewport and spacer invariants

* docs(review): record green expanded verification checkpoint

* fix(maintenance): acquire visibility before node locks and skip busy rows

* test(maintenance): retain concurrent RF-only activity in native fixtures

* fix(owner): replace regrouped last-hop buckets during refresh

* test(owner): verify warm regrouping with actual last-hop SQL

* test(maintenance): retain concurrently committed node consent

* test(maintenance): avoid a waiting consent-trigger deadlock

* test(feed): verify responsive anchoring in Firefox and WebKit

* docs(review): reconcile completed checks and remote CI evidence

* docs(review): correct checkpoint wording

* test(maintenance): verify privacy fences on native PostgreSQL

* test(health): verify link heartbeat publisher lifecycle

* docs(review): record stop-signal handoff and remaining verification

---------

Co-authored-by: gadgethd <111318106+gadgethd@users.noreply.github.com>

* fix(feed): preserve scroll anchors across scroll-mode flips

React 19.3 fires the window scroll listener before React commits its new
row range, so updateViewport could overwrite the last known content offset
with the new-mode artifact (0) before the anchor-preserving layout effect
ran — dropping the feed to the top on a responsive resize.

Detect the scroll-mode flip (internal container <-> document scrolling) in
updateViewport and transfer the last known content offset into the new mode
before the anchor effect evaluates.

Verified: feed-virtualizer e2e (both viewports) and the full public-desktop
suite under react 19.2.8 and 19.3.0.

* fix(#553): sync replace-container test harness

* test(ops): sync replace-container harness with adoption + infra flows (#553)

Union of both branch evolutions: Compose adoption drills (check-compose-adoption
staging, MOCK_ADOPTION_* modes, adoption/long-service/overlay cases) plus the
infra-compose staging and timescaledb resolver drill assertions. Fixes the
Workers-and-Compose gate failing on this PR.

Verified locally: 16/16 drills pass.

---------

Co-authored-by: gadgethd <111318106+gadgethd@users.noreply.github.com>
Co-authored-by: hermes-gadget <284726080+hermes-gadget@users.noreply.github.com>
2026-10-03 17:14:34 +01:00

406 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
replace_script="${script_dir}/replace-container.sh"
source_revision="0123456789abcdef0123456789abcdef01234567"
desired_image="registry.example/meshcore-backend@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
prior_image="registry.example/meshcore-backend@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
test -x "$replace_script"
for command in jq openssl sha256sum; do
command -v "$command" >/dev/null
done
write_mock_commands() {
local fake_bin="$1"
cat >"${fake_bin}/cosign" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"$MOCK_COSIGN_LOG"
test "${1:-}" = "verify"
exit 0
EOF
cat >"${fake_bin}/git" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
case "${1:-} ${2:-}" in
"rev-parse HEAD")
printf '%s\n' "$MOCK_SOURCE_REVISION"
;;
"diff --quiet"|"diff --cached")
exit 0
;;
*)
printf 'unexpected git invocation: %s\n' "$*" >&2
exit 90
;;
esac
EOF
cat >"${fake_bin}/curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '{"status":"ready"}\n'
EOF
cat >"${fake_bin}/openssl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exit 0
EOF
cat >"${fake_bin}/docker" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"$MOCK_DOCKER_LOG"
command_name="${1:-}"
shift || true
case "$command_name" in
compose)
project_name=''
while [ "$#" -gt 0 ]; do
case "$1" in
--project-name)
project_name="$2"
shift 2
;;
--project-directory|-f)
shift 2
;;
config)
if [ "${2:-}" = "--services" ]; then
printf '%s\n' backend db-migrate timescaledb app-ukmesh
if [ "${MOCK_ADOPTION_MODE:-valid}" = long_service_list ]; then
# More than a pipe buffer: an early grep -q exit must not turn
# Compose's SIGPIPE into an incorrect unknown-service rejection.
for ((index = 0; index < 10000; index++)); do printf 'fixture-service-%s\n' "$index"; done
fi
elif [ "${2:-}" != "-q" ]; then
printf 'services: {}\n'
fi
exit 0
;;
ps)
shift
if [ "${1:-}" != "-q" ]; then
printf 'unexpected compose ps invocation: %s\n' "$*" >&2
exit 92
fi
service="${2:-}"
if [ "$service" = "timescaledb" ] || [ "$project_name" = 'meshcore-infra' ]; then
printf 'infra-timescaledb\n'
exit 0
fi
mode=valid
if [ "$service" = "$MOCK_ADOPTION_SERVICE" ]; then mode="$MOCK_ADOPTION_MODE"; fi
case "$mode" in
no_container) ;;
multiple_containers) printf 'current-%s\nother-%s\n' "$service" "$service" ;;
*) printf 'current-%s\n' "$service" ;;
esac
exit 0
;;
run)
test "${*: -1}" = "db-migrate"
exit 0
;;
exec)
if [[ "$*" == *"timescaledb"* ]]; then
printf '30\n'
elif [[ "$*" == *"/readyz"* ]]; then
printf '{"status":"ready"}\n'
elif [[ "$*" == *"/metrics"* ]]; then
printf 'meshcore_process_start_time_seconds 1\n'
else
printf 'unexpected docker compose exec invocation: %s\n' "$*" >&2
exit 91
fi
exit 0
;;
up)
if [ "${MOCK_FAIL_DESIRED_DEPLOY:-false}" = "true" ] \
&& [ "${BACKEND_IMAGE:-}" = "$MOCK_DESIRED_IMAGE" ]; then
exit 1
fi
exit 0
;;
*)
printf 'unexpected docker compose invocation: %s\n' "$*" >&2
exit 92
;;
esac
done
;;
pull)
;;
image)
test "${1:-}" = "inspect"
printf '%s\n' "$MOCK_SOURCE_REVISION"
;;
inspect)
case "$*" in
*'.Config.Image'*)
printf '%s\n' "$MOCK_PRIOR_IMAGE"
;;
*'.Config.Labels'*)
service="${1#current-}"
mode=valid
if [ "$service" = "$MOCK_ADOPTION_SERVICE" ]; then mode="$MOCK_ADOPTION_MODE"; fi
if [ "$mode" = malformed_labels ]; then printf 'invalid JSON\n'; exit 0; fi
jq -n --arg directory "$MOCK_PROJECT_DIR" --arg mode "$mode" --arg service "$service" '
{
"com.docker.compose.project": "meshcore-analytics",
"com.docker.compose.service": $service,
"com.docker.compose.project.working_dir": $directory,
"com.docker.compose.project.config_files": ($directory + "/docker-compose.yml")
}
| if $mode == "empty" then {}
elif $mode == "missing_config" then .["com.docker.compose.project.config_files"] = ""
elif $mode == "wrong_directory" then .["com.docker.compose.project.working_dir"] = "/wrong-source"
elif $mode == "wrong_project" then .["com.docker.compose.project"] = "wrong-project"
elif $mode == "wrong_service" then .["com.docker.compose.service"] = "wrong-service"
elif $mode == "null_labels" then null
elif $mode == "overlay_only" then .["com.docker.compose.project.config_files"] = ($directory + "/docker-compose.live.yml")
elif $mode == "overlay" then .["com.docker.compose.project.config_files"] += ("," + $directory + "/docker-compose.live.yml")
else . end'
;;
*'.Config.Env'*)
printf '%s\n' \
'DATABASE_URL=postgresql://meshcore:fixture@timescaledb:5432/meshcore' \
'REDIS_URL=redis://redis:6379'
;;
*'.NetworkSettings.Networks'*)
printf 'meshcore-net\n'
;;
*'.State.Status'*)
printf 'running\n'
;;
*'.State.Running'*)
printf 'true\n'
;;
*'.State.Health'*)
printf 'healthy\n'
;;
*)
printf 'unexpected docker inspect invocation: %s\n' "$*" >&2
exit 93
;;
esac
;;
run)
printf 'compat-container-id\n'
;;
exec)
if [[ "$*" == *"infra-timescaledb"*"psql"* ]]; then
printf '30\n'
exit 0
fi
if [ "${MOCK_COMPAT_READY:-true}" != "true" ]; then
exit 1
fi
if [[ "$*" == *"/readyz"* ]]; then
printf '{"status":"ready"}\n'
elif [[ "$*" == *"/api/stats"* ]]; then
printf '{"totalNodes":1}\n'
else
printf 'unexpected docker exec invocation: %s\n' "$*" >&2
exit 94
fi
;;
rm|logs)
;;
*)
printf 'unexpected docker invocation: %s\n' "$command_name $*" >&2
exit 95
;;
esac
EOF
chmod 0755 \
"${fake_bin}/cosign" \
"${fake_bin}/curl" \
"${fake_bin}/docker" \
"${fake_bin}/git" \
"${fake_bin}/openssl"
}
run_case() {
local case_name="$1"
local compat_ready="$2"
local fail_desired_deploy="$3"
local expected_status="$4"
local expected_up_count="$5"
local signature_mode="$6"
local adoption_mode="${7:-valid}"
local service="${8:-backend}"
local adoption_service="${9:-$service}"
local test_root
test_root="$(mktemp -d)"
trap 'rm -rf -- "$test_root"' RETURN
mkdir -p \
"${test_root}/project/scripts/lib" \
"${test_root}/infra" \
"${test_root}/fake-bin" \
"${test_root}/releases"
cp "$replace_script" "${test_root}/project/scripts/replace-container.sh"
if [ -f "${script_dir}/check-compose-adoption.sh" ]; then
cp "${script_dir}/check-compose-adoption.sh" "${test_root}/project/scripts/check-compose-adoption.sh"
fi
cp "${script_dir}/lib/infra-compose.sh" "${test_root}/project/scripts/lib/infra-compose.sh"
chmod 0755 "${test_root}/project/scripts/replace-container.sh"
printf 'services: {}\n' >"${test_root}/project/docker-compose.yml"
printf 'services: {}\n' >"${test_root}/infra/docker-compose.yml"
local now
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
jq -n \
--arg now "$now" \
'{
format:"meshcore-restore-receipt-v1",
receipt_id:"restore-fixture",
status:"verified",
backup_completed_at:$now,
restore_verified_at:$now,
datasets:["analytics","owner_auth","mosquitto","redis","configuration"]
}' >"${test_root}/restore-receipt.json"
: >"${test_root}/restore-receipt.json.sig"
: >"${test_root}/receipt-verify.pem"
: >"${test_root}/cosign.pub"
: >"${test_root}/cosign.log"
: >"${test_root}/docker.log"
write_mock_commands "${test_root}/fake-bin"
local cosign_public_key="${test_root}/cosign.pub"
local cosign_identity_regexp=""
local cosign_oidc_issuer=""
if [ "$signature_mode" = "keyless" ]; then
cosign_public_key=""
cosign_identity_regexp='^https://github.com/example/meshcore/.github/workflows/release.yml@'
cosign_oidc_issuer='https://token.actions.githubusercontent.com'
fi
set +e
PATH="${test_root}/fake-bin:${PATH}" \
MOCK_SOURCE_REVISION="$source_revision" \
MOCK_PROJECT_DIR="${test_root}/project" \
MOCK_ADOPTION_MODE="$adoption_mode" \
MOCK_ADOPTION_SERVICE="$adoption_service" \
MOCK_DESIRED_IMAGE="$desired_image" \
MOCK_PRIOR_IMAGE="$prior_image" \
MOCK_COMPAT_READY="$compat_ready" \
MOCK_FAIL_DESIRED_DEPLOY="$fail_desired_deploy" \
MOCK_DOCKER_LOG="${test_root}/docker.log" \
MOCK_COSIGN_LOG="${test_root}/cosign.log" \
COSIGN_PUBLIC_KEY="$cosign_public_key" \
COSIGN_CERTIFICATE_IDENTITY_REGEXP="$cosign_identity_regexp" \
COSIGN_CERTIFICATE_OIDC_ISSUER="$cosign_oidc_issuer" \
RESTORE_RECEIPT_PATH="${test_root}/restore-receipt.json" \
RESTORE_RECEIPT_VERIFY_KEY="${test_root}/receipt-verify.pem" \
RELEASE_STATUS_DIR="${test_root}/releases" \
MESHCORE_INFRA_PROJECT_DIR="${test_root}/infra" \
COMPATIBILITY_TIMEOUT_SECONDS=1 \
"${test_root}/project/scripts/replace-container.sh" \
"$service" \
"--image=${desired_image}" \
"--backend-image=${desired_image}" \
"--source-revision=${source_revision}" \
>"${test_root}/stdout.log" 2>"${test_root}/stderr.log"
local status=$?
set -e
if [ "$adoption_mode" != "valid" ] && [ "$adoption_mode" != "overlay" ] && [ "$adoption_mode" != "long_service_list" ]; then
if [ "$status" -ne 65 ]; then
printf '%s: expected Compose adoption rejection (65), got %s\n' "$case_name" "$status" >&2
return 1
fi
grep -q 'not Compose-adoptable' "${test_root}/stderr.log"
grep -q "service ${adoption_service} is not Compose-adoptable" "${test_root}/stderr.log"
test ! -s "${test_root}/cosign.log"
if grep -Eq '^(pull |compose .* (up|run) )' "${test_root}/docker.log"; then
echo 'adoption guard allowed a mutation before rejection' >&2
return 1
fi
test -z "$(find "${test_root}/releases" -type f -print -quit)"
printf '%s passed\n' "$case_name"
return 0
fi
if [ "$status" -eq 0 ]; then
printf '%s: expected a controlled failure\n' "$case_name" >&2
return 1
fi
local release_status
release_status="$(
find "${test_root}/releases" -maxdepth 1 -type f -name '*.json' \
-print -quit
)"
if [ -z "$release_status" ]; then
printf '%s: expected a controlled release receipt, got exit %s\n' "$case_name" "$status" >&2
cat "${test_root}/stderr.log" >&2
return 1
fi
test "$(jq -r '.status' "$release_status")" = "$expected_status"
test "$(jq -r '.schema_version' "$release_status")" = "30"
test "$(jq -r '.prior_image' "$release_status")" = "$prior_image"
local up_count
up_count="$(
grep -Ec '^compose --project-name meshcore-analytics up ' \
"${test_root}/docker.log" || true
)"
test "$up_count" -eq "$expected_up_count"
grep -Eq -- \
'^run .* --volumes-from current-backend:ro( |$)' \
"${test_root}/docker.log"
if grep -qF 'ps -q timescaledb' "${test_root}/docker.log"; then
local resolver_line migration_line
resolver_line="$(grep -nF 'ps -q timescaledb' "${test_root}/docker.log" | head -1 | cut -d: -f1)"
migration_line="$(grep -nF 'compose --project-name meshcore-analytics run --rm db-migrate' "${test_root}/docker.log" | head -1 | cut -d: -f1)"
test -n "$migration_line"
test "$resolver_line" -lt "$migration_line"
grep -Fq 'exec -i infra-timescaledb psql -U meshcore -d meshcore -Atc' "${test_root}/docker.log"
fi
if [ "$signature_mode" = "keyless" ]; then
grep -q -- '--certificate-identity-regexp' "${test_root}/cosign.log"
grep -q -- '--certificate-oidc-issuer' "${test_root}/cosign.log"
if grep -q -- '--key' "${test_root}/cosign.log"; then
echo "keyless verification unexpectedly used a public key" >&2
return 1
fi
else
grep -q -- '--key' "${test_root}/cosign.log"
fi
printf '%s passed\n' "$case_name"
}
run_case \
failed_deploy_rolls_back \
true \
true \
rolled_back \
2 \
public-key
run_case \
incompatible_schema_stops_before_deploy \
false \
false \
stopped \
0 \
keyless
for adoption_mode in empty missing_config wrong_directory wrong_project wrong_service no_container multiple_containers null_labels malformed_labels overlay_only; do
run_case "adoption_${adoption_mode}_stops_before_mutation" true false stopped 0 public-key "$adoption_mode"
done
run_case overlay_labels_remain_adoptable false false stopped 0 keyless overlay
run_case long_service_list_remains_adoptable false false stopped 0 keyless long_service_list
run_case invalid_frontend_stops_before_mutation true false stopped 0 public-key empty app-ukmesh
run_case valid_frontend_with_invalid_backend_stops_before_mutation true false stopped 0 public-key empty app-ukmesh backend
printf 'replace-container rollback, compatibility and Compose adoption drills passed (16/16)\n'