From 0ed6f4997a883d7472ee43f041f3d921dec4111c Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Tue, 24 Mar 2026 12:49:35 +0100 Subject: [PATCH 1/5] test: extend smoke tests for v0.4.0 features and missing API coverage Add smoke test assertions for new gateway v0.4.0 features across all 3 demos (sensor, moveit, turtlebot3): - Linux introspection (procfs plugin data per entity) - Scripts API (list scripts, execute, poll completion) - Triggers CRUD (create, list, verify, delete) for moveit/turtlebot - Beacon discovery (sensor demo, conditional on BEACON_MODE) Fill gaps in pre-v0.4.0 API coverage: - Functions entity discovery (all demos) - Discovery relationships (areas -> components) - Operations endpoint (via medkit-fault-manager) - Data access and configurations (moveit, turtlebot) - Bulk data endpoint (all demos) - Faults listing (moveit, turtlebot) Add reusable helpers to smoke_lib.sh: - assert_procfs_introspection - assert_scripts_list / assert_script_execution - assert_triggers_crud Tested against sensor_diagnostics demo: 40/40 pass. Closes #45 --- tests/smoke_lib.sh | 172 +++++++++++++++++++++++++++++++++ tests/smoke_test.sh | 56 +++++++++++ tests/smoke_test_moveit.sh | 46 +++++++++ tests/smoke_test_turtlebot3.sh | 46 +++++++++ 4 files changed, 320 insertions(+) diff --git a/tests/smoke_lib.sh b/tests/smoke_lib.sh index 30e930d..8e2ee2c 100755 --- a/tests/smoke_lib.sh +++ b/tests/smoke_lib.sh @@ -162,6 +162,178 @@ test_entity_discovery() { fi } +# Test that procfs introspection data is available for an app +# Usage: assert_procfs_introspection "lidar-sim" +assert_procfs_introspection() { + local app_id="$1" + local endpoint="/apps/${app_id}/x-medkit-procfs" + if api_get "$endpoint"; then + if echo "$RESPONSE" | jq -e '.pid' > /dev/null 2>&1; then + pass "GET ${endpoint} returns procfs data with pid" + else + fail "GET ${endpoint} returns procfs data with pid" "pid field missing" + fi + else + fail "GET ${endpoint} returns 200" "unexpected status code" + fi +} + +# Test that scripts are listed for a component +# Usage: assert_scripts_list "compute-unit" "run-diagnostics" +assert_scripts_list() { + local component_id="$1" + local expected_script="$2" + local endpoint="/components/${component_id}/scripts" + if api_get "$endpoint"; then + if echo "$RESPONSE" | jq -e '.items | length > 0' > /dev/null 2>&1; then + pass "GET ${endpoint} returns non-empty items" + else + fail "GET ${endpoint} returns non-empty items" "items is empty" + fi + if echo "$RESPONSE" | jq -e --arg s "$expected_script" '.items[] | select(.id == $s)' > /dev/null 2>&1; then + pass "scripts contains '${expected_script}'" + else + fail "scripts contains '${expected_script}'" "not found in response" + fi + else + fail "GET ${endpoint} returns 200" "unexpected status code" + fi +} + +# Execute a script and verify it completes +# Usage: assert_script_execution "compute-unit" "run-diagnostics" [max_wait] +assert_script_execution() { + local component_id="$1" + local script_id="$2" + local max_wait="${3:-30}" + local exec_endpoint="/components/${component_id}/scripts/${script_id}/executions" + + # Start execution + local exec_response + exec_response=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}${exec_endpoint}" \ + -H "Content-Type: application/json" \ + -d '{"execution_type": "now"}' 2>/dev/null) || true + local exec_http + exec_http=$(echo "$exec_response" | tail -1) + local exec_body + exec_body=$(echo "$exec_response" | sed '$d') + + if [ "$exec_http" != "201" ] && [ "$exec_http" != "200" ] && [ "$exec_http" != "202" ]; then + fail "POST ${exec_endpoint} starts execution" "got HTTP ${exec_http}" + return + fi + pass "POST ${exec_endpoint} starts execution" + + local exec_id + exec_id=$(echo "$exec_body" | jq -r '.id') + if [ -z "$exec_id" ] || [ "$exec_id" = "null" ]; then + fail "script execution returns valid id" "id is null or empty" + return + fi + pass "script execution returns valid id" + + # Poll until completed + echo " Waiting for script '${script_id}' to complete (max ${max_wait}s)..." + local elapsed=0 + while [ $elapsed -lt "$max_wait" ]; do + if api_get "${exec_endpoint}/${exec_id}"; then + local status + status=$(echo "$RESPONSE" | jq -r '.status') + case "$status" in + completed) + pass "script '${script_id}' completed successfully" + return + ;; + failed|terminated) + fail "script '${script_id}' completed successfully" "status: ${status}" + return + ;; + esac + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + fail "script '${script_id}' completed successfully" "timed out after ${max_wait}s" +} + +# Test trigger CRUD lifecycle on an entity +# Usage: assert_triggers_crud "apps" "diagnostic-bridge" "/api/v1/apps/diagnostic-bridge/faults" +assert_triggers_crud() { + local entity_type="$1" + local entity_id="$2" + local resource_uri="$3" + local triggers_endpoint="/${entity_type}/${entity_id}/triggers" + + # Create trigger + echo " Creating OnChange trigger on ${entity_type}/${entity_id}..." + local create_response + create_response=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}${triggers_endpoint}" \ + -H "Content-Type: application/json" \ + -d "{\"resource\":\"${resource_uri}\",\"trigger_condition\":{\"condition_type\":\"OnChange\"},\"multishot\":true,\"lifetime\":60}" 2>/dev/null) || true + + local create_http + create_http=$(echo "$create_response" | tail -1) + local create_body + create_body=$(echo "$create_response" | sed '$d') + + if [ "$create_http" = "201" ]; then + pass "POST ${triggers_endpoint} returns 201" + else + fail "POST ${triggers_endpoint} returns 201" "got HTTP ${create_http}" + return + fi + + local trigger_id + trigger_id=$(echo "$create_body" | jq -r '.id') + if [ -n "$trigger_id" ] && [ "$trigger_id" != "null" ]; then + pass "trigger response contains valid id" + else + fail "trigger response contains valid id" "id is null or empty" + return + fi + + local trigger_status + trigger_status=$(echo "$create_body" | jq -r '.status') + if [ "$trigger_status" = "active" ]; then + pass "trigger status is 'active'" + else + fail "trigger status is 'active'" "got '${trigger_status}'" + fi + + # List triggers - verify it appears + if api_get "${triggers_endpoint}"; then + if echo "$RESPONSE" | jq -e --arg id "$trigger_id" '.items[] | select(.id == $id)' > /dev/null 2>&1; then + pass "GET ${triggers_endpoint} lists created trigger" + else + fail "GET ${triggers_endpoint} lists created trigger" "trigger ${trigger_id} not found" + fi + else + fail "GET ${triggers_endpoint} returns 200" "unexpected status code" + fi + + # Delete trigger + local delete_status + delete_status=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + "${API_BASE}${triggers_endpoint}/${trigger_id}" 2>/dev/null) || true + + if [ "$delete_status" = "204" ]; then + pass "DELETE trigger ${trigger_id} returns 204" + else + fail "DELETE trigger ${trigger_id} returns 204" "got HTTP ${delete_status}" + fi + + # Verify trigger is gone + if api_get "${triggers_endpoint}"; then + if ! echo "$RESPONSE" | jq -e --arg id "$trigger_id" '.items[] | select(.id == $id)' > /dev/null 2>&1; then + pass "trigger no longer listed after deletion" + else + fail "trigger no longer listed after deletion" "still found in list" + fi + else + fail "GET ${triggers_endpoint} returns 200 after delete" "unexpected status code" + fi +} + # Print test summary and exit with appropriate code print_summary() { echo "" diff --git a/tests/smoke_test.sh b/tests/smoke_test.sh index 1c991ce..3ea07e9 100755 --- a/tests/smoke_test.sh +++ b/tests/smoke_test.sh @@ -34,6 +34,15 @@ fi test_entity_discovery "areas" sensors processing diagnostics test_entity_discovery "components" lidar-unit imu-unit gps-unit camera-unit test_entity_discovery "apps" lidar-sim imu-sim gps-sim camera-sim anomaly-detector +test_entity_discovery "functions" sensor-monitoring anomaly-detection fault-management + +section "Discovery Relationships" + +assert_non_empty_items "/areas/sensors/components" + +section "Linux Introspection" + +assert_procfs_introspection "lidar-sim" section "Data Access" @@ -49,6 +58,24 @@ else fail "configurations contains 'noise_stddev' parameter" "not found in response" fi +section "Operations" + +assert_non_empty_items "/apps/medkit-fault-manager/operations" + +section "Scripts" + +assert_scripts_list "compute-unit" "run-diagnostics" +assert_script_execution "compute-unit" "run-diagnostics" 30 + +section "Bulk Data" + +# Bulk data endpoint should return 200 with categories list (may be empty without faults) +if api_get "/apps/diagnostic-bridge/bulk-data"; then + pass "GET /apps/diagnostic-bridge/bulk-data returns 200" +else + fail "GET /apps/diagnostic-bridge/bulk-data returns 200" "unexpected status code" +fi + section "Logs" assert_non_empty_items "/apps/medkit-gateway/logs" @@ -177,6 +204,35 @@ else fail "GET /apps/diagnostic-bridge/triggers returns 200 after delete" "unexpected status code" fi +section "Beacon Discovery" + +# Beacon data is exposed at vendor extension endpoints: +# /apps/{id}/x-medkit-topic-beacon (BEACON_MODE=topic) +# /apps/{id}/x-medkit-param-beacon (BEACON_MODE=param) +# When BEACON_MODE=none (CI default), these endpoints return 404. +beacon_found=false +for beacon_type in topic-beacon param-beacon; do + if api_get "/apps/lidar-sim/x-medkit-${beacon_type}"; then + beacon_found=true + pass "GET /apps/lidar-sim/x-medkit-${beacon_type} returns 200" + if echo "$RESPONSE" | jq -e '.status' > /dev/null 2>&1; then + pass "beacon response contains 'status' field" + else + fail "beacon response contains 'status' field" "field missing" + fi + if echo "$RESPONSE" | jq -e '.entity_id' > /dev/null 2>&1; then + pass "beacon response contains 'entity_id' field" + else + fail "beacon response contains 'entity_id' field" "field missing" + fi + break + fi +done +if [ "$beacon_found" = false ]; then + # Not a failure - beacons are optional depending on BEACON_MODE + echo -e " ${BLUE}SKIP${NC} beacon not active (BEACON_MODE=none or plugin not loaded)" +fi + # --- Summary --- print_summary diff --git a/tests/smoke_test_moveit.sh b/tests/smoke_test_moveit.sh index c279dbc..7287b12 100755 --- a/tests/smoke_test_moveit.sh +++ b/tests/smoke_test_moveit.sh @@ -35,11 +35,57 @@ fi test_entity_discovery "areas" manipulation planning diagnostics bridge test_entity_discovery "components" panda-arm panda-gripper moveit-planning pick-place-loop gateway fault-manager diagnostic-bridge test_entity_discovery "apps" joint-state-broadcaster panda-arm-controller panda-hand-controller robot-state-publisher move-group pick-place-node medkit-gateway medkit-fault-manager diagnostic-bridge-app manipulation-monitor +test_entity_discovery "functions" pick-and-place motion-planning gripper-control fault-management + +section "Discovery Relationships" + +assert_non_empty_items "/areas/manipulation/components" + +section "Linux Introspection" + +assert_procfs_introspection "medkit-gateway" + +section "Data Access" + +assert_non_empty_items "/apps/medkit-gateway/data" + +section "Operations" + +assert_non_empty_items "/apps/medkit-fault-manager/operations" + +section "Configurations" + +assert_non_empty_items "/apps/medkit-gateway/configurations" + +section "Scripts" + +assert_scripts_list "moveit-planning" "arm-self-test" +assert_script_execution "moveit-planning" "arm-self-test" 30 + +section "Bulk Data" + +if api_get "/apps/diagnostic-bridge-app/bulk-data"; then + pass "GET /apps/diagnostic-bridge-app/bulk-data returns 200" +else + fail "GET /apps/diagnostic-bridge-app/bulk-data returns 200" "unexpected status code" +fi + +section "Faults" + +if api_get "/faults"; then + pass "GET /faults returns 200" +else + fail "GET /faults returns 200" "unexpected status code" +fi section "Logs" assert_non_empty_items "/apps/medkit-gateway/logs" +section "Triggers" + +assert_triggers_crud "apps" "diagnostic-bridge-app" "/api/v1/apps/diagnostic-bridge-app/faults" + # --- Summary --- print_summary diff --git a/tests/smoke_test_turtlebot3.sh b/tests/smoke_test_turtlebot3.sh index 1a54ffa..8e169d4 100755 --- a/tests/smoke_test_turtlebot3.sh +++ b/tests/smoke_test_turtlebot3.sh @@ -36,11 +36,57 @@ fi test_entity_discovery "areas" robot navigation diagnostics bridge test_entity_discovery "components" turtlebot3-base lidar-sensor nav2-stack gateway fault-manager diagnostic-bridge-unit test_entity_discovery "apps" turtlebot3-node robot-state-publisher amcl bt-navigator controller-server planner-server velocity-smoother medkit-gateway medkit-fault-manager diagnostic-bridge anomaly-detector +test_entity_discovery "functions" autonomous-navigation robot-control fault-management + +section "Discovery Relationships" + +assert_non_empty_items "/areas/robot/components" + +section "Linux Introspection" + +assert_procfs_introspection "medkit-gateway" + +section "Data Access" + +assert_non_empty_items "/apps/medkit-gateway/data" + +section "Operations" + +assert_non_empty_items "/apps/medkit-fault-manager/operations" + +section "Configurations" + +assert_non_empty_items "/apps/medkit-gateway/configurations" + +section "Scripts" + +assert_scripts_list "nav2-stack" "nav-health-check" +assert_script_execution "nav2-stack" "nav-health-check" 30 + +section "Bulk Data" + +if api_get "/apps/diagnostic-bridge/bulk-data"; then + pass "GET /apps/diagnostic-bridge/bulk-data returns 200" +else + fail "GET /apps/diagnostic-bridge/bulk-data returns 200" "unexpected status code" +fi + +section "Faults" + +if api_get "/faults"; then + pass "GET /faults returns 200" +else + fail "GET /faults returns 200" "unexpected status code" +fi section "Logs" assert_non_empty_items "/apps/medkit-gateway/logs" +section "Triggers" + +assert_triggers_crud "apps" "diagnostic-bridge" "/api/v1/apps/diagnostic-bridge/faults" + # --- Summary --- print_summary From 86c9c6e4c24e288b13a95d068ded336fad3dbaaa Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Tue, 24 Mar 2026 13:26:55 +0100 Subject: [PATCH 2/5] fix: poll for fault-manager operations in turtlebot3 smoke test In Gazebo-heavy demos, the fault_manager ROS 2 services take longer to be discovered by runtime graph introspection. Use poll_until with a 30s timeout instead of an instant assert_non_empty_items check. --- tests/smoke_test_turtlebot3.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/smoke_test_turtlebot3.sh b/tests/smoke_test_turtlebot3.sh index 8e169d4..6352591 100755 --- a/tests/smoke_test_turtlebot3.sh +++ b/tests/smoke_test_turtlebot3.sh @@ -52,7 +52,13 @@ assert_non_empty_items "/apps/medkit-gateway/data" section "Operations" -assert_non_empty_items "/apps/medkit-fault-manager/operations" +# fault_manager services may take extra time to be discovered in Gazebo-heavy demos +echo " Waiting for fault-manager operations to appear (max 30s)..." +if poll_until "/apps/medkit-fault-manager/operations" '.items | length > 0' 30; then + pass "GET /apps/medkit-fault-manager/operations returns non-empty items" +else + fail "GET /apps/medkit-fault-manager/operations returns non-empty items" "items still empty after 30s" +fi section "Configurations" From c5e2d759a91441c09733784704dca86f1091e43e Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Tue, 24 Mar 2026 13:39:54 +0100 Subject: [PATCH 3/5] fix: poll for fault-manager operations in sensor and moveit smoke tests Same race condition as turtlebot3 - fault_manager ROS 2 services may not be discovered by the first runtime refresh cycle. Use poll_until with 30s timeout consistently across all demos. --- tests/smoke_test.sh | 8 +++++++- tests/smoke_test_moveit.sh | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/smoke_test.sh b/tests/smoke_test.sh index 3ea07e9..f73e64d 100755 --- a/tests/smoke_test.sh +++ b/tests/smoke_test.sh @@ -60,7 +60,13 @@ fi section "Operations" -assert_non_empty_items "/apps/medkit-fault-manager/operations" +# fault_manager services may take extra time to be discovered via runtime graph introspection +echo " Waiting for fault-manager operations to appear (max 30s)..." +if poll_until "/apps/medkit-fault-manager/operations" '.items | length > 0' 30; then + pass "GET /apps/medkit-fault-manager/operations returns non-empty items" +else + fail "GET /apps/medkit-fault-manager/operations returns non-empty items" "items still empty after 30s" +fi section "Scripts" diff --git a/tests/smoke_test_moveit.sh b/tests/smoke_test_moveit.sh index 7287b12..b77a374 100755 --- a/tests/smoke_test_moveit.sh +++ b/tests/smoke_test_moveit.sh @@ -51,7 +51,13 @@ assert_non_empty_items "/apps/medkit-gateway/data" section "Operations" -assert_non_empty_items "/apps/medkit-fault-manager/operations" +# fault_manager services may take extra time to be discovered via runtime graph introspection +echo " Waiting for fault-manager operations to appear (max 30s)..." +if poll_until "/apps/medkit-fault-manager/operations" '.items | length > 0' 30; then + pass "GET /apps/medkit-fault-manager/operations returns non-empty items" +else + fail "GET /apps/medkit-fault-manager/operations returns non-empty items" "items still empty after 30s" +fi section "Configurations" From 44ca4f90760728cf3915891104e9025fc17b14f6 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Tue, 24 Mar 2026 19:01:11 +0100 Subject: [PATCH 4/5] refactor: address deep review findings - Replace 62 lines of inline trigger CRUD in smoke_test.sh with assert_triggers_crud helper call (deduplication) - Use jq -n for JSON body construction in assert_triggers_crud instead of bash string interpolation (safety, project convention) - Update stale file header comments in moveit and turtlebot3 tests to list actual coverage (12+ feature areas, not just entity discovery) - Update root README test descriptions to reflect expanded coverage - Add EXIT trap for print_summary in all test scripts so gateway startup timeouts still produce structured output in CI - Guard print_summary against double-printing (trap + explicit call) --- README.md | 6 ++-- tests/smoke_lib.sh | 16 +++++++-- tests/smoke_test.sh | 66 +++------------------------------- tests/smoke_test_moveit.sh | 10 ++++-- tests/smoke_test_turtlebot3.sh | 10 ++++-- 5 files changed, 37 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 539a6b9..3624498 100644 --- a/README.md +++ b/README.md @@ -179,9 +179,9 @@ Each demo has automated smoke tests that verify the gateway starts and the REST ```bash # Run smoke tests against a running demo (default: http://localhost:8080) -./tests/smoke_test.sh # Sensor diagnostics (21 tests, incl. fault injection) -./tests/smoke_test_turtlebot3.sh # TurtleBot3 (entity discovery) -./tests/smoke_test_moveit.sh # MoveIt pick-and-place (entity discovery) +./tests/smoke_test.sh # Sensor diagnostics (full API coverage + fault injection + beacons) +./tests/smoke_test_turtlebot3.sh # TurtleBot3 (discovery, data, operations, scripts, triggers, logs) +./tests/smoke_test_moveit.sh # MoveIt pick-and-place (discovery, data, operations, scripts, triggers, logs) ``` CI runs all 3 demos in parallel - each job builds the Docker image, starts the container, and runs the smoke tests against it. See [CI workflow](.github/workflows/ci.yml). diff --git a/tests/smoke_lib.sh b/tests/smoke_lib.sh index 8e2ee2c..a18d9ce 100755 --- a/tests/smoke_lib.sh +++ b/tests/smoke_lib.sh @@ -266,10 +266,13 @@ assert_triggers_crud() { # Create trigger echo " Creating OnChange trigger on ${entity_type}/${entity_id}..." + local payload + payload=$(jq -n --arg resource "$resource_uri" \ + '{resource: $resource, trigger_condition: {condition_type: "OnChange"}, multishot: true, lifetime: 60}') local create_response create_response=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}${triggers_endpoint}" \ -H "Content-Type: application/json" \ - -d "{\"resource\":\"${resource_uri}\",\"trigger_condition\":{\"condition_type\":\"OnChange\"},\"multishot\":true,\"lifetime\":60}" 2>/dev/null) || true + -d "$payload" 2>/dev/null) || true local create_http create_http=$(echo "$create_response" | tail -1) @@ -334,8 +337,15 @@ assert_triggers_crud() { fi } -# Print test summary and exit with appropriate code +# Print test summary (called via EXIT trap - do not call exit here) +SUMMARY_PRINTED=false print_summary() { + # Guard against double-printing when called as both trap and explicit call + if [ "$SUMMARY_PRINTED" = true ]; then + return + fi + SUMMARY_PRINTED=true + echo "" echo -e "${BLUE}================================${NC}" local total=$((PASS_COUNT + FAIL_COUNT)) @@ -344,7 +354,7 @@ print_summary() { if [ "$FAIL_COUNT" -gt 0 ]; then echo -e "\n ${RED}Failed tests:${FAILED_TESTS}${NC}" echo -e "${BLUE}================================${NC}" - exit 1 + return fi echo -e "${BLUE}================================${NC}" diff --git a/tests/smoke_test.sh b/tests/smoke_test.sh index f73e64d..5e5a359 100755 --- a/tests/smoke_test.sh +++ b/tests/smoke_test.sh @@ -12,6 +12,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=tests/smoke_lib.sh source "${SCRIPT_DIR}/smoke_lib.sh" +trap print_summary EXIT + # --- Wait for gateway startup --- wait_for_gateway 90 @@ -149,66 +151,7 @@ fi section "Triggers" -# Create a trigger on diagnostic-bridge faults -echo " Creating OnChange fault trigger..." -TRIGGER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}/apps/diagnostic-bridge/triggers" \ - -H "Content-Type: application/json" \ - -d '{"resource":"/api/v1/apps/diagnostic-bridge/faults","trigger_condition":{"condition_type":"OnChange"},"multishot":true,"lifetime":60}' 2>/dev/null) || true - -TRIGGER_HTTP=$(echo "$TRIGGER_RESPONSE" | tail -1) -TRIGGER_BODY=$(echo "$TRIGGER_RESPONSE" | sed '$d') - -if [ "$TRIGGER_HTTP" = "201" ]; then - pass "POST /apps/diagnostic-bridge/triggers returns 201" -else - fail "POST /apps/diagnostic-bridge/triggers returns 201" "got HTTP $TRIGGER_HTTP" -fi - -TRIGGER_ID=$(echo "$TRIGGER_BODY" | jq -r '.id') -if [ -n "$TRIGGER_ID" ] && [ "$TRIGGER_ID" != "null" ]; then - pass "trigger response contains valid id" -else - fail "trigger response contains valid id" "id is null or empty" -fi - -TRIGGER_STATUS=$(echo "$TRIGGER_BODY" | jq -r '.status') -if [ "$TRIGGER_STATUS" = "active" ]; then - pass "trigger status is 'active'" -else - fail "trigger status is 'active'" "got '$TRIGGER_STATUS'" -fi - -# List triggers - verify it appears -if api_get "/apps/diagnostic-bridge/triggers"; then - if echo "$RESPONSE" | jq -e --arg id "$TRIGGER_ID" '.items[] | select(.id == $id)' > /dev/null 2>&1; then - pass "GET /apps/diagnostic-bridge/triggers lists created trigger" - else - fail "GET /apps/diagnostic-bridge/triggers lists created trigger" "trigger $TRIGGER_ID not found" - fi -else - fail "GET /apps/diagnostic-bridge/triggers returns 200" "unexpected status code" -fi - -# Delete trigger -TRIGGER_DELETE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ - "${API_BASE}/apps/diagnostic-bridge/triggers/${TRIGGER_ID}" 2>/dev/null) || true - -if [ "$TRIGGER_DELETE_STATUS" = "204" ]; then - pass "DELETE /apps/diagnostic-bridge/triggers/$TRIGGER_ID returns 204" -else - fail "DELETE /apps/diagnostic-bridge/triggers/$TRIGGER_ID returns 204" "got HTTP $TRIGGER_DELETE_STATUS" -fi - -# Verify trigger is gone -if api_get "/apps/diagnostic-bridge/triggers"; then - if ! echo "$RESPONSE" | jq -e --arg id "$TRIGGER_ID" '.items[] | select(.id == $id)' > /dev/null 2>&1; then - pass "trigger no longer listed after deletion" - else - fail "trigger no longer listed after deletion" "still found in list" - fi -else - fail "GET /apps/diagnostic-bridge/triggers returns 200 after delete" "unexpected status code" -fi +assert_triggers_crud "apps" "diagnostic-bridge" "/api/v1/apps/diagnostic-bridge/faults" section "Beacon Discovery" @@ -241,4 +184,5 @@ fi # --- Summary --- -print_summary +# print_summary runs via EXIT trap; exit code reflects test results +[ "$FAIL_COUNT" -eq 0 ] diff --git a/tests/smoke_test_moveit.sh b/tests/smoke_test_moveit.sh index b77a374..30533e4 100755 --- a/tests/smoke_test_moveit.sh +++ b/tests/smoke_test_moveit.sh @@ -2,7 +2,10 @@ # Smoke tests for moveit_pick_place demo # Runs from the host against the containerized gateway on localhost:8080 # -# Tests: health, entity discovery (areas, components, apps from manifest) +# Tests: health, entity discovery (areas/components/apps/functions), +# discovery relationships, Linux introspection, data access, operations, +# configurations, scripts (list + execution), bulk data, faults, logs, +# trigger CRUD lifecycle # Uses demo.launch.py (fake hardware, no Gazebo) for CI stability # # Usage: ./tests/smoke_test_moveit.sh [GATEWAY_URL] @@ -15,6 +18,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=tests/smoke_lib.sh source "${SCRIPT_DIR}/smoke_lib.sh" +trap print_summary EXIT + # --- Wait for gateway startup --- wait_for_gateway 120 @@ -94,4 +99,5 @@ assert_triggers_crud "apps" "diagnostic-bridge-app" "/api/v1/apps/diagnostic-bri # --- Summary --- -print_summary +# print_summary runs via EXIT trap; exit code reflects test results +[ "$FAIL_COUNT" -eq 0 ] diff --git a/tests/smoke_test_turtlebot3.sh b/tests/smoke_test_turtlebot3.sh index 6352591..206fedc 100755 --- a/tests/smoke_test_turtlebot3.sh +++ b/tests/smoke_test_turtlebot3.sh @@ -2,7 +2,10 @@ # Smoke tests for turtlebot3_integration demo # Runs from the host against the containerized gateway on localhost:8080 # -# Tests: health, entity discovery (areas, components, apps from manifest) +# Tests: health, entity discovery (areas/components/apps/functions), +# discovery relationships, Linux introspection, data access, operations, +# configurations, scripts (list + execution), bulk data, faults, logs, +# trigger CRUD lifecycle # No fault injection - Gazebo-based demo is too complex for reliable CI fault testing # # Usage: ./tests/smoke_test_turtlebot3.sh [GATEWAY_URL] @@ -15,6 +18,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=tests/smoke_lib.sh source "${SCRIPT_DIR}/smoke_lib.sh" +trap print_summary EXIT + # --- Wait for gateway startup --- # Turtlebot3 needs Gazebo + Nav2 - allow extra startup time @@ -95,4 +100,5 @@ assert_triggers_crud "apps" "diagnostic-bridge" "/api/v1/apps/diagnostic-bridge/ # --- Summary --- -print_summary +# print_summary runs via EXIT trap; exit code reflects test results +[ "$FAIL_COUNT" -eq 0 ] From fdb199dc1bf3e7f1074080c6a510ea798cfa5d06 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Tue, 24 Mar 2026 21:59:13 +0100 Subject: [PATCH 5/5] test: add entity count assertions to catch duplicate/synthetic entity leaks test_entity_discovery now verifies exact entity count matches expected, not just that expected entities exist. This catches the merge pipeline bug where runtime-created synthetic entities (underscored IDs) are not suppressed after being linked to manifest entities (hyphenated IDs). Also adds missing entity IDs to sensor and turtlebot3 tests so all manifest-defined entities are checked. Will fail CI until ros2_medkit#307 is fixed. --- tests/smoke_lib.sh | 11 +++++++++++ tests/smoke_test.sh | 6 +++--- tests/smoke_test_turtlebot3.sh | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/smoke_lib.sh b/tests/smoke_lib.sh index a18d9ce..6b0edc3 100755 --- a/tests/smoke_lib.sh +++ b/tests/smoke_lib.sh @@ -144,12 +144,23 @@ test_entity_discovery() { local entity_type="$1" shift local entity_ids=("$@") + local expected_count=${#entity_ids[@]} local label label="${entity_type^}" section "Entity Discovery - ${label}" if api_get "/${entity_type}"; then + # Verify exact count - catches duplicate/synthetic entities leaking through + local actual_count + actual_count=$(echo "$RESPONSE" | jq '.items | length') + if [ "$actual_count" = "$expected_count" ]; then + pass "${entity_type} count is ${expected_count}" + else + local actual_ids + actual_ids=$(echo "$RESPONSE" | jq -r '[.items[].id] | sort | join(", ")') + fail "${entity_type} count is ${expected_count}" "got ${actual_count}: ${actual_ids}" + fi for entity_id in "${entity_ids[@]}"; do if echo "$RESPONSE" | items_contain_id "$entity_id"; then pass "${entity_type} contains '${entity_id}'" diff --git a/tests/smoke_test.sh b/tests/smoke_test.sh index 5e5a359..d827540 100755 --- a/tests/smoke_test.sh +++ b/tests/smoke_test.sh @@ -33,9 +33,9 @@ else fail "GET /health returns 200" "unexpected status code" fi -test_entity_discovery "areas" sensors processing diagnostics -test_entity_discovery "components" lidar-unit imu-unit gps-unit camera-unit -test_entity_discovery "apps" lidar-sim imu-sim gps-sim camera-sim anomaly-detector +test_entity_discovery "areas" sensors processing diagnostics bridge +test_entity_discovery "components" lidar-unit imu-unit gps-unit camera-unit compute-unit gateway fault-manager diagnostic-bridge-unit +test_entity_discovery "apps" lidar-sim imu-sim gps-sim camera-sim anomaly-detector medkit-gateway medkit-fault-manager diagnostic-bridge test_entity_discovery "functions" sensor-monitoring anomaly-detection fault-management section "Discovery Relationships" diff --git a/tests/smoke_test_turtlebot3.sh b/tests/smoke_test_turtlebot3.sh index 206fedc..54a1fcd 100755 --- a/tests/smoke_test_turtlebot3.sh +++ b/tests/smoke_test_turtlebot3.sh @@ -40,7 +40,7 @@ fi test_entity_discovery "areas" robot navigation diagnostics bridge test_entity_discovery "components" turtlebot3-base lidar-sensor nav2-stack gateway fault-manager diagnostic-bridge-unit -test_entity_discovery "apps" turtlebot3-node robot-state-publisher amcl bt-navigator controller-server planner-server velocity-smoother medkit-gateway medkit-fault-manager diagnostic-bridge anomaly-detector +test_entity_discovery "apps" turtlebot3-node robot-state-publisher gazebo amcl bt-navigator controller-server planner-server velocity-smoother medkit-gateway medkit-fault-manager diagnostic-bridge anomaly-detector test_entity_discovery "functions" autonomous-navigation robot-control fault-management section "Discovery Relationships"