#!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." source build.sh fail() { echo "test_full_infrastructure_selection: $*" >&2; exit 1; } # Resolve real board inheritance, but never compile or flash firmware. init_project_context >/dev/null # No fixture assertion is allowed to fall through into a firmware build. The # sole PlatformIO operation above resolves board metadata; all later calls are # errors, including dependency preflight before the compiler entry point. pio() { fail "selection fixture attempted PlatformIO after metadata resolution"; } prepare_esp32_arduino3_framework() { fail "selection fixture reached build preflight"; } run_pio_with_size_detection() { fail "selection fixture attempted compilation"; } collect_build_artifacts() { fail "selection fixture attempted artifact publication"; } use_qualified_resume_fixture() { fixture_output_dir=$(mktemp -d "${TMPDIR:-/tmp}/mesh-full-selection.XXXXXX") OUTPUT_DIR=$fixture_output_dir trap 'rm -rf -- "$fixture_output_dir"' EXIT # These tests mock package qualification to inspect target selection. Supply # a real, schema-valid receipt for the new recipe gate too; its actual # occupied/matches helpers run normally. Recipe hashing and package sealing # are exercised independently by test_firmware_build_recipe.py. compute_build_recipe_digest() { python3 - "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" "$1" "$3" <<'PY' import json from pathlib import Path import sys digest = "a" * 64 manifest = {"target": sys.argv[2], "platformio_env": sys.argv[3], "build_recipe": {"schema_version": 1, "sha256": digest}} Path(sys.argv[1]).write_text(json.dumps(manifest) + "\n") print(digest) PY } } assert_auto_unified() ( local plain=$1 expected=$2 BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 SINGLE_TARGET_FULL_BUILD=0 RESOLVED_BUILD_TARGETS=("$plain") configure_effective_build_profile build-firmware >/dev/null [ "${RESOLVED_BUILD_TARGETS[*]}" = "$expected" ] || fail "$plain selected the wrong target" [ "$SINGLE_TARGET_FULL_BUILD" = 1 ] || fail "$plain bypassed the unified recipe" [ "$AUTO_PREFER_FULL_BUILD" = 0 ] || fail "$plain still schedules a plain Full artifact" local -a built=() run_logged_build_targets() { [ "$FIRMWARE_FILENAME_INFIX" = full-usb-wifi ] || fail "wrong artifact name" [ "$ESP32_FULL_BUILD" = 1 ] || fail "lost expanded partitions" [ "$PACKET_LOGGING_OVERRIDE" = on ] || fail "lost USB packet logging" [ "$MQTT_BRIDGE_OVERRIDE" = on ] || fail "lost MQTT" [ "$MESHDEBUG_OVERRIDE" = off ] || fail "enabled verbose debug" built+=("$@") } run_command build-firmware >/dev/null [ "${built[*]}" = "$expected" ] || fail "auto emitted duplicate Full artifacts" ) assert_auto_unified heltec_v4_r8_repeater heltec_v4_r8_repeater_observer_mqtt assert_auto_unified heltec_v4_r8_room_server heltec_v4_r8_room_server_observer_mqtt assert_auto_unified heltec_v4_r8_tft_repeater heltec_v4_r8_tft_repeater_observer_mqtt assert_auto_unified Heltec_T190_repeater_ Heltec_T190_repeater_observer_mqtt assert_auto_unified LilyGo_TLora_V2_1_1_6_room_server LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt_ assert_auto_unified heltec_v4_r8_repeater_observer_mqtt heltec_v4_r8_repeater_observer_mqtt assert_auto_unified Station_G2_repeater Station_G2_repeater_observer_mqtt assert_auto_unified Station_G2_room_server Station_G2_room_server_observer_mqtt # A one-board migration package must retain the old repeater's mOTA identity, # even when its Full sources come from the matching observer environment. ( BUILD_PROFILE_OVERRIDE=full BUILD_PROFILE_EXPLICIT=1 SINGLE_TARGET_FULL_BUILD=1 EXACT_IDENTITY_FULL_BUILD=1 RESOLVED_BUILD_TARGETS=(heltec_v4_repeater) configure_effective_build_profile build-firmware >/dev/null [ "${RESOLVED_BUILD_TARGETS[*]}" = heltec_v4_repeater ] \ || fail "exact Full target changed identity" run_logged_build_targets() { [ "${*}" = heltec_v4_repeater ] || fail "exact Full built a different target" [ "$ESP32_FULL_BUILD" = 1 ] || fail "exact Full lost expanded partitions" } run_command build-firmware >/dev/null ) # Operators may still build an alternate Full identity that is omitted from # the ordinary bulk release, including capacity-specific plain recipes. for alternate in Tbeam_SX1262_repeater \ Tbeam_SX1262_repeater_bridge_espnow \ LilyGo_TLora_V2_1_1_6_repeater; do ( BUILD_PROFILE_OVERRIDE=full BUILD_PROFILE_EXPLICIT=1 SINGLE_TARGET_FULL_BUILD=1 EXACT_IDENTITY_FULL_BUILD=1 RESOLVED_BUILD_TARGETS=("$alternate") configure_effective_build_profile build-firmware >/dev/null [ "${RESOLVED_BUILD_TARGETS[*]}" = "$alternate" ] \ || fail "alternate Full target changed identity: $alternate" run_logged_build_targets() { [ "$*" = "$alternate" ] || fail "alternate Full built another target" [ "$ESP32_FULL_BUILD" = 1 ] || fail "alternate Full lost expanded partitions" } run_command build-firmware >/dev/null ) done # Only targets whose standard and FULL board recipes retain the exact same # partition table may omit their redundant portable bulk artifact. full_only_count=0 for target in "${!PIO_ENV_PLATFORM_BY_NAME[@]}"; do if is_esp32_full_only_bulk_target "$target"; then full_only_count=$((full_only_count + 1)) fi done [ "$full_only_count" -eq 149 ] || fail "expected 149 same-partition FULL-only targets, found $full_only_count" for target in heltec_rc32_repeater Station_G2_repeater_observer_mqtt \ Heltec_v3_repeater heltec_v4_r8_repeater \ heltec_v4_tft_repeater RAK_3112_repeater \ Station_G3_ESP32_sensor Station_G3_ESP32_r2_sensor; do is_esp32_full_only_bulk_target "$target" || fail "missed same-partition FULL-only target $target" done for target in LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_ \ LilyGo_TLora_V2_1_1_6_companion_radio_wifi \ LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow WHY2025_badge_repeater_; do if is_esp32_full_only_bulk_target "$target"; then fail "consolidated target with a partition migration: $target" fi done # Partition-changing normal roles publish a canonical Full image alongside # their portable compatibility image. They must never be mistaken for the # same-partition policy, which is the only policy allowed to install through # the automatic mOTA target alias. for target in LilyGo_T3S3_sx1262_repeater \ LilyGo_T3S3_sx1262_room_server \ LilyGo_T3S3_sx1276_repeater \ LilyGo_T3S3_sx1276_room_server \ Station_G2_repeater Station_G2_room_server \ ThinkNode_M2_Repeater ThinkNode_M2_room_server; do is_esp32_partition_migration_full_target "$target" \ || fail "missed partition-migration Full target $target" if is_esp32_full_only_bulk_target "$target"; then fail "partition-migration target was incorrectly made FULL-only: $target" fi done [ "$(get_exact_identity_full_pio_env heltec_v4_r8_repeater)" \ = heltec_v4_r8_repeater_observer_mqtt ] \ || fail "V4 R8 Full lost its observer feature base" [ "$(get_exact_identity_full_pio_env heltec_v4_r8_sensor)" \ = heltec_v4_r8_sensor ] \ || fail "V4 R8 sensor should retain its own feature base" [ "$(get_exact_identity_full_pio_env Station_G2_repeater)" \ = Station_G2_repeater_observer_mqtt ] \ || fail "Station G2 migration Full lost its observer feature base" [ "$(get_exact_identity_full_pio_env LilyGo_T3S3_sx1262_room_server)" \ = LilyGo_T3S3_sx1262_room_server_observer_mqtt ] \ || fail "T3S3 migration Full lost its observer feature base" [ "$(get_exact_identity_full_migration_target heltec_v4_r8_repeater_observer_mqtt)" \ = heltec_v4_r8_repeater ] \ || fail "legacy V4 R8 observer lacks its canonical migration target" if get_exact_identity_full_migration_target heltec_v4_r8_repeater >/dev/null; then fail "ordinary V4 R8 target unexpectedly has a migration alias" fi if get_exact_identity_full_migration_target heltec_v4_r8_repeater_observer_mqtt_sim >/dev/null; then fail "simulated observer target unexpectedly received a migration alias" fi if get_exact_identity_full_migration_target Station_G2_repeater_observer_mqtt >/dev/null; then fail "partition-changing Station G2 observer unexpectedly received an mOTA alias" fi # Portable images and exact reduced OTA identities must not be consolidated. for target in heltec_v4_r8_repeater heltec_v4_r8_room_server \ heltec_v4_r8_repeater_lora_ota_no_external_sensors; do ( BUILD_PROFILE_OVERRIDE=standard BUILD_PROFILE_EXPLICIT=1 SINGLE_TARGET_FULL_BUILD=0 RESOLVED_BUILD_TARGETS=("$target") configure_effective_build_profile build-firmware >/dev/null [ "${RESOLVED_BUILD_TARGETS[*]}" = "$target" ] || fail "lost portable target $target" [ "$BUILD_PROFILE_EFFECTIVE" = standard ] || fail "expanded portable image $target" [ "$SINGLE_TARGET_FULL_BUILD" = 0 ] || fail "portable image entered Full pipeline" ) done for target in Tbeam_SX1262_repeater Tbeam_SX1276_repeater \ Tbeam_SX1262_room_server Tbeam_SX1276_room_server \ LilyGo_TLora_V2_1_1_6_repeater \ heltec_v4_r8_repeater_lora_ota_no_external_sensors \ RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors; do if get_unified_full_infrastructure_target "$target" >/dev/null; then fail "consolidated a distinct capacity/OTA contract: $target" fi done ( PIO_ENV_BOARD_BY_NAME[heltec_v4_r8_repeater_observer_mqtt]=different_board if get_unified_full_infrastructure_target heltec_v4_r8_repeater >/dev/null; then fail "accepted a different physical board" fi ) # Direct observer promotion must use the matrix's exact output recipe even if # interactive flags previously requested MQTT without USB logging. Stop at the # matching-recipe/qualified-artifact check without entering compilation. ( use_qualified_resume_fixture ESP32_FULL_BUILD=0 BUILD_PROFILE_EFFECTIVE=standard BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 FIRMWARE_VERSION=vtest RESUME_BUILD_OUTPUT=1 FIRMWARE_FILENAME_INFIX="" MESHDEBUG_OVERRIDE=on PACKET_LOGGING_OVERRIDE=off MQTT_BRIDGE_OVERRIDE=off MQTT_DEBUG_OVERRIDE=on observed_filename="" observed_output="" build_artifacts_exist() { observed_filename=$3 observed_output="$PACKET_LOGGING_OVERRIDE/$MQTT_BRIDGE_OVERRIDE/$MESHDEBUG_OVERRIDE/$MQTT_DEBUG_OVERRIDE" return 0 } build_firmware heltec_v4_r8_repeater_observer_mqtt >/dev/null [[ "$observed_filename" == heltec_v4_r8_repeater_observer_mqtt-full-usb-wifi-ota-vtest-* ]] \ || fail "direct observer kept a duplicate name" [ "$observed_output" = on/on/off/off ] || fail "direct observer differs from unified recipe" [ "$PACKET_LOGGING_OVERRIDE/$MQTT_BRIDGE_OVERRIDE/$MESHDEBUG_OVERRIDE/$MQTT_DEBUG_OVERRIDE" = off/off/on/on ] \ || fail "observer output settings leaked into subsequent builds" ) # A canonical bulk build promotes an audited target in-place, retaining its # environment/mOTA identity. Explicit standard builds remain untouched above. ( use_qualified_resume_fixture ESP32_FULL_BUILD=0 BUILD_PROFILE_EFFECTIVE=standard BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 BATCH_BUILD_MODE=1 FIRMWARE_VERSION=vtest RESUME_BUILD_OUTPUT=1 FIRMWARE_FILENAME_INFIX="" observed_filename="" observed_full=0 build_artifacts_exist() { observed_filename=$3 observed_full=$ESP32_FULL_BUILD return 0 } build_firmware heltec_rc32_repeater >/dev/null [ "$observed_full" = 1 ] || fail "audited bulk target did not promote to FULL" [[ "$observed_filename" == heltec_rc32_repeater-full-usb-wifi-ota-vtest-* ]] \ || fail "FULL-only bulk target lost its exact identity" ) ( use_qualified_resume_fixture ESP32_FULL_BUILD=0 BUILD_PROFILE_EFFECTIVE=standard BUILD_PROFILE_OVERRIDE=auto BUILD_PROFILE_EXPLICIT=0 BATCH_BUILD_MODE=1 FIRMWARE_VERSION=vtest RESUME_BUILD_OUTPUT=1 FIRMWARE_FILENAME_INFIX="" observed_filename="" build_artifacts_exist() { observed_filename=$3 return 0 } build_firmware heltec_v4_r8_repeater >/dev/null [[ "$observed_filename" == heltec_v4_r8_repeater-full-usb-wifi-ota-vtest-* ]] \ || fail "V4 Full did not keep its identity and unified feature recipe" ) ( use_qualified_resume_fixture ESP32_FULL_BUILD=0 BUILD_PROFILE_EFFECTIVE=standard BUILD_PROFILE_OVERRIDE=standard BUILD_PROFILE_EXPLICIT=1 BATCH_BUILD_MODE=1 FIRMWARE_VERSION=vtest RESUME_BUILD_OUTPUT=1 observed_full=1 build_artifacts_exist() { observed_full=$ESP32_FULL_BUILD return 0 } build_firmware heltec_rc32_repeater >/dev/null [ "$observed_full" = 0 ] || fail "explicit standard recovery build was promoted" ) ( standard_built="" full_built="" REQUIRE_OTA_UPDATES=0 run_logged_build_targets() { if [ "$ESP32_FULL_BUILD" = 0 ]; then standard_built="$*"; else full_built="$*"; fi } run_logging_matrix_build_targets heltec_v4_r8_repeater \ heltec_v4_r8_repeater_lora_ota_no_external_sensors \ heltec_v4_r8_repeater_observer_mqtt >/dev/null [ "$standard_built" = heltec_v4_r8_repeater_lora_ota_no_external_sensors ] \ || fail "matrix changed the distinct reduced OTA build" [ "$full_built" = heltec_v4_r8_repeater ] \ || fail "matrix emitted duplicate V4 Full identities" [ "$ESP32_LORA_OTA_APP_LIMIT" = 1310720 ] || fail "changed the 1.25 MiB portable limit" ) ( calls=() REQUIRE_OTA_UPDATES=0 run_logged_build_targets() { calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") } run_logging_matrix_build_targets heltec_rc32_repeater \ heltec_rc32_repeater_bridge_espnow >/dev/null [ "${#calls[@]}" -eq 1 ] || fail "FULL-only targets emitted duplicate matrix artifacts" [ "${calls[0]}" = 'full:1:full-logging:heltec_rc32_repeater' ] \ || fail "matrix emitted duplicate RC32 Full identities" ) # Canonical board/role identities share one exact Full pass. A group without # an audited plain Full keeps its feature-complete observer identity; legacy # observer/ESP-NOW aliases must not create duplicate builds or lose a role. ( calls=() run_logged_build_targets() { [ "$PACKET_LOGGING_OVERRIDE" = on ] || fail "multi-role Full lost packet logging" [ "$MQTT_BRIDGE_OVERRIDE" = off ] || fail "multi-role Full changed its exact-identity recipe" [ "$MESHDEBUG_OVERRIDE" = off ] || fail "multi-role Full enabled verbose debug" calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") } run_logging_matrix_build_targets Heltec_v3_repeater \ Heltec_v3_repeater_observer_mqtt Heltec_v3_repeater_bridge_espnow \ Tbeam_SX1262_repeater_bridge_espnow Tbeam_SX1262_repeater_observer_mqtt \ Heltec_v3_room_server >/dev/null [ "${#calls[@]}" -eq 1 ] || fail "multi-role matrix did not emit one exact Full pass" [ "${calls[0]}" = 'full:1:full-logging:Heltec_v3_repeater Tbeam_SX1262_repeater_observer_mqtt Heltec_v3_room_server' ] \ || fail "multi-role Full changed canonical identities, roles, order, or profile" ) ( mapfile -t matrix_targets < <(resolve_logging_matrix_firmwares) mapfile -t full_targets < <(select_ordinary_full_targets "${matrix_targets[@]}") declare -A seen=() for target in "${full_targets[@]}"; do key=$(get_ordinary_full_group_key "$target") || fail "invalid Full target $target" [ -z "${seen[$key]+x}" ] || fail "duplicate ordinary Full group $key" seen[$key]=1 done [ "${#full_targets[@]}" -gt 100 ] || fail "Full selection unexpectedly dropped board roles" ) ( calls=() run_logged_build_targets() { [ "$PACKET_LOGGING_OVERRIDE" = on ] || fail "Full-only bulk lost packet logging" [ "$MQTT_BRIDGE_OVERRIDE" = off ] || fail "Full-only bulk changed its exact-identity recipe" [ "$MESHDEBUG_OVERRIDE" = off ] || fail "Full-only bulk enabled verbose debug" calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") } run_full_esp32_build_targets all heltec_v4_r8_repeater \ heltec_v4_r8_repeater_observer_mqtt \ Tbeam_SX1262_repeater_bridge_espnow \ Tbeam_SX1262_repeater_observer_mqtt >/dev/null [ "${#calls[@]}" -eq 1 ] || fail "Full-only bulk did not emit one exact Full pass" [ "${calls[0]}" = 'full:1:full-logging:heltec_v4_r8_repeater Tbeam_SX1262_repeater_observer_mqtt' ] \ || fail "Full-only bulk changed canonical identities, order, or profile" ) ( calls=() run_logged_build_targets() { calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") } run_logging_matrix_build_targets Station_G2_repeater \ Station_G2_repeater_observer_mqtt >/dev/null full_calls=() for call in "${calls[@]}"; do [[ "$call" == full:1:* ]] && full_calls+=("$call") done [ "${#full_calls[@]}" -eq 1 ] || fail "G2 repeater matrix emitted two Full images" [[ "${full_calls[0]}" == *:Station_G2_repeater_observer_mqtt ]] \ || fail "G2 repeater matrix lost the observer OTA identity" ) ( calls=() run_logged_build_targets() { calls+=("$BUILD_PROFILE_EFFECTIVE:$ESP32_FULL_BUILD:$FIRMWARE_FILENAME_INFIX:$*") } run_partition_migration_full_esp32_profile \ Station_G2_repeater Station_G2_room_server ThinkNode_M2_Repeater >/dev/null [ "${#calls[@]}" -eq 1 ] || fail "partition-migration pass emitted the wrong number of builds" [ "${calls[0]}" = 'full:1:full-logging:ThinkNode_M2_Repeater' ] \ || fail "partition-migration pass emitted duplicate G2 Full identities" ) # The default build settings retain power saving; this does not overwrite # preferences already stored on a device. [ "$FIRMWARE_PROFILE_OVERRIDE" = cascade ] || fail "default profile changed" PLATFORMIO_BUILD_FLAGS="" apply_firmware_profile_overrides [[ "$PLATFORMIO_BUILD_FLAGS" == *-DDEFAULT_POWERSAVING_ENABLED=1* ]] || fail "device power saving disabled" [[ "$PLATFORMIO_BUILD_FLAGS" == *-DDEFAULT_RXPS_ENABLED=1* ]] || fail "RX power saving disabled" echo "test_full_infrastructure_selection: OK"