diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 5a7221a950..15c753252b 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -153,6 +153,13 @@ to be listening. Changes to `scripts/hot-dev.sh` and `scripts/hot-dev-bg.sh` must therefore stay on their own direct dev-runtime orchestration proof path instead of piggybacking on installer proof coverage for unrelated deployment scripts. +That same dev-runtime orchestration boundary also owns watcher stability for +the managed local stack: `scripts/hot-dev.sh` may only rebuild the backend for +runtime Go sources, not `*_test.go` churn, and it must suppress `pulse` binary +change events produced by its own successful managed rebuilds or startup build. +Otherwise unrelated parallel test edits or hot-dev's own binary output can +tear down `7655`, produce transient `5173` proxy failures, and undermine the +canonical browser-runtime proof path. That same launcher boundary also owns its CLI contract: managed commands such as `start --takeover` and `restart --takeover` must preserve the takeover flag through the actual script entrypoint instead of silently dropping second-arg diff --git a/scripts/hot-dev.sh b/scripts/hot-dev.sh index 6be63bbffb..ef8cedc636 100755 --- a/scripts/hot-dev.sh +++ b/scripts/hot-dev.sh @@ -514,8 +514,9 @@ log_info "Starting backend file watcher..." ( cd "${ROOT_DIR}" - LAST_RESTART_TIME=0 WATCHER_READY_AT=$(date +%s) + LAST_RESTART_TIME=0 + SELF_BUILD_IGNORE_UNTIL=$((WATCHER_READY_AT + HOT_DEV_WATCHER_STARTUP_GRACE_SECONDS + 5)) watcher_within_startup_grace() { local now @@ -523,6 +524,27 @@ log_info "Starting backend file watcher..." (( now - WATCHER_READY_AT < HOT_DEV_WATCHER_STARTUP_GRACE_SECONDS )) } + mark_self_build_output() { + local now + now=$(date +%s) + SELF_BUILD_IGNORE_UNTIL=$((now + 5)) + } + + manual_build_event_suppressed() { + local now + now=$(date +%s) + (( now < SELF_BUILD_IGNORE_UNTIL )) + } + + should_rebuild_backend_for_change() { + local changed_file=$1 + + [[ "${changed_file}" == *.go ]] || return 1 + [[ "${changed_file}" == *_test.go ]] && return 1 + + return 0 + } + restart_backend() { # Debounce: skip if we restarted less than 3 seconds ago local now @@ -604,6 +626,7 @@ log_info "Starting backend file watcher..." # Use the same build logic as the initial build. if build_backend_binary; then + mark_self_build_output restart_backend else log_error "✗ Build failed; keeping the current backend process running." @@ -627,9 +650,12 @@ log_info "Starting backend file watcher..." log_info "🚀 Managed restart requested, restarting backend..." restart_backend elif [[ "$changed_file" == "${ROOT_DIR}/pulse" ]]; then + if manual_build_event_suppressed; then + continue + fi log_info "🚀 Manual build detected (pulse binary changed), restarting..." restart_backend - elif [[ "$changed_file" == *.go ]] || [[ "$event" =~ CREATE|DELETE|MOVED|ATTRIB ]]; then + elif should_rebuild_backend_for_change "$changed_file"; then rebuild_backend "$changed_file" fi done @@ -651,6 +677,9 @@ log_info "Starting backend file watcher..." if [[ "$changed_file" == "${HOT_DEV_RESTART_SENTINEL}" ]]; then log_info "🚀 Managed restart requested, restarting backend..." else + if manual_build_event_suppressed; then + continue + fi log_info "🚀 Manual build detected (pulse binary changed), restarting..." fi restart_backend @@ -663,8 +692,7 @@ log_info "Starting backend file watcher..." --include '\.go$' \ "${ROOT_DIR}/cmd" "${ROOT_DIR}/internal" "${ROOT_DIR}/pkg" 2>/dev/null | \ while read -r changed_file; do - # fswatch sends absolute paths, simple check for .go extension - if [[ "$changed_file" == *.go ]]; then + if should_rebuild_backend_for_change "$changed_file"; then rebuild_backend "$changed_file" fi done diff --git a/scripts/tests/test-hot-dev-bg.sh b/scripts/tests/test-hot-dev-bg.sh index c2acfea5fe..0678b42be1 100755 --- a/scripts/tests/test-hot-dev-bg.sh +++ b/scripts/tests/test-hot-dev-bg.sh @@ -6,6 +6,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" HOT_DEV_BG="${ROOT_DIR}/scripts/hot-dev-bg.sh" +HOT_DEV="${ROOT_DIR}/scripts/hot-dev.sh" CLEAN_MOCK_ALERTS="${ROOT_DIR}/scripts/clean-mock-alerts.sh" DEV_CHECK="${ROOT_DIR}/scripts/dev-check.sh" PACKAGE_JSON="${ROOT_DIR}/package.json" @@ -468,7 +469,7 @@ test_makefile_routes_managed_runtime_through_npm() { test_hot_dev_script_advertises_foreground_escape_hatch() { local output - output="$(sed -n '1,30p' "${ROOT_DIR}/scripts/hot-dev.sh")" + output="$(sed -n '1,30p' "${HOT_DEV}")" assert_contains "hot-dev header identifies foreground escape hatch" "${output}" "hot-dev.sh - Foreground Pulse dev runtime escape hatch" assert_contains "hot-dev usage points to managed runtime first" "${output}" "npm run dev # Canonical managed dev runtime" @@ -476,6 +477,16 @@ test_hot_dev_script_advertises_foreground_escape_hatch() { assert_not_contains "hot-dev usage no longer claims standard dev mode" "${output}" "Standard dev mode" } +test_hot_dev_script_ignores_test_only_backend_churn() { + local output + output="$(cat "${HOT_DEV}")" + + assert_contains "hot-dev watcher ignores Go test files" "${output}" '[[ "${changed_file}" == *_test.go ]] && return 1' + assert_contains "hot-dev watcher routes rebuild decisions through shared helper" "${output}" 'should_rebuild_backend_for_change "$changed_file"' + assert_contains "hot-dev watcher suppresses self-build binary restart loops" "${output}" 'if manual_build_event_suppressed; then' + assert_contains "hot-dev watcher suppresses startup self-build pulse events" "${output}" 'SELF_BUILD_IGNORE_UNTIL=$((WATCHER_READY_AT + HOT_DEV_WATCHER_STARTUP_GRACE_SECONDS + 5))' +} + test_hot_dev_bg_script_advertises_managed_entrypoint() { local output output="$(cat "${ROOT_DIR}/scripts/hot-dev-bg.sh")" @@ -744,6 +755,7 @@ main() { test_frontend_package_exposes_managed_runtime_entrypoints test_makefile_routes_managed_runtime_through_npm test_hot_dev_script_advertises_foreground_escape_hatch + test_hot_dev_script_ignores_test_only_backend_churn test_hot_dev_bg_script_advertises_managed_entrypoint test_hot_dev_bg_usage_prefers_managed_wrappers test_integration_readme_uses_managed_backend_restart_wrapper