From 7c188a729bddb319114330e839dcaa9c885e88a0 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Sat, 1 Aug 2026 15:31:09 -0500 Subject: [PATCH] test: profile-map drift guard + reverse-switch coverage for profile deactivation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two stack-tier hardenings for the merged profile-deactivation fix: - test_compose.sh: enumerate every profile-gated service from docker-compose.yml (--profile '*') and diff against the service<->profile map parsed out of remove_deactivated_profile_containers in the pithead script. A new profile-gated service, a renamed profile, or a stale map entry now fails CI until both sides agree — same pattern as the depends_on count guard. - run.sh: black-box tari remote->local reverse switch through apply against the stubs. Asserts the reconcile removes only the still-off profiles (no tari in the rm list), the recreate up follows the reconcile, and local_tari is back in the committed profile list so that up brings the container up again. Closes #822 Co-Authored-By: Claude Fable 5 --- tests/stack/run.sh | 22 ++++++++++++++++++++++ tests/stack/test_compose.sh | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 7caa6698..5ce74bb0 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -3416,6 +3416,28 @@ printf '{ "monero": {"mode":"remote","remote":{"host":"node.example"},"wallet_ad out="$(cd "$V" && DOCKER_LOG="$SWLOG" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" assert_rc "monero local-to-remote switch applies cleanly" "$?" "0" assert_contains "switch removes the deactivated monerod container" "$(cat "$SWLOG")" "compose rm -sf monerod wallet-rpc tari-wallet" +# (4) Reverse switch, tari remote→local (#822): the freshly-reactivated profile's service must be +# left alone by the reconcile — only the still-off profiles (monerod stays remote, both payout +# wallets off) reconcile — and the recreate up must run with local_tari back in the committed +# profile list, which is what brings the tari container up again. First commit a tari-remote .env +# so the switch back is a real transition. +printf '{ "monero": {"mode":"remote","remote":{"host":"node.example"},"wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T","mode":"remote","remote":{"host":"tari.example.com"}}, "p2pool":{"pool":"main"}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$V/config.json" +out="$(cd "$V" && PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "tari remote baseline applies cleanly" "$?" "0" +assert_not_contains "tari remote baseline commits a profile list without local_tari" "$(grep '^COMPOSE_PROFILES=' "$V/.env")" "local_tari" +printf '{ "monero": {"mode":"remote","remote":{"host":"node.example"},"wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T","mode":"local"}, "p2pool":{"pool":"main"}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$V/config.json" +: >"$SWLOG" +out="$(cd "$V" && DOCKER_LOG="$SWLOG" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "tari remote-to-local switch applies cleanly" "$?" "0" +# The reconcile list is exactly the still-deactivated profiles — no tari between monerod and +# wallet-rpc (the list renders in fixed map order), so the reactivated service's container is +# never touched by the removal. +assert_contains "reverse switch reconciles only the still-off profiles" "$(cat "$SWLOG")" "compose rm -sf monerod wallet-rpc tari-wallet" +assert_not_contains "reverse switch leaves the reactivated tari alone" "$(cat "$SWLOG")" "rm -sf tari " +# And the recreate up runs AFTER that reconcile with local_tari committed back into the profile +# list — that up is what (re)creates the tari container. +assert_contains "recreate up follows the reconcile" "$(sed -n '/compose rm -sf monerod wallet-rpc tari-wallet/,$p' "$SWLOG")" "compose up --pull never -d --remove-orphans" +assert_contains "reactivated profile is live for the recreate up" "$(grep '^COMPOSE_PROFILES=' "$V/.env")" "local_tari" echo "== black-box: monero.rpc_lan_access + prep_blocks_threads reflect into .env (#523) ==" # The rendered .env must match the config input. rpc_lan_access gates the monerod RPC bind: default diff --git a/tests/stack/test_compose.sh b/tests/stack/test_compose.sh index a16c5dc7..027c8538 100755 --- a/tests/stack/test_compose.sh +++ b/tests/stack/test_compose.sh @@ -288,6 +288,24 @@ jq_assert "exactly 5 depends_on edges total (#565)" \ # runs fine (#777). Asserted here because tari-wallet only renders under tari_payout_confirm. jq_assert "tari-wallet healthcheck pattern survives ps CMD truncation (#777)" \ '(.services["tari-wallet"].healthcheck.test | tostring) | contains("[m]inotari_consol") and (contains("[m]inotari_console_wallet") | not)' +# Profile-map drift guard (#822, same pattern as the depends_on count guard above): compose never +# removes a profile-deactivated service's container (#795), so remove_deactivated_profile_containers +# in the pithead script is the only thing that does — and it hardcodes the service↔profile map. +# Enumerate every profile-gated service from the compose file itself (--profile '*' activates them +# all, whatever their names) and diff against the map parsed out of the script's function body: a +# new profile-gated service, a renamed profile, or a stale map entry fails here until both agree. +COMPOSE_PROFILE_MAP="$(docker compose --env-file "$ENV_FILE" --profile '*' -f "$ROOT/docker-compose.yml" config --format json 2>/dev/null | + jq -r '.services | to_entries[] | select(.value.profiles) | .value.profiles[] as $p | "\(.key)=\($p)"' | sort)" +SCRIPT_PROFILE_MAP="$(sed -n '/^remove_deactivated_profile_containers()/,/^}/p' "$ROOT/pithead" | + sed -n 's/.*\*,\([a-z_]*\),\* ]] || gone+=(\([a-z-]*\)).*/\2=\1/p' | sort)" +if [ -n "$COMPOSE_PROFILE_MAP" ] && [ "$COMPOSE_PROFILE_MAP" = "$SCRIPT_PROFILE_MAP" ]; then + echo " ✓ removal map covers exactly the profile-gated services (#795/#822)" +else + echo " ✗ compose profiles and remove_deactivated_profile_containers disagree (#795/#822):" + echo " compose file: $(printf '%s' "$COMPOSE_PROFILE_MAP" | tr '\n' ' ')" + echo " pithead map: $(printf '%s' "$SCRIPT_PROFILE_MAP" | tr '\n' ' ')" + fails=$((fails + 1)) +fi # Restore $JSON to the default-profile render for every check below this point. JSON="$(docker compose --env-file "$ENV_FILE" -f "$ROOT/docker-compose.yml" config --format json 2>/dev/null)"