From 8d30aa8efe85dedc743a695f0dce1f426c2fe807 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:43:15 +0000 Subject: [PATCH 1/6] fix(ci): runtime-policy accepts the js-runtimes locks (Bun > Deno > pnpm > npm) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repository's runtime-policy.yml failed any repository holding bun.lock, deno.lock and pnpm-lock.yaml together ("Mixed toolchains: 3 package managers"), which is exactly what `js-runtimes regen` writes under the 2026-10-14 ruling (standards LANGUAGE-POLICY §1.3-§1.4). It is replaced with rsr-template-repo's fixed version: the three regen locks count as one dependency graph, package-lock.json next to them still fails, and a deno.json is a notice. The file is kept here, not deleted as in governance-calling repositories, because this repository does not call governance-reusable and so has no other check that fails a tracked package-lock.json. Checked: the gate's step exits 0 on bun.lock + deno.lock + pnpm-lock.yaml and 1 once package-lock.json is added. Its actions.lock record (actions/checkout@v7.0.1) is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/runtime-policy.yml | 49 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml index 8110273..3036a80 100644 --- a/.github/workflows/runtime-policy.yml +++ b/.github/workflows/runtime-policy.yml @@ -1,10 +1,14 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. # Runtime and package-manager policy check. # -# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1. -# Ordering: Bun (1st) > Deno (2nd) > pnpm (3rd) > npm (last resort). +# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1.3–§1.4 (owner ruling +# 2026-10-14). Ordering: Bun (1st) > Deno (2nd) > pnpm (3rd) > npm (last resort). +# +# One manifest, every runtime: package.json is the only manifest, and +# `js-runtimes regen` writes bun.lock, deno.lock and pnpm-lock.yaml from it. Those +# three locks are ONE dependency graph locked three ways, not three toolchains; +# governance's "JS runtimes" jobs check each one against package.json. # # REPLACES npm-bun-blocker.yml, which failed any build carrying `bun.lockb` with # the message "npm/bun artifacts detected. Use Deno instead." That gate blocked @@ -12,11 +16,13 @@ # 55 repositories. # # What this fails on, deliberately: -# MIXED TOOLCHAINS -- two different package managers' lockfiles in one repo. +# MIXED TOOLCHAINS -- an npm package-lock.json next to the js-runtimes locks. # That is real, actionable drift: two dependency graphs that can disagree. # What it does NOT fail on: -# Using bun, deno, pnpm or npm. npm is LAST but PERMITTED; the check reports -# the tier in use so drift is visible without blocking legitimate work. +# The js-runtimes locks together, or any one runtime alone. npm is LAST but +# PERMITTED; a deno.json is a notice (Deno is tier 2, LANGUAGE-POLICY §1.3). +# The check reports the tiers in use so drift is visible without blocking +# legitimate work. name: Runtime Policy on: push: @@ -33,7 +39,7 @@ permissions: jobs: runtime-policy: name: Runtime Policy - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read @@ -44,30 +50,33 @@ jobs: run: | set -euo pipefail - bun=0; deno=0; pnpm=0; npm=0 + bun=0; deno=0; pnpm=0; npm=0; deno_cfg=0 [ -f bun.lockb ] || [ -f bun.lock ] && bun=1 || true - [ -f deno.lock ] || [ -f deno.json ] || [ -f deno.jsonc ] && deno=1 || true + [ -f deno.lock ] && deno=1 || true + [ -f deno.json ] || [ -f deno.jsonc ] && deno_cfg=1 || true [ -f pnpm-lock.yaml ] && pnpm=1 || true [ -f package-lock.json ] && npm=1 || true - total=$((bun + deno + pnpm + npm)) + # bun, deno and pnpm locks are the one graph js-runtimes regen writes. + regen=$((bun + deno + pnpm)) - if [ "$total" -eq 0 ]; then - echo "::notice::No JS/TS package manager in use — nothing to check." + if [ $((regen + npm + deno_cfg)) -eq 0 ]; then + echo "::notice::No JS package manager in use — nothing to check." exit 0 fi - # Report the tier actually in use (LANGUAGE-POLICY.adoc §1). + # Report the tiers actually in use (LANGUAGE-POLICY.adoc §1). [ "$bun" -eq 1 ] && echo "Bun — tier 1 (preferred)" - [ "$deno" -eq 1 ] && echo "Deno — tier 2 (accepted; existing projects are grandfathered)" + [ "$deno" -eq 1 ] && echo "Deno — tier 2" + [ "$deno_cfg" -eq 1 ] && echo "::notice::A deno.json is authored for Deno (tier 2, permitted; notice only, LANGUAGE-POLICY.adoc §1.3). Keep scripts in package.json so Bun runs them too." [ "$pnpm" -eq 1 ] && echo "pnpm — tier 3" - [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. See LANGUAGE-POLICY.adoc §1." + [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred, and it keeps no lock: package-lock.json is refused (standards#67). See LANGUAGE-POLICY.adoc §1.4." - if [ "$total" -gt 1 ]; then - echo "::error::Mixed toolchains: $total package managers have lockfiles in this repository." - echo "Two dependency graphs that can disagree is real drift. Pick one — preferring the" - echo "highest tier present — and delete the others' lockfiles." + if [ "$npm" -eq 1 ] && [ "$regen" -gt 0 ]; then + echo "::error::Mixed toolchains: package-lock.json sits next to the js-runtimes locks." + echo "Two dependency graphs that can disagree is real drift. Delete package-lock.json" + echo "and let 'js-runtimes regen' write bun.lock, deno.lock and pnpm-lock.yaml." exit 1 fi - echo "✅ Single package manager in use." + echo "✅ One dependency graph in use." From bb9bbff41f370c159d685a8c61221988104b6104 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:27:02 +0100 Subject: [PATCH 2/6] Update .github/workflows/runtime-policy.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- .github/workflows/runtime-policy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml index 3036a80..1029a93 100644 --- a/.github/workflows/runtime-policy.yml +++ b/.github/workflows/runtime-policy.yml @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # Runtime and package-manager policy check. From d93087acde14c81b98151490612c31b26a11a8c9 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:29:29 +0000 Subject: [PATCH 3/6] fix(ci): clarify that standalone npm lockfiles are permitted in runtime-policy warning --- .github/workflows/runtime-policy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml index 1029a93..b4e8671 100644 --- a/.github/workflows/runtime-policy.yml +++ b/.github/workflows/runtime-policy.yml @@ -71,7 +71,7 @@ jobs: [ "$deno" -eq 1 ] && echo "Deno — tier 2" [ "$deno_cfg" -eq 1 ] && echo "::notice::A deno.json is authored for Deno (tier 2, permitted; notice only, LANGUAGE-POLICY.adoc §1.3). Keep scripts in package.json so Bun runs them too." [ "$pnpm" -eq 1 ] && echo "pnpm — tier 3" - [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred, and it keeps no lock: package-lock.json is refused (standards#67). See LANGUAGE-POLICY.adoc §1.4." + [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. A standalone package-lock.json is permitted; it is rejected only beside a js-runtimes lock (standards#67). See LANGUAGE-POLICY.adoc §1.4." if [ "$npm" -eq 1 ] && [ "$regen" -gt 0 ]; then echo "::error::Mixed toolchains: package-lock.json sits next to the js-runtimes locks." From fa99baa589096ec19cecd7012b661c74f0f9d323 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:40:46 +0000 Subject: [PATCH 4/6] fix(shell): prevent sourced modules from clobbering script paths and stabilize CI tests --- src/diagnostics/connectivity.sh | 8 ++++---- src/diagnostics/dns.sh | 6 +++--- src/diagnostics/firewall.sh | 8 ++++---- src/diagnostics/interfaces.sh | 6 +++--- src/diagnostics/networkmanager.sh | 6 +++--- src/diagnostics/routing.sh | 6 +++--- src/main.sh | 34 +++++++++++++++---------------- src/repairs/dns.sh | 10 ++++----- src/repairs/interfaces.sh | 8 ++++---- src/repairs/networkmanager.sh | 8 ++++---- src/repairs/routing.sh | 12 +++++------ src/utils/logging.sh | 4 ++-- src/utils/privileges.sh | 3 ++- tests/run-tests.sh | 8 +++++--- tests/test-utils.sh | 32 +++++++++++++++++++++++++---- 15 files changed, 93 insertions(+), 66 deletions(-) diff --git a/src/diagnostics/connectivity.sh b/src/diagnostics/connectivity.sh index 6d2523b..61ee48b 100644 --- a/src/diagnostics/connectivity.sh +++ b/src/diagnostics/connectivity.sh @@ -2,9 +2,9 @@ # Connectivity diagnostics module # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" +DIAGNOSTICS_CONNECTIVITY_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${DIAGNOSTICS_CONNECTIVITY_SCRIPT_DIR}/../utils/logging.sh" +source "${DIAGNOSTICS_CONNECTIVITY_SCRIPT_DIR}/../utils/system.sh" # Test basic connectivity test_basic_connectivity() { @@ -167,7 +167,7 @@ test_mtu() { # Subtract 28 bytes for IP + ICMP header local packet_size=$((size - 28)) - if ping -c 1 -W 2 -M do -s ${packet_size} 8.8.8.8 >/dev/null 2>&1; then + if ping -c 1 -W 2 -M 'do' -s "${packet_size}" 8.8.8.8 >/dev/null 2>&1; then log_success " Packet size ${size} bytes: OK" break else diff --git a/src/diagnostics/dns.sh b/src/diagnostics/dns.sh index 51c5f09..03143f7 100644 --- a/src/diagnostics/dns.sh +++ b/src/diagnostics/dns.sh @@ -2,9 +2,9 @@ # DNS diagnostics module # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" +DIAGNOSTICS_DNS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${DIAGNOSTICS_DNS_SCRIPT_DIR}/../utils/logging.sh" +source "${DIAGNOSTICS_DNS_SCRIPT_DIR}/../utils/system.sh" # Check DNS configuration check_dns_config() { diff --git a/src/diagnostics/firewall.sh b/src/diagnostics/firewall.sh index afbbf9a..3381b67 100644 --- a/src/diagnostics/firewall.sh +++ b/src/diagnostics/firewall.sh @@ -2,10 +2,10 @@ # Firewall diagnostics module # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" -source "${SCRIPT_DIR}/../utils/privileges.sh" +DIAGNOSTICS_FIREWALL_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${DIAGNOSTICS_FIREWALL_SCRIPT_DIR}/../utils/logging.sh" +source "${DIAGNOSTICS_FIREWALL_SCRIPT_DIR}/../utils/system.sh" +source "${DIAGNOSTICS_FIREWALL_SCRIPT_DIR}/../utils/privileges.sh" # Check iptables rules check_iptables() { diff --git a/src/diagnostics/interfaces.sh b/src/diagnostics/interfaces.sh index dd429be..3d14449 100644 --- a/src/diagnostics/interfaces.sh +++ b/src/diagnostics/interfaces.sh @@ -2,9 +2,9 @@ # Network interface diagnostics module # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" +DIAGNOSTICS_INTERFACES_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${DIAGNOSTICS_INTERFACES_SCRIPT_DIR}/../utils/logging.sh" +source "${DIAGNOSTICS_INTERFACES_SCRIPT_DIR}/../utils/system.sh" # Check network interfaces check_interfaces() { diff --git a/src/diagnostics/networkmanager.sh b/src/diagnostics/networkmanager.sh index 89b9f7c..a5530b6 100644 --- a/src/diagnostics/networkmanager.sh +++ b/src/diagnostics/networkmanager.sh @@ -2,9 +2,9 @@ # NetworkManager diagnostics module # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" +DIAGNOSTICS_NETWORKMANAGER_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${DIAGNOSTICS_NETWORKMANAGER_SCRIPT_DIR}/../utils/logging.sh" +source "${DIAGNOSTICS_NETWORKMANAGER_SCRIPT_DIR}/../utils/system.sh" # Check NetworkManager status check_nm_status() { diff --git a/src/diagnostics/routing.sh b/src/diagnostics/routing.sh index 59ce834..6c843b2 100644 --- a/src/diagnostics/routing.sh +++ b/src/diagnostics/routing.sh @@ -2,9 +2,9 @@ # Routing diagnostics module # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" +DIAGNOSTICS_ROUTING_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${DIAGNOSTICS_ROUTING_SCRIPT_DIR}/../utils/logging.sh" +source "${DIAGNOSTICS_ROUTING_SCRIPT_DIR}/../utils/system.sh" # Check routing table check_routing_table() { diff --git a/src/main.sh b/src/main.sh index a971913..711cd09 100755 --- a/src/main.sh +++ b/src/main.sh @@ -6,32 +6,32 @@ set -euo pipefail # Script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MAIN_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Version VERSION="1.0.0" # Source utilities -source "${SCRIPT_DIR}/utils/colors.sh" -source "${SCRIPT_DIR}/utils/logging.sh" -source "${SCRIPT_DIR}/utils/privileges.sh" -source "${SCRIPT_DIR}/utils/system.sh" -source "${SCRIPT_DIR}/utils/backup.sh" -source "${SCRIPT_DIR}/utils/safemode.sh" +source "${MAIN_SCRIPT_DIR}/utils/colors.sh" +source "${MAIN_SCRIPT_DIR}/utils/logging.sh" +source "${MAIN_SCRIPT_DIR}/utils/privileges.sh" +source "${MAIN_SCRIPT_DIR}/utils/system.sh" +source "${MAIN_SCRIPT_DIR}/utils/backup.sh" +source "${MAIN_SCRIPT_DIR}/utils/safemode.sh" # Source diagnostics -source "${SCRIPT_DIR}/diagnostics/dns.sh" -source "${SCRIPT_DIR}/diagnostics/interfaces.sh" -source "${SCRIPT_DIR}/diagnostics/routing.sh" -source "${SCRIPT_DIR}/diagnostics/connectivity.sh" -source "${SCRIPT_DIR}/diagnostics/firewall.sh" -source "${SCRIPT_DIR}/diagnostics/networkmanager.sh" +source "${MAIN_SCRIPT_DIR}/diagnostics/dns.sh" +source "${MAIN_SCRIPT_DIR}/diagnostics/interfaces.sh" +source "${MAIN_SCRIPT_DIR}/diagnostics/routing.sh" +source "${MAIN_SCRIPT_DIR}/diagnostics/connectivity.sh" +source "${MAIN_SCRIPT_DIR}/diagnostics/firewall.sh" +source "${MAIN_SCRIPT_DIR}/diagnostics/networkmanager.sh" # Source repairs -source "${SCRIPT_DIR}/repairs/dns.sh" -source "${SCRIPT_DIR}/repairs/interfaces.sh" -source "${SCRIPT_DIR}/repairs/routing.sh" -source "${SCRIPT_DIR}/repairs/networkmanager.sh" +source "${MAIN_SCRIPT_DIR}/repairs/dns.sh" +source "${MAIN_SCRIPT_DIR}/repairs/interfaces.sh" +source "${MAIN_SCRIPT_DIR}/repairs/routing.sh" +source "${MAIN_SCRIPT_DIR}/repairs/networkmanager.sh" # Configuration DRY_RUN="${DRY_RUN:-false}" diff --git a/src/repairs/dns.sh b/src/repairs/dns.sh index 174686d..4f90cf1 100644 --- a/src/repairs/dns.sh +++ b/src/repairs/dns.sh @@ -2,11 +2,11 @@ # DNS repair procedures # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" -source "${SCRIPT_DIR}/../utils/privileges.sh" -source "${SCRIPT_DIR}/../utils/backup.sh" +REPAIRS_DNS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/logging.sh" +source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/system.sh" +source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/privileges.sh" +source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/backup.sh" # Repair DNS configuration repair_dns_config() { diff --git a/src/repairs/interfaces.sh b/src/repairs/interfaces.sh index 946e312..17b4715 100644 --- a/src/repairs/interfaces.sh +++ b/src/repairs/interfaces.sh @@ -2,10 +2,10 @@ # Network interface repair procedures # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" -source "${SCRIPT_DIR}/../utils/privileges.sh" +REPAIRS_INTERFACES_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${REPAIRS_INTERFACES_SCRIPT_DIR}/../utils/logging.sh" +source "${REPAIRS_INTERFACES_SCRIPT_DIR}/../utils/system.sh" +source "${REPAIRS_INTERFACES_SCRIPT_DIR}/../utils/privileges.sh" # Bring up network interface interface_up() { diff --git a/src/repairs/networkmanager.sh b/src/repairs/networkmanager.sh index af850fe..b6bbe82 100644 --- a/src/repairs/networkmanager.sh +++ b/src/repairs/networkmanager.sh @@ -2,10 +2,10 @@ # NetworkManager repair procedures # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" -source "${SCRIPT_DIR}/../utils/privileges.sh" +REPAIRS_NETWORKMANAGER_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${REPAIRS_NETWORKMANAGER_SCRIPT_DIR}/../utils/logging.sh" +source "${REPAIRS_NETWORKMANAGER_SCRIPT_DIR}/../utils/system.sh" +source "${REPAIRS_NETWORKMANAGER_SCRIPT_DIR}/../utils/privileges.sh" # Restart NetworkManager restart_networkmanager() { diff --git a/src/repairs/routing.sh b/src/repairs/routing.sh index deee95d..a243eb3 100644 --- a/src/repairs/routing.sh +++ b/src/repairs/routing.sh @@ -2,10 +2,10 @@ # Routing repair procedures # Source utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/../utils/logging.sh" -source "${SCRIPT_DIR}/../utils/system.sh" -source "${SCRIPT_DIR}/../utils/privileges.sh" +REPAIRS_ROUTING_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${REPAIRS_ROUTING_SCRIPT_DIR}/../utils/logging.sh" +source "${REPAIRS_ROUTING_SCRIPT_DIR}/../utils/system.sh" +source "${REPAIRS_ROUTING_SCRIPT_DIR}/../utils/privileges.sh" # Add default route add_default_route() { @@ -134,7 +134,7 @@ repair_default_route() { log_step "Attempting to obtain gateway via DHCP" # Renew DHCP which should set up routes - source "${SCRIPT_DIR}/interfaces.sh" + source "${REPAIRS_ROUTING_SCRIPT_DIR}/interfaces.sh" if renew_dhcp "${primary}"; then sleep 2 @@ -171,7 +171,7 @@ flush_routing_table() { log_step "Rebuilding routes via DHCP" # Renew DHCP to rebuild routes - source "${SCRIPT_DIR}/interfaces.sh" + source "${REPAIRS_ROUTING_SCRIPT_DIR}/interfaces.sh" if renew_dhcp "${primary}"; then sleep 3 diff --git a/src/utils/logging.sh b/src/utils/logging.sh index 7f5b030..1311331 100644 --- a/src/utils/logging.sh +++ b/src/utils/logging.sh @@ -2,8 +2,8 @@ # Logging utilities for network repair tool # Source color utilities -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/colors.sh" +LOGGING_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${LOGGING_SCRIPT_DIR}/colors.sh" # Log levels declare -gA LOG_LEVELS=( diff --git a/src/utils/privileges.sh b/src/utils/privileges.sh index 4ca6a64..0e63d4a 100644 --- a/src/utils/privileges.sh +++ b/src/utils/privileges.sh @@ -42,7 +42,8 @@ require_root() { ( while true; do sudo -n true; sleep 50; done 2>/dev/null ) & SUDO_KEEPER_PID=$! - # Kill sudo keeper on exit + # Capture this keeper's PID now so exit cleanup targets the process we started. + # shellcheck disable=SC2064 trap "kill ${SUDO_KEEPER_PID} 2>/dev/null || true" EXIT } diff --git a/tests/run-tests.sh b/tests/run-tests.sh index cb2ff0c..f23bf8e 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -252,9 +252,11 @@ test_safe_mode() { print_result "FAIL" "Repairs should be blocked in safe mode" fi - # Test that diagnose works without flags - if "${PROJECT_DIR}/network-repair" diagnose >/dev/null 2>&1 || \ - "${PROJECT_DIR}/network-repair" diagnose 2>&1 | grep -q "Diagnostic"; then + # Diagnostics may report issues with a nonzero exit. Capture output first so + # grep -q cannot stop the producer with SIGPIPE under pipefail. + local diagnose_status=0 + output=$("${PROJECT_DIR}/network-repair" diagnose 2>&1) || diagnose_status=$? + if [[ ${diagnose_status} -eq 0 ]] || grep -q "Diagnostic" <<< "${output}"; then print_result "PASS" "Diagnose works in safe mode" else print_result "FAIL" "Diagnose should work in safe mode" diff --git a/tests/test-utils.sh b/tests/test-utils.sh index 98cd1fe..9459054 100755 --- a/tests/test-utils.sh +++ b/tests/test-utils.sh @@ -11,14 +11,37 @@ source "${PROJECT_DIR}/src/utils/colors.sh" source "${PROJECT_DIR}/src/utils/logging.sh" source "${PROJECT_DIR}/src/utils/system.sh" +# Load modules in fresh shells from an unrelated directory. Sourced dependencies +# must not replace the caller's directory or break subsequent relative imports. +test_module_loading() { + echo "Testing module loading..." + + local module + for module in "${PROJECT_DIR}/src/utils/logging.sh" \ + "${PROJECT_DIR}"/src/diagnostics/*.sh \ + "${PROJECT_DIR}"/src/repairs/*.sh; do + if ! bash -euc ' + cd / + SCRIPT_DIR=caller-directory + source "$1" + [[ "${SCRIPT_DIR}" == caller-directory ]] + ' _ "${module}"; then + echo "Module loading failed: ${module}" + exit 1 + fi + done + + echo "✓ Modules load without changing the caller directory" +} + # Test colors test_colors() { echo "Testing color utilities..." - # Test that color variables are defined - [[ -n "${COLOR_RED}" ]] || { echo "COLOR_RED not defined"; exit 1; } - [[ -n "${COLOR_GREEN}" ]] || { echo "COLOR_GREEN not defined"; exit 1; } - [[ -n "${COLOR_RESET}" ]] || { echo "COLOR_RESET not defined"; exit 1; } + # Color values are intentionally empty when stdout is not a terminal (CI). + [[ -v COLOR_RED ]] || { echo "COLOR_RED not defined"; exit 1; } + [[ -v COLOR_GREEN ]] || { echo "COLOR_GREEN not defined"; exit 1; } + [[ -v COLOR_RESET ]] || { echo "COLOR_RESET not defined"; exit 1; } echo "✓ Color utilities work" } @@ -68,6 +91,7 @@ main() { test_colors test_logging test_system + test_module_loading echo "" echo "All utility tests passed!" From e9784dc8e7d34a1c571132514e8c4659d4fda70e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 21:40:47 +0000 Subject: [PATCH 5/6] fix(ci): runtime-policy refuses any package-lock.json A package-lock.json on its own fell through to "One dependency graph in use". standards#67 refuses tracked package locks outright: npm is the last-resort runtime, permitted, and it keeps no lock. The gate now fails on any package-lock.json, and still names the mixed-toolchain case when the js-runtimes locks sit beside it. This replaces d93087a's warning text, which described the gap as policy. Checked on seven lockfile combinations: none, bun, the three js-runtimes locks, deno.json, package-lock.json alone, and package-lock.json beside one and all three locks. Only the last three fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/runtime-policy.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml index b4e8671..54b4103 100644 --- a/.github/workflows/runtime-policy.yml +++ b/.github/workflows/runtime-policy.yml @@ -17,13 +17,14 @@ # 55 repositories. # # What this fails on, deliberately: -# MIXED TOOLCHAINS -- an npm package-lock.json next to the js-runtimes locks. -# That is real, actionable drift: two dependency graphs that can disagree. +# ANY tracked package-lock.json. npm is LAST but PERMITTED as a runtime, and +# it keeps no lock: package-lock.json is refused (standards#67). Next to the +# js-runtimes locks it is also MIXED TOOLCHAINS, two dependency graphs that +# can disagree. # What it does NOT fail on: -# The js-runtimes locks together, or any one runtime alone. npm is LAST but -# PERMITTED; a deno.json is a notice (Deno is tier 2, LANGUAGE-POLICY §1.3). -# The check reports the tiers in use so drift is visible without blocking -# legitimate work. +# The js-runtimes locks together, or any one of them alone; a deno.json is a +# notice (Deno is tier 2, LANGUAGE-POLICY §1.3). The check reports the tiers +# in use so drift is visible without blocking legitimate work. name: Runtime Policy on: push: @@ -71,12 +72,17 @@ jobs: [ "$deno" -eq 1 ] && echo "Deno — tier 2" [ "$deno_cfg" -eq 1 ] && echo "::notice::A deno.json is authored for Deno (tier 2, permitted; notice only, LANGUAGE-POLICY.adoc §1.3). Keep scripts in package.json so Bun runs them too." [ "$pnpm" -eq 1 ] && echo "pnpm — tier 3" - [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. A standalone package-lock.json is permitted; it is rejected only beside a js-runtimes lock (standards#67). See LANGUAGE-POLICY.adoc §1.4." + [ "$npm" -eq 1 ] && echo "npm — tier 4 (last resort)" - if [ "$npm" -eq 1 ] && [ "$regen" -gt 0 ]; then - echo "::error::Mixed toolchains: package-lock.json sits next to the js-runtimes locks." - echo "Two dependency graphs that can disagree is real drift. Delete package-lock.json" - echo "and let 'js-runtimes regen' write bun.lock, deno.lock and pnpm-lock.yaml." + if [ "$npm" -eq 1 ]; then + if [ "$regen" -gt 0 ]; then + echo "::error::Mixed toolchains: package-lock.json sits next to the js-runtimes locks." + echo "Two dependency graphs that can disagree is real drift." + else + echo "::error::package-lock.json is refused (standards#67). npm is the last resort, permitted but never preferred, and it keeps no lock." + fi + echo "Delete package-lock.json and let 'js-runtimes regen' write bun.lock, deno.lock" + echo "and pnpm-lock.yaml from package.json. See LANGUAGE-POLICY.adoc §1.4." exit 1 fi From 2e64e1d3943db84ca22ed797fd8d0a1884c3e43c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 21:40:47 +0000 Subject: [PATCH 6/6] fix(shell): carry #79's SCRIPT_DIR fix, replacing fa99baa fa99baa fixed the same bug as #79 with different variable names, so whichever PR merged second would conflict. src/ and tests/ now match #79 at 4270e56 exactly: NR_UTILS_DIR / NR_REPAIRS_DIR in the modules, diagnostics that report issues without tripping errexit, the safe-mode and colour test fixes, and the module-loading regression test that fa99baa introduced. Once #79 merges this change is a no-op. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- src/diagnostics/connectivity.sh | 8 ++-- src/diagnostics/dns.sh | 6 +-- src/diagnostics/firewall.sh | 8 ++-- src/diagnostics/interfaces.sh | 8 ++-- src/diagnostics/networkmanager.sh | 6 +-- src/diagnostics/routing.sh | 6 +-- src/main.sh | 57 +++++++++++++---------------- src/repairs/dns.sh | 10 ++--- src/repairs/interfaces.sh | 8 ++-- src/repairs/networkmanager.sh | 8 ++-- src/repairs/routing.sh | 36 ++++++++++++++---- src/utils/logging.sh | 4 +- src/utils/privileges.sh | 5 +-- tests/run-tests.sh | 6 +-- tests/test-utils.sh | 61 +++++++++++++++++-------------- 15 files changed, 129 insertions(+), 108 deletions(-) diff --git a/src/diagnostics/connectivity.sh b/src/diagnostics/connectivity.sh index 61ee48b..fb8f9f3 100644 --- a/src/diagnostics/connectivity.sh +++ b/src/diagnostics/connectivity.sh @@ -2,9 +2,9 @@ # Connectivity diagnostics module # Source utilities -DIAGNOSTICS_CONNECTIVITY_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${DIAGNOSTICS_CONNECTIVITY_SCRIPT_DIR}/../utils/logging.sh" -source "${DIAGNOSTICS_CONNECTIVITY_SCRIPT_DIR}/../utils/system.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" # Test basic connectivity test_basic_connectivity() { @@ -167,7 +167,7 @@ test_mtu() { # Subtract 28 bytes for IP + ICMP header local packet_size=$((size - 28)) - if ping -c 1 -W 2 -M 'do' -s "${packet_size}" 8.8.8.8 >/dev/null 2>&1; then + if ping -c 1 -W 2 -M 'do' -s ${packet_size} 8.8.8.8 >/dev/null 2>&1; then log_success " Packet size ${size} bytes: OK" break else diff --git a/src/diagnostics/dns.sh b/src/diagnostics/dns.sh index 03143f7..3b23a7e 100644 --- a/src/diagnostics/dns.sh +++ b/src/diagnostics/dns.sh @@ -2,9 +2,9 @@ # DNS diagnostics module # Source utilities -DIAGNOSTICS_DNS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${DIAGNOSTICS_DNS_SCRIPT_DIR}/../utils/logging.sh" -source "${DIAGNOSTICS_DNS_SCRIPT_DIR}/../utils/system.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" # Check DNS configuration check_dns_config() { diff --git a/src/diagnostics/firewall.sh b/src/diagnostics/firewall.sh index 3381b67..4cef14e 100644 --- a/src/diagnostics/firewall.sh +++ b/src/diagnostics/firewall.sh @@ -2,10 +2,10 @@ # Firewall diagnostics module # Source utilities -DIAGNOSTICS_FIREWALL_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${DIAGNOSTICS_FIREWALL_SCRIPT_DIR}/../utils/logging.sh" -source "${DIAGNOSTICS_FIREWALL_SCRIPT_DIR}/../utils/system.sh" -source "${DIAGNOSTICS_FIREWALL_SCRIPT_DIR}/../utils/privileges.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" +source "${NR_UTILS_DIR}/privileges.sh" # Check iptables rules check_iptables() { diff --git a/src/diagnostics/interfaces.sh b/src/diagnostics/interfaces.sh index 3d14449..788122d 100644 --- a/src/diagnostics/interfaces.sh +++ b/src/diagnostics/interfaces.sh @@ -2,9 +2,9 @@ # Network interface diagnostics module # Source utilities -DIAGNOSTICS_INTERFACES_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${DIAGNOSTICS_INTERFACES_SCRIPT_DIR}/../utils/logging.sh" -source "${DIAGNOSTICS_INTERFACES_SCRIPT_DIR}/../utils/system.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" # Check network interfaces check_interfaces() { @@ -187,7 +187,7 @@ check_interface_conflicts() { ip=$(get_interface_ip "${iface}") if [[ -n "${ip}" ]]; then - if [[ -n "${ip_map[${ip}]}" ]]; then + if [[ -n "${ip_map[${ip}]:-}" ]]; then log_error " Duplicate IP ${ip} on ${iface} and ${ip_map[${ip}]}" issues=$((issues + 1)) else diff --git a/src/diagnostics/networkmanager.sh b/src/diagnostics/networkmanager.sh index a5530b6..3ed6c5d 100644 --- a/src/diagnostics/networkmanager.sh +++ b/src/diagnostics/networkmanager.sh @@ -2,9 +2,9 @@ # NetworkManager diagnostics module # Source utilities -DIAGNOSTICS_NETWORKMANAGER_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${DIAGNOSTICS_NETWORKMANAGER_SCRIPT_DIR}/../utils/logging.sh" -source "${DIAGNOSTICS_NETWORKMANAGER_SCRIPT_DIR}/../utils/system.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" # Check NetworkManager status check_nm_status() { diff --git a/src/diagnostics/routing.sh b/src/diagnostics/routing.sh index 6c843b2..b4cac2f 100644 --- a/src/diagnostics/routing.sh +++ b/src/diagnostics/routing.sh @@ -2,9 +2,9 @@ # Routing diagnostics module # Source utilities -DIAGNOSTICS_ROUTING_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${DIAGNOSTICS_ROUTING_SCRIPT_DIR}/../utils/logging.sh" -source "${DIAGNOSTICS_ROUTING_SCRIPT_DIR}/../utils/system.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" # Check routing table check_routing_table() { diff --git a/src/main.sh b/src/main.sh index 711cd09..20fce16 100755 --- a/src/main.sh +++ b/src/main.sh @@ -5,33 +5,34 @@ set -euo pipefail -# Script directory -MAIN_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Script directory. The modules sourced below resolve their own directory +# into NR_UTILS_DIR / NR_REPAIRS_DIR and must not assign SCRIPT_DIR. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Version VERSION="1.0.0" # Source utilities -source "${MAIN_SCRIPT_DIR}/utils/colors.sh" -source "${MAIN_SCRIPT_DIR}/utils/logging.sh" -source "${MAIN_SCRIPT_DIR}/utils/privileges.sh" -source "${MAIN_SCRIPT_DIR}/utils/system.sh" -source "${MAIN_SCRIPT_DIR}/utils/backup.sh" -source "${MAIN_SCRIPT_DIR}/utils/safemode.sh" +source "${SCRIPT_DIR}/utils/colors.sh" +source "${SCRIPT_DIR}/utils/logging.sh" +source "${SCRIPT_DIR}/utils/privileges.sh" +source "${SCRIPT_DIR}/utils/system.sh" +source "${SCRIPT_DIR}/utils/backup.sh" +source "${SCRIPT_DIR}/utils/safemode.sh" # Source diagnostics -source "${MAIN_SCRIPT_DIR}/diagnostics/dns.sh" -source "${MAIN_SCRIPT_DIR}/diagnostics/interfaces.sh" -source "${MAIN_SCRIPT_DIR}/diagnostics/routing.sh" -source "${MAIN_SCRIPT_DIR}/diagnostics/connectivity.sh" -source "${MAIN_SCRIPT_DIR}/diagnostics/firewall.sh" -source "${MAIN_SCRIPT_DIR}/diagnostics/networkmanager.sh" +source "${SCRIPT_DIR}/diagnostics/dns.sh" +source "${SCRIPT_DIR}/diagnostics/interfaces.sh" +source "${SCRIPT_DIR}/diagnostics/routing.sh" +source "${SCRIPT_DIR}/diagnostics/connectivity.sh" +source "${SCRIPT_DIR}/diagnostics/firewall.sh" +source "${SCRIPT_DIR}/diagnostics/networkmanager.sh" # Source repairs -source "${MAIN_SCRIPT_DIR}/repairs/dns.sh" -source "${MAIN_SCRIPT_DIR}/repairs/interfaces.sh" -source "${MAIN_SCRIPT_DIR}/repairs/routing.sh" -source "${MAIN_SCRIPT_DIR}/repairs/networkmanager.sh" +source "${SCRIPT_DIR}/repairs/dns.sh" +source "${SCRIPT_DIR}/repairs/interfaces.sh" +source "${SCRIPT_DIR}/repairs/routing.sh" +source "${SCRIPT_DIR}/repairs/networkmanager.sh" # Configuration DRY_RUN="${DRY_RUN:-false}" @@ -123,29 +124,23 @@ run_all_diagnostics() { log_info "Network Manager: $(detect_network_manager)" echo "" - # Run diagnostics - diagnose_interfaces - total_issues=$((total_issues + $?)) + # Diagnostic failures report issues; collect them without triggering errexit. + diagnose_interfaces || total_issues=$((total_issues + $?)) echo "" - diagnose_routing - total_issues=$((total_issues + $?)) + diagnose_routing || total_issues=$((total_issues + $?)) echo "" - diagnose_dns - total_issues=$((total_issues + $?)) + diagnose_dns || total_issues=$((total_issues + $?)) echo "" - diagnose_connectivity - total_issues=$((total_issues + $?)) + diagnose_connectivity || total_issues=$((total_issues + $?)) echo "" - diagnose_firewall - total_issues=$((total_issues + $?)) + diagnose_firewall || total_issues=$((total_issues + $?)) echo "" - diagnose_networkmanager - total_issues=$((total_issues + $?)) + diagnose_networkmanager || total_issues=$((total_issues + $?)) echo "" # Summary diff --git a/src/repairs/dns.sh b/src/repairs/dns.sh index 4f90cf1..1203142 100644 --- a/src/repairs/dns.sh +++ b/src/repairs/dns.sh @@ -2,11 +2,11 @@ # DNS repair procedures # Source utilities -REPAIRS_DNS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/logging.sh" -source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/system.sh" -source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/privileges.sh" -source "${REPAIRS_DNS_SCRIPT_DIR}/../utils/backup.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" +source "${NR_UTILS_DIR}/privileges.sh" +source "${NR_UTILS_DIR}/backup.sh" # Repair DNS configuration repair_dns_config() { diff --git a/src/repairs/interfaces.sh b/src/repairs/interfaces.sh index 17b4715..c7e50df 100644 --- a/src/repairs/interfaces.sh +++ b/src/repairs/interfaces.sh @@ -2,10 +2,10 @@ # Network interface repair procedures # Source utilities -REPAIRS_INTERFACES_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${REPAIRS_INTERFACES_SCRIPT_DIR}/../utils/logging.sh" -source "${REPAIRS_INTERFACES_SCRIPT_DIR}/../utils/system.sh" -source "${REPAIRS_INTERFACES_SCRIPT_DIR}/../utils/privileges.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" +source "${NR_UTILS_DIR}/privileges.sh" # Bring up network interface interface_up() { diff --git a/src/repairs/networkmanager.sh b/src/repairs/networkmanager.sh index b6bbe82..55a982c 100644 --- a/src/repairs/networkmanager.sh +++ b/src/repairs/networkmanager.sh @@ -2,10 +2,10 @@ # NetworkManager repair procedures # Source utilities -REPAIRS_NETWORKMANAGER_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${REPAIRS_NETWORKMANAGER_SCRIPT_DIR}/../utils/logging.sh" -source "${REPAIRS_NETWORKMANAGER_SCRIPT_DIR}/../utils/system.sh" -source "${REPAIRS_NETWORKMANAGER_SCRIPT_DIR}/../utils/privileges.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" +source "${NR_UTILS_DIR}/privileges.sh" # Restart NetworkManager restart_networkmanager() { diff --git a/src/repairs/routing.sh b/src/repairs/routing.sh index a243eb3..ac553d7 100644 --- a/src/repairs/routing.sh +++ b/src/repairs/routing.sh @@ -2,10 +2,13 @@ # Routing repair procedures # Source utilities -REPAIRS_ROUTING_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${REPAIRS_ROUTING_SCRIPT_DIR}/../utils/logging.sh" -source "${REPAIRS_ROUTING_SCRIPT_DIR}/../utils/system.sh" -source "${REPAIRS_ROUTING_SCRIPT_DIR}/../utils/privileges.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../utils" && pwd)" +# Resolved now: the functions below source interfaces.sh after other +# modules have loaded. +NR_REPAIRS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${NR_UTILS_DIR}/logging.sh" +source "${NR_UTILS_DIR}/system.sh" +source "${NR_UTILS_DIR}/privileges.sh" # Add default route add_default_route() { @@ -89,7 +92,16 @@ remove_duplicate_routes() { return 0 } -# Repair default route +# Remove duplicate default routes if the initial route query succeeds; +# otherwise try renewing DHCP on the primary interface. +# Returns 0 when the initial `ip route show default` succeeds, even if its +# output is empty or the gateway is unreachable. The DHCP fallback returns 0 +# only if renewal and a subsequent route query succeed; otherwise it returns 1, +# including when no primary interface is found. +# May remove routes and release/renew a DHCP lease. Requests root/sudo access +# and exits the shell if access cannot be obtained. With errexit active, +# unguarded failures such as interface lookup or sourcing helpers can terminate +# the caller before a return status is produced. repair_default_route() { log_section "Repair Default Route" @@ -134,7 +146,7 @@ repair_default_route() { log_step "Attempting to obtain gateway via DHCP" # Renew DHCP which should set up routes - source "${REPAIRS_ROUTING_SCRIPT_DIR}/interfaces.sh" + source "${NR_REPAIRS_DIR}/interfaces.sh" if renew_dhcp "${primary}"; then sleep 2 @@ -148,7 +160,15 @@ repair_default_route() { return 1 } -# Flush and rebuild routing table +# Flush the main routing table and try to rebuild routes by renewing DHCP on +# the primary interface selected before the flush. May disconnect the network +# and release/renew a DHCP lease; no rollback is performed on failure. +# Returns 0 if DHCP renewal and the subsequent `ip route show default` succeed, +# even if the query output is empty. Returns 1 if no primary interface is found, +# renewal fails or the final query fails. +# Requests root/sudo access and exits the shell if access cannot be obtained. +# With errexit active, unguarded failures such as interface lookup, flushing +# routes or sourcing helpers can terminate the caller before returning. flush_routing_table() { log_section "Flush and Rebuild Routing Table" @@ -171,7 +191,7 @@ flush_routing_table() { log_step "Rebuilding routes via DHCP" # Renew DHCP to rebuild routes - source "${REPAIRS_ROUTING_SCRIPT_DIR}/interfaces.sh" + source "${NR_REPAIRS_DIR}/interfaces.sh" if renew_dhcp "${primary}"; then sleep 3 diff --git a/src/utils/logging.sh b/src/utils/logging.sh index 1311331..73efe5f 100644 --- a/src/utils/logging.sh +++ b/src/utils/logging.sh @@ -2,8 +2,8 @@ # Logging utilities for network repair tool # Source color utilities -LOGGING_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${LOGGING_SCRIPT_DIR}/colors.sh" +NR_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${NR_UTILS_DIR}/colors.sh" # Log levels declare -gA LOG_LEVELS=( diff --git a/src/utils/privileges.sh b/src/utils/privileges.sh index 0e63d4a..3f28093 100644 --- a/src/utils/privileges.sh +++ b/src/utils/privileges.sh @@ -42,9 +42,8 @@ require_root() { ( while true; do sudo -n true; sleep 50; done 2>/dev/null ) & SUDO_KEEPER_PID=$! - # Capture this keeper's PID now so exit cleanup targets the process we started. - # shellcheck disable=SC2064 - trap "kill ${SUDO_KEEPER_PID} 2>/dev/null || true" EXIT + # Kill sudo keeper on exit + trap 'kill "${SUDO_KEEPER_PID}" 2>/dev/null || true' EXIT } # Run command with appropriate privileges diff --git a/tests/run-tests.sh b/tests/run-tests.sh index f23bf8e..c9a2ec7 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -252,11 +252,11 @@ test_safe_mode() { print_result "FAIL" "Repairs should be blocked in safe mode" fi - # Diagnostics may report issues with a nonzero exit. Capture output first so - # grep -q cannot stop the producer with SIGPIPE under pipefail. + # Diagnose returns 1 for network issues, but must complete in safe mode. + # Capture once so pipefail and grep closing early cannot mask the result. local diagnose_status=0 output=$("${PROJECT_DIR}/network-repair" diagnose 2>&1) || diagnose_status=$? - if [[ ${diagnose_status} -eq 0 ]] || grep -q "Diagnostic" <<< "${output}"; then + if [[ ${diagnose_status} -le 1 && "${output}" == *"=== Diagnostic Summary ==="* ]]; then print_result "PASS" "Diagnose works in safe mode" else print_result "FAIL" "Diagnose should work in safe mode" diff --git a/tests/test-utils.sh b/tests/test-utils.sh index 9459054..4b439e4 100755 --- a/tests/test-utils.sh +++ b/tests/test-utils.sh @@ -11,37 +11,24 @@ source "${PROJECT_DIR}/src/utils/colors.sh" source "${PROJECT_DIR}/src/utils/logging.sh" source "${PROJECT_DIR}/src/utils/system.sh" -# Load modules in fresh shells from an unrelated directory. Sourced dependencies -# must not replace the caller's directory or break subsequent relative imports. -test_module_loading() { - echo "Testing module loading..." - - local module - for module in "${PROJECT_DIR}/src/utils/logging.sh" \ - "${PROJECT_DIR}"/src/diagnostics/*.sh \ - "${PROJECT_DIR}"/src/repairs/*.sh; do - if ! bash -euc ' - cd / - SCRIPT_DIR=caller-directory - source "$1" - [[ "${SCRIPT_DIR}" == caller-directory ]] - ' _ "${module}"; then - echo "Module loading failed: ${module}" - exit 1 - fi - done - - echo "✓ Modules load without changing the caller directory" -} - # Test colors test_colors() { echo "Testing color utilities..." - # Color values are intentionally empty when stdout is not a terminal (CI). - [[ -v COLOR_RED ]] || { echo "COLOR_RED not defined"; exit 1; } - [[ -v COLOR_GREEN ]] || { echo "COLOR_GREEN not defined"; exit 1; } - [[ -v COLOR_RESET ]] || { echo "COLOR_RESET not defined"; exit 1; } + # Colors are defined but may be empty when stdout is not a terminal. + [[ "${COLOR_RED+x}" == x ]] || { echo "COLOR_RED not defined"; exit 1; } + [[ "${COLOR_GREEN+x}" == x ]] || { echo "COLOR_GREEN not defined"; exit 1; } + [[ "${COLOR_RESET+x}" == x ]] || { echo "COLOR_RESET not defined"; exit 1; } + + # Explicitly disabled colors must stay empty regardless of terminal support. + ( + USE_COLORS=never + source "${PROJECT_DIR}/src/utils/colors.sh" + [[ -z "${COLOR_RED}${COLOR_GREEN}${COLOR_RESET}" ]] || { + echo "Colors should be empty when disabled" + exit 1 + } + ) echo "✓ Color utilities work" } @@ -83,6 +70,26 @@ test_system() { echo "✓ System utilities work" } +# Test that sourcing a module leaves the caller's SCRIPT_DIR alone. Each module +# loads in a fresh shell started from an unrelated directory. +test_module_loading() { + echo "Testing module loading..." + + local module + for module in "${PROJECT_DIR}/src/utils/logging.sh" \ + "${PROJECT_DIR}"/src/diagnostics/*.sh \ + "${PROJECT_DIR}"/src/repairs/*.sh; do + bash -euc ' + cd / + SCRIPT_DIR=caller-directory + source "$1" + [[ "${SCRIPT_DIR}" == caller-directory ]] + ' _ "${module}" || { echo "Sourcing ${module} changed SCRIPT_DIR"; exit 1; } + done + + echo "✓ Modules leave the caller's SCRIPT_DIR alone" +} + # Run all tests main() { echo "Running utility function tests..."