fix: revert TUNNEL_LOCAL_SSH_PORT to 2223, fix registry, add incident docs

- wizard.sh + connect.conf: TUNNEL_LOCAL_SSH_PORT back to 2223 (Synology SSH port)
- port-registry: renamed x6340 → x4662 (matches QuickConnect ID)
- port-registry: added MR2200ac mesh satellite note for x4662
- deployment checklist: added kit-connect section, post-deployment rules,
  incident log, and complete fleet port registry table
- memory: added fleet-router-inventory.md with full fleet state
- hub README: updated with current status and locations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:20:08 +00:00
parent 44566d9563
commit 24d8fbd366
178 changed files with 874 additions and 15 deletions
+23
View File
@@ -0,0 +1,23 @@
setup() { load '../../package/kit-busrouter/files/usr/lib/busrouter/lib-score.sh'; }
@test "composite weights sum to a 0-100 score" {
# latency35 dl25 up10 jit10 loss10 sig10 ; all metrics perfect -> 100
run score_composite 5 25 12.5 1 0 -65
[ "$output" -eq 100 ]
}
@test "composite all-worst -> 0" {
run score_composite 200 0.5 0.5 50 40 -95
[ "$output" -eq 0 ]
}
@test "rolling average of 3 samples" {
run rolling_avg 90 60 30 # -> 60
[ "$output" -eq 60 ]
}
@test "rolling average rounds half-up" {
run rolling_avg 100 100 99 # 299/3 = 99.67 -> 100
[ "$output" -eq 100 ]
}
@test "signal weight redistributes when no modem (signal='')" {
run score_composite 5 25 12.5 1 0 "" # still 100, sig weight moved to latency/throughput
[ "$output" -eq 100 ]
}
+71
View File
@@ -0,0 +1,71 @@
LIBDIR="$(cd "$BATS_TEST_DIRNAME/../.."/package/kit-busrouter/files/usr/lib/busrouter && pwd)"
setup() {
export LIB_DIR="$LIBDIR"
export STATE_DIR="$BATS_TMPDIR/daemon_state"
rm -rf "$STATE_DIR"
mkdir -p "$STATE_DIR"
export SCORE_SAMPLES=3
export CELL_IFACE=rmnet_mhi0 CELL_MBR=modem_0001
export ETH_IFACE=eth0 ETH_MBR=wan
export INTERVAL=30
# Load all libs (daemon.sh's while loop is guarded by case "${0##*/}" so safe to load)
load '../../package/kit-busrouter/files/usr/lib/busrouter/lib-decide.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/lib-score.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/metrics-net.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/speedtest.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/modem-sim.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/starlink.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/wan-mwan.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/telemetry.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/daemon.sh'
}
@test "daemon.sh loads without error and does not start loop" {
run true
[ "$status" -eq 0 ]
# If we reach here, the load in setup() did not hang -> loop guard works
}
@test "_score_history_update: primes from default 50 50 50 and appends new score" {
run _score_history_update "modem_0001" 70
[ "$status" -eq 0 ]
echo "$output" | grep -qE '^50 50 70$'
}
@test "_score_history_update: keeps last SCORE_SAMPLES scores only" {
export SCORE_SAMPLES=3
_score_history_update "modem_0001" 60 >/dev/null
_score_history_update "modem_0001" 70 >/dev/null
_score_history_update "modem_0001" 80 >/dev/null
run _score_history_update "modem_0001" 90
[ "$status" -eq 0 ]
# Should be "70 80 90" (last 3 of: 50 50 50 60 70 80 90)
echo "$output" | grep -qE '^70 80 90$'
}
@test "_collect_wan: writes metric and score files" {
net_probe_iface() { echo "20 5 0"; }
speed_test_due() { return 0; }
speed_test_iface() { echo "50.0 10.0"; }
score_composite() { echo "75"; }
rolling_avg() { echo "72"; }
run _collect_wan "eth0" "wan"
[ "$status" -eq 0 ]
[ -f "${STATE_DIR}/metric_wan" ]
[ -f "${STATE_DIR}/score_wan" ]
score=$(cat "${STATE_DIR}/score_wan")
[ "$score" -eq 72 ]
}
@test "_collect_wan: fail-open when net_probe_iface returns error" {
net_probe_iface() { return 1; }
speed_test_due() { return 1; }
score_composite() { echo "0"; }
rolling_avg() { echo "0"; }
run _collect_wan "rmnet_mhi0" "modem_0001" "-99"
[ "$status" -eq 0 ]
[ -f "${STATE_DIR}/metric_modem_0001" ]
}
+21
View File
@@ -0,0 +1,21 @@
setup(){ load '../../package/kit-busrouter/files/usr/lib/busrouter/lib-decide.sh'; }
@test "equal scores -> 50/50" { run wan_weight 70 70; [ "$output" -eq 50 ]; }
# 50 + 2*(80-60) = 90 -> clamp to 80 (max). So 80 is the true formula result.
@test "weight = 50 + 2*diff, rounded to 10" { run wan_weight 80 60; [ "$output" -eq 80 ]; }
@test "clamped to 20..80 (high)" { run wan_weight 100 0; [ "$output" -eq 80 ]; }
@test "clamped to 20..80 (low)" { run wan_weight 0 100; [ "$output" -eq 20 ]; }
# 50 + 2*(58-55) = 56 -> round to nearest 10 -> 60
@test "rounds to nearest 10" { run wan_weight 58 55; [ "$output" -eq 60 ]; }
@test "no change if within hysteresis band" { run weight_changed 50 54; [ "$status" -eq 1 ]; }
@test "change if crosses a 10% step" { run weight_changed 50 65; [ "$status" -eq 0 ]; }
@test "change is symmetric (decrease)" { run weight_changed 60 50; [ "$status" -eq 0 ]; }
@test "sim switch only after N bad cycles" { run sim_should_switch 3 3; [ "$status" -eq 0 ]; }
@test "sim no switch before threshold" { run sim_should_switch 2 3; [ "$status" -eq 1 ]; }
@test "gw_verdict: 3/3 -> alive" { run gw_verdict 1 1 1; [ "$status" -eq 0 ]; }
@test "gw_verdict: 2/3 no http -> alive" { run gw_verdict 1 1 0; [ "$status" -eq 0 ]; }
@test "gw_verdict: 2/3 no ping -> alive" { run gw_verdict 1 0 1; [ "$status" -eq 0 ]; }
@test "gw_verdict: 2/3 no dns -> alive" { run gw_verdict 0 1 1; [ "$status" -eq 0 ]; }
@test "gw_verdict: 1/3 -> dead" { run gw_verdict 1 0 0; [ "$status" -eq 1 ]; }
@test "gw_verdict: 0/3 -> dead" { run gw_verdict 0 0 0; [ "$status" -eq 1 ]; }
+125
View File
@@ -0,0 +1,125 @@
FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
LIBDIR="$(cd "$BATS_TEST_DIRNAME/../.."/package/kit-busrouter/files/usr/lib/busrouter && pwd)"
setup() {
export LIB_DIR="$LIBDIR"
load '../../package/kit-busrouter/files/usr/lib/busrouter/lib-decide.sh'
load '../../package/kit-busrouter/files/usr/lib/busrouter/modem-sim.sh'
}
@test "modem_signal ubus parse: rsrp=-96 rsrq=-10 sinr=13" {
run _modem_parse_ubus < "$FIXTURES/modem-signal-ubus.json"
[ "$status" -eq 0 ]
rsrp=$(echo "$output" | awk '{print $1}')
rsrq=$(echo "$output" | awk '{print $2}')
sinr=$(echo "$output" | awk '{print $3}')
[ "$rsrp" -eq -96 ]
[ "$rsrq" -eq -10 ]
[ "$sinr" -eq 13 ]
}
@test "modem_signal AT parse: rsrp=-97 rsrq=-10 sinr=14 (rsrp rsrq sinr order)" {
run _modem_parse_at < "$FIXTURES/modem-signal-at.txt"
[ "$status" -eq 0 ]
rsrp=$(echo "$output" | awk '{print $1}')
rsrq=$(echo "$output" | awk '{print $2}')
sinr=$(echo "$output" | awk '{print $3}')
[ "$rsrp" -eq -97 ]
[ "$rsrq" -eq -10 ]
[ "$sinr" -eq 14 ]
}
@test "modem_signal ubus parse: empty input exits 1" {
run _modem_parse_ubus < /dev/null
[ "$status" -ne 0 ]
[ -z "$output" ]
}
@test "modem_signal AT parse: empty input exits 1" {
run _modem_parse_at < /dev/null
[ "$status" -ne 0 ]
[ -z "$output" ]
}
@test "modem_sim_switch: rejects invalid slot" {
export MODEM_AT_CMD=/bin/true MODEM_BUS=1-1.2
run modem_sim_switch 3
[ "$status" -eq 1 ]
}
@test "modem_sim_switch: accepts slot 1" {
# Stub gl_modem to emit 'OK' and stub ifdown/ifup
mkdir -p "$BATS_TMPDIR/stub"
printf '#!/bin/sh
echo OK
' > "$BATS_TMPDIR/stub/gl_modem"
printf '#!/bin/sh
exit 0
' > "$BATS_TMPDIR/stub/ifdown"
printf '#!/bin/sh
exit 0
' > "$BATS_TMPDIR/stub/ifup"
chmod +x "$BATS_TMPDIR/stub/gl_modem" "$BATS_TMPDIR/stub/ifdown" "$BATS_TMPDIR/stub/ifup"
export PATH="$BATS_TMPDIR/stub:$PATH"
export MODEM_AT_CMD=gl_modem MODEM_BUS=1-1.2
run modem_sim_switch 1
[ "$status" -eq 0 ]
}
# --- Task 4.2: SIM auto-switch ---
_sim_make_stub() {
mkdir -p "$BATS_TMPDIR/sim_stub"
printf '#!/bin/sh
case "$*" in *QUIMSLOT\?*) echo "+QUIMSLOT: 1"; echo "OK" ;; *QUIMSLOT=*) echo "OK" ;; *) echo "OK" ;; esac
' > "$BATS_TMPDIR/sim_stub/gl_modem"
printf '#!/bin/sh
exit 0
' > "$BATS_TMPDIR/sim_stub/ifdown"
printf '#!/bin/sh
exit 0
' > "$BATS_TMPDIR/sim_stub/ifup"
chmod +x "$BATS_TMPDIR/sim_stub/gl_modem" "$BATS_TMPDIR/sim_stub/ifdown" "$BATS_TMPDIR/sim_stub/ifup"
export PATH="$BATS_TMPDIR/sim_stub:$PATH"
export MODEM_AT_CMD=gl_modem MODEM_BUS=1-1.2
export SIM_STATE_DIR="$BATS_TMPDIR/simstate"
export SIM_SWITCH_RSRP=-80 # threshold above actual -99 bench value -> easy to trigger
export SIM_SWITCH_CYCLES=3
export SIM_SWITCH_COOLDOWN=3600
rm -rf "$BATS_TMPDIR/simstate"
mkdir -p "$BATS_TMPDIR/simstate"
}
@test "sim_check: good signal resets counter and does not switch" {
_sim_make_stub
# First call with bad signal to set counter to 1
sim_check_and_switch -90 || true
bad=$(cat "$SIM_STATE_DIR/sim_bad_cycles" 2>/dev/null || echo 0)
[ "$bad" -eq 1 ]
# Now good signal resets
sim_check_and_switch -70 || true
bad=$(cat "$SIM_STATE_DIR/sim_bad_cycles" 2>/dev/null || echo 0)
[ "$bad" -eq 0 ]
}
@test "sim_check: triggers switch after SIM_SWITCH_CYCLES bad cycles" {
_sim_make_stub
# 2 bad cycles -> no switch yet
sim_check_and_switch -90 || true
sim_check_and_switch -90 || true
[ ! -f "$SIM_STATE_DIR/sim_last_switch" ]
# 3rd bad cycle -> switch triggered
run sim_check_and_switch -90
[ "$status" -eq 0 ]
[ -f "$SIM_STATE_DIR/sim_last_switch" ]
}
@test "sim_check: cooldown prevents switch within SIM_SWITCH_COOLDOWN" {
_sim_make_stub
export SIM_SWITCH_COOLDOWN=3600
# Pre-seed a recent switch time
date +%s > "$SIM_STATE_DIR/sim_last_switch"
run sim_check_and_switch -90
[ "$status" -eq 1 ] # blocked by cooldown
}
+20
View File
@@ -0,0 +1,20 @@
FIXTURES="$BATS_TEST_DIRNAME/../../tests/fixtures"
setup() { load '../../package/kit-busrouter/files/usr/lib/busrouter/metrics-net.sh'; }
@test "net: zero-jitter no-loss" {
run _net_parse_ping < "$FIXTURES/ping-ok.txt"
[ "$status" -eq 0 ]
[ "$output" = "15.0 0.0 0" ]
}
@test "net: jitter and packet loss" {
run _net_parse_ping < "$FIXTURES/ping-loss.txt"
[ "$output" = "15.0 5.0 20" ]
}
@test "net: 100pct loss gives 0 0 100" {
run _net_parse_ping < "$FIXTURES/ping-timeout.txt"
[ "$output" = "0 0 100" ]
}
@test "net: partial output defaults loss to 100" {
run _net_parse_ping < "$FIXTURES/ping-partial.txt"
[ "$output" = "20.0 0.0 100" ]
}
+16
View File
@@ -0,0 +1,16 @@
setup() { load '../../package/kit-busrouter/files/usr/lib/busrouter/lib-score.sh'; }
@test "latency: 5ms scores 100" { run score_latency 5; [ "$output" -eq 100 ]; }
@test "latency: 200ms scores 0" { run score_latency 200; [ "$output" -eq 0 ]; }
@test "latency clamps below 0" { run score_latency 500; [ "$output" -eq 0 ]; }
@test "download: 25MB/s scores 100" { run score_download 25; [ "$output" -eq 100 ]; }
@test "download: 0.5MB/s scores 0" { run score_download 0.5; [ "$output" -eq 0 ]; }
@test "upload: 12.5MB/s scores 100" { run score_upload 12.5; [ "$output" -eq 100 ]; }
@test "upload: 0.5MB/s scores 0" { run score_upload 0.5; [ "$output" -eq 0 ]; }
@test "jitter: 1ms scores 100" { run score_jitter 1; [ "$output" -eq 100 ]; }
@test "jitter: 50ms scores 0" { run score_jitter 50; [ "$output" -eq 0 ]; }
@test "loss: 0% scores 100" { run score_loss 0; [ "$output" -eq 100 ]; }
@test "loss: 40% scores 0" { run score_loss 40; [ "$output" -eq 0 ]; }
@test "signal: -65dBm scores 100" { run score_signal -65; [ "$output" -eq 100 ]; }
@test "signal: -95dBm scores 0" { run score_signal -95; [ "$output" -eq 0 ]; }
@test "signal clamps above 100" { run score_signal -50; [ "$output" -eq 100 ]; }
+1
View File
@@ -0,0 +1 @@
@test "bats runs" { run true; [ "$status" -eq 0 ]; }
+39
View File
@@ -0,0 +1,39 @@
SPEED_STATE_DIR="$BATS_TMPDIR/busrouter"
setup() {
load '../../package/kit-busrouter/files/usr/lib/busrouter/speedtest.sh'
mkdir -p "$SPEED_STATE_DIR"
export SPEED_STATE_DIR
}
teardown() { rm -rf "$SPEED_STATE_DIR"; }
@test "speed: CELL_METERED=1 skips cellular, emits 0 0" {
export CELL_METERED=1 CELLULAR_IFACE=rmnet_mhi0
run speed_test_iface rmnet_mhi0
[ "$status" -eq 0 ]
[ "$output" = "0 0" ]
}
@test "speed: CELL_METERED=0 does not skip cellular (returns non 0 0)" {
export CELL_METERED=0 CELLULAR_IFACE=rmnet_mhi0
run speed_test_iface rmnet_mhi0
[ "$output" != "0 0" ] || [ "$status" -ne 0 ]
}
@test "speed_test_due: no state file -> due (exit 0)" {
run speed_test_due eth0
[ "$status" -eq 0 ]
}
@test "speed_test_due: fresh state file -> not due (exit 1)" {
date +%s > "$SPEED_STATE_DIR/speed_last_eth0"
export SPEED_INTERVAL=3600
run speed_test_due eth0
[ "$status" -eq 1 ]
}
@test "speed_test_due: old state file -> due (exit 0)" {
echo "0" > "$SPEED_STATE_DIR/speed_last_eth0"
export SPEED_INTERVAL=300
run speed_test_due eth0
[ "$status" -eq 0 ]
}
+59
View File
@@ -0,0 +1,59 @@
FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
setup() {
load '../../package/kit-busrouter/files/usr/lib/busrouter/starlink.sh'
}
# Helper: run _starlink_parse on a fixture file
_parse_fixture() {
_starlink_parse < "$FIXTURES/$1"
}
@test "starlink: online dish: lat=28 dl=200000000 obstructed=0 outage=0" {
run _parse_fixture starlink-online.json
[ "$status" -eq 0 ]
lat=$(echo "$output" | awk '{print $1}')
dl=$(echo "$output" | awk '{print $2}')
obs=$(echo "$output" | awk '{print $3}')
outage=$(echo "$output" | awk '{print $4}')
[ "$lat" -eq 28 ]
[ "$dl" -gt 100000000 ]
[ "$obs" -eq 0 ]
[ "$outage" -eq 0 ]
}
@test "starlink: obstructed dish: obstructed=1" {
run _parse_fixture starlink-obstructed.json
[ "$status" -eq 0 ]
obs=$(echo "$output" | awk '{print $3}')
[ "$obs" -eq 1 ]
}
@test "starlink: obstructed dish: fractionObstructed>0.01 sets obstructed=1" {
run _parse_fixture starlink-obstructed.json
[ "$status" -eq 0 ]
obs=$(echo "$output" | awk '{print $3}')
[ "$obs" -eq 1 ]
}
@test "starlink: outage dish: outage=1" {
run _parse_fixture starlink-outage.json
[ "$status" -eq 0 ]
outage=$(echo "$output" | awk '{print $4}')
[ "$outage" -eq 1 ]
}
@test "starlink: outage dish: dl=0 lat=0" {
run _parse_fixture starlink-outage.json
[ "$status" -eq 0 ]
lat=$(echo "$output" | awk '{print $1}')
dl=$(echo "$output" | awk '{print $2}')
[ "$lat" -eq 0 ]
[ "$dl" -eq 0 ]
}
@test "starlink: grpcurl absent returns exit 1" {
export GRPCURL=/nonexistent/grpcurl
run starlink_status
[ "$status" -eq 1 ]
}
+225
View File
@@ -0,0 +1,225 @@
# telemetry-synology.bats — Tests for Synology telemetry agent
LIBDIR="$(cd "$BATS_TEST_DIRNAME/../.."/package/kit-busrouter/files/usr/lib/busrouter && pwd)"
setup() {
export LIB_DIR="$LIBDIR"
export AIWANBAL_STATE_DIR="$BATS_TMPDIR/aiwanbal"
export TELEMETRY_BUFFER_DIR="$BATS_TMPDIR/telemetry-buf"
export TELEMETRY_HUB="http://10.88.0.1:8080/api/telemetry"
rm -rf "$AIWANBAL_STATE_DIR" "$TELEMETRY_BUFFER_DIR"
mkdir -p "$AIWANBAL_STATE_DIR"
# Seed minimal aiwanbal state for collection tests
printf '85\n' > "$AIWANBAL_STATE_DIR/wan1_score_avg"
printf '15\n' > "$AIWANBAL_STATE_DIR/wan1_latency"
printf '50000000\n' > "$AIWANBAL_STATE_DIR/wan1_throughput" # 50 Mbps
printf '10000000\n' > "$AIWANBAL_STATE_DIR/wan1_upload"
printf '2\n' > "$AIWANBAL_STATE_DIR/wan1_jitter"
printf '0\n' > "$AIWANBAL_STATE_DIR/wan1_packet_loss"
printf -- '-80\n' > "$AIWANBAL_STATE_DIR/wan1_rsrp"
printf -- '-12\n' > "$AIWANBAL_STATE_DIR/wan1_rsrq"
printf '8\n' > "$AIWANBAL_STATE_DIR/wan1_sinr"
printf '5G-NR\n' > "$AIWANBAL_STATE_DIR/wan1_technology"
printf 'eyeride\n' > "$AIWANBAL_STATE_DIR/wan1_modem_type"
printf '70\n' > "$AIWANBAL_STATE_DIR/wan2_score_avg"
printf '25\n' > "$AIWANBAL_STATE_DIR/wan2_latency"
printf '30000000\n' > "$AIWANBAL_STATE_DIR/wan2_throughput"
printf '5000000\n' > "$AIWANBAL_STATE_DIR/wan2_upload"
printf '5\n' > "$AIWANBAL_STATE_DIR/wan2_jitter"
printf '1\n' > "$AIWANBAL_STATE_DIR/wan2_packet_loss"
printf -- '-90\n' > "$AIWANBAL_STATE_DIR/wan2_rsrp"
printf 'generic\n' > "$AIWANBAL_STATE_DIR/wan2_modem_type"
printf 'active\n' > "$AIWANBAL_STATE_DIR/wan0_mode"
# Fake device identity
mkdir -p /etc/busrouter
printf 'B0042\n' > /etc/busrouter/device-id
printf '1.0\n' > /etc/busrouter/version
# Load the library under test
load '../../package/kit-busrouter/files/usr/lib/busrouter/telemetry-synology.sh'
}
teardown() {
rm -rf "$AIWANBAL_STATE_DIR" "$TELEMETRY_BUFFER_DIR"
rm -f /etc/busrouter/device-id /etc/busrouter/version
}
# ── Data collection ──────────────────────────────────────────────────────────────
@test "telemetry_synology_collect: output contains required JSON keys" {
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"device_id"'
echo "$output" | grep -q '"timestamp"'
echo "$output" | grep -q '"uptime"'
echo "$output" | grep -q '"version"'
echo "$output" | grep -q '"platform":"synology"'
echo "$output" | grep -q '"gps"'
echo "$output" | grep -q '"wan"'
echo "$output" | grep -q '"wan1"'
echo "$output" | grep -q '"wan2"'
echo "$output" | grep -q '"starlink"'
echo "$output" | grep -q '"sel_primary"'
}
@test "telemetry_synology_collect: device_id from identity file" {
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"device_id":"B0042"'
}
@test "telemetry_synology_collect: falls back to hostname when identity file missing" {
rm -f /etc/busrouter/device-id
run telemetry_synology_collect
[ "$status" -eq 0 ]
# Should contain a device_id (whatever hostname returns)
echo "$output" | grep -q '"device_id"'
}
@test "telemetry_synology_collect: handles missing state files gracefully" {
rm -rf "$AIWANBAL_STATE_DIR"
mkdir -p "$AIWANBAL_STATE_DIR"
# No state files at all — should still produce valid JSON
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"wan1"'
echo "$output" | grep -q '"wan2"'
}
@test "telemetry_synology_collect: sel_primary is wan1 in active mode" {
printf 'active\n' > "$AIWANBAL_STATE_DIR/wan0_mode"
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"sel_primary":"wan1"'
}
@test "telemetry_synology_collect: sel_primary is wan2 in failover when wan2 up" {
printf 'failover\n' > "$AIWANBAL_STATE_DIR/wan0_mode"
printf 'up\n' > "$AIWANBAL_STATE_DIR/wan2_state"
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"sel_primary":"wan2"'
}
@test "telemetry_synology_collect: wan JSON includes cellular metadata" {
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"rsrp_dbm"'
echo "$output" | grep -q '"technology"'
echo "$output" | grep -q '"carrier"'
echo "$output" | grep -q '"5G-NR"'
}
@test "telemetry_synology_collect: throughput converted from B/s to Mbps" {
run telemetry_synology_collect
[ "$status" -eq 0 ]
# 50000000 B/s = ~400 Mbps → should appear as "dl_mbps":400.0 or similar
echo "$output" | grep -q '"dl_mbps"'
}
@test "telemetry_synology_collect: gps is null when Eyeride unavailable" {
# Without EYERIDE_PASSWORD, eyeride_gps returns 1 → gps fields are null
unset EYERIDE_PASSWORD
run telemetry_synology_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"lat":null'
echo "$output" | grep -q '"lon":null'
echo "$output" | grep -q '"fix":null'
}
# ── Buffering + POST ─────────────────────────────────────────────────────────────
@test "telemetry_synology_send: buffers payload when curl fails" {
mkdir -p "$BATS_TMPDIR/tel_stub"
printf '#!/bin/sh\nexit 7\n' > "$BATS_TMPDIR/tel_stub/curl"
chmod +x "$BATS_TMPDIR/tel_stub/curl"
export PATH="$BATS_TMPDIR/tel_stub:$PATH"
export TELEMETRY_BUFFER_DIR="$BATS_TMPDIR/buf_test"
rm -rf "$TELEMETRY_BUFFER_DIR"
run telemetry_synology_send
[ -d "$TELEMETRY_BUFFER_DIR" ]
buf_count=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | wc -l)
[ "$buf_count" -ge 1 ]
}
@test "telemetry_synology_flush: posts and removes buffered file on success" {
mkdir -p "$BATS_TMPDIR/tel_stub2"
printf '#!/bin/sh\necho 200\n' > "$BATS_TMPDIR/tel_stub2/curl"
chmod +x "$BATS_TMPDIR/tel_stub2/curl"
export PATH="$BATS_TMPDIR/tel_stub2:$PATH"
export TELEMETRY_BUFFER_DIR="$BATS_TMPDIR/flush_test"
mkdir -p "$TELEMETRY_BUFFER_DIR"
echo '{"test":1}' > "$TELEMETRY_BUFFER_DIR/1234567890_test.json"
run telemetry_synology_flush
[ "$status" -eq 0 ]
[ ! -f "$TELEMETRY_BUFFER_DIR/1234567890_test.json" ]
}
@test "telemetry_synology_flush: stops on first failure" {
mkdir -p "$BATS_TMPDIR/tel_stub3"
# curl that fails on second call
printf '#!/bin/sh\nif [ -f /tmp/first_done ]; then exit 7; fi\ntouch /tmp/first_done; echo 200\n' > "$BATS_TMPDIR/tel_stub3/curl"
chmod +x "$BATS_TMPDIR/tel_stub3/curl"
export PATH="$BATS_TMPDIR/tel_stub3:$PATH"
export TELEMETRY_BUFFER_DIR="$BATS_TMPDIR/flush_fail"
rm -f /tmp/first_done
mkdir -p "$TELEMETRY_BUFFER_DIR"
echo '{"test":1}' > "$TELEMETRY_BUFFER_DIR/1111111111_a.json"
echo '{"test":2}' > "$TELEMETRY_BUFFER_DIR/2222222222_b.json"
run telemetry_synology_flush
[ "$status" -ne 0 ]
# First file should be deleted, second should remain
[ ! -f "$TELEMETRY_BUFFER_DIR/1111111111_a.json" ]
[ -f "$TELEMETRY_BUFFER_DIR/2222222222_b.json" ]
rm -f /tmp/first_done
}
# ── WAN JSON fragment ────────────────────────────────────────────────────────────
@test "_synology_wan_json: produces valid JSON fragment" {
run _synology_wan_json 1
[ "$status" -eq 0 ]
# Should be a valid JSON object (starts with {, ends with })
echo "$output" | grep -q '^{'
echo "$output" | grep -q '}$'
echo "$output" | grep -q '"score"'
echo "$output" | grep -q '"latency_ms"'
echo "$output" | grep -q '"carrier"'
}
@test "_synology_wan_json: defaults score to 0 when file missing" {
rm -f "$AIWANBAL_STATE_DIR/wan1_score_avg"
run _synology_wan_json 1
[ "$status" -eq 0 ]
echo "$output" | grep -q '"score":0'
}
@test "_synology_wan_json: modem type maps to carrier name" {
printf 'peplink\n' > "$AIWANBAL_STATE_DIR/wan1_modem_type"
run _synology_wan_json 1
[ "$status" -eq 0 ]
echo "$output" | grep -q '"carrier":"Peplink"'
}
# ── eyeride_gps ──────────────────────────────────────────────────────────────────
@test "eyeride_gps: returns 1 when no password set" {
unset EYERIDE_PASSWORD
run eyeride_gps
[ "$status" -ne 0 ]
}
@test "eyeride_gps: returns 1 when eyeride unreachable" {
export EYERIDE_PASSWORD="test"
export EYERIDE_IP="192.0.2.1" # TEST-NET-1 — guaranteed unreachable
export EYERIDE_PORT="9999"
run eyeride_gps
[ "$status" -ne 0 ]
}
+73
View File
@@ -0,0 +1,73 @@
FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
LIBDIR="$(cd "$BATS_TEST_DIRNAME/../.."/package/kit-busrouter/files/usr/lib/busrouter && pwd)"
setup() {
export LIB_DIR="$LIBDIR"
load '../../package/kit-busrouter/files/usr/lib/busrouter/telemetry.sh'
}
@test "gps_fix parse: 3D fix returns lat lon fix_quality=3" {
run _gps_parse < "$FIXTURES/gps-fix.txt"
[ "$status" -eq 0 ]
lat=$(echo "$output" | awk '{print $1}')
lon=$(echo "$output" | awk '{print $2}')
fix=$(echo "$output" | awk '{print $3}')
[ "$lat" = "40.712776" ]
[ "$lon" = "-74.005974" ]
[ "$fix" -eq 3 ]
}
@test "gps_fix parse: no-fix response exits 1" {
run _gps_parse < "$FIXTURES/gps-nofix.txt"
[ "$status" -ne 0 ]
[ -z "$output" ]
}
@test "gps_fix parse: empty input exits 1" {
run _gps_parse < /dev/null
[ "$status" -ne 0 ]
[ -z "$output" ]
}
# --- Task 5.2: telemetry collect + POST ---
@test "telemetry_collect: output contains required JSON keys" {
run telemetry_collect
[ "$status" -eq 0 ]
echo "$output" | grep -q '"device_id"'
echo "$output" | grep -q '"timestamp"'
echo "$output" | grep -q '"gps"'
echo "$output" | grep -q '"wan"'
echo "$output" | grep -q '"starlink"'
echo "$output" | grep -q '"sim_slot"'
echo "$output" | grep -q '"uptime"'
}
@test "telemetry_send: buffers payload when curl fails" {
mkdir -p "$BATS_TMPDIR/tel_stub"
printf '#!/bin/sh\nexit 7\n' > "$BATS_TMPDIR/tel_stub/curl"
chmod +x "$BATS_TMPDIR/tel_stub/curl"
export PATH="$BATS_TMPDIR/tel_stub:$PATH"
export TELEMETRY_HUB="http://10.88.0.1:8080/api/telemetry"
export TELEMETRY_BUFFER_DIR="$BATS_TMPDIR/tel_buf"
rm -rf "$TELEMETRY_BUFFER_DIR"
run telemetry_send
[ -d "$TELEMETRY_BUFFER_DIR" ]
buf_count=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | wc -l)
[ "$buf_count" -ge 1 ]
}
@test "telemetry_flush: posts and removes buffered file on success" {
mkdir -p "$BATS_TMPDIR/tel_stub2"
printf '#!/bin/sh\necho 200\n' > "$BATS_TMPDIR/tel_stub2/curl"
chmod +x "$BATS_TMPDIR/tel_stub2/curl"
export PATH="$BATS_TMPDIR/tel_stub2:$PATH"
export TELEMETRY_HUB="http://10.88.0.1:8080/api/telemetry"
export TELEMETRY_BUFFER_DIR="$BATS_TMPDIR/tel_flush_buf"
mkdir -p "$TELEMETRY_BUFFER_DIR"
echo '{"test":1}' > "$TELEMETRY_BUFFER_DIR/1234567890_test.json"
run telemetry_flush
[ "$status" -eq 0 ]
[ ! -f "$TELEMETRY_BUFFER_DIR/1234567890_test.json" ]
}
+2
View File
@@ -0,0 +1,2 @@
+QGPSLOC: 154321.000,40.712776,-74.005974,1.1,10.2,3,0.00,0.0,0.0,010124,08
OK
+1
View File
@@ -0,0 +1 @@
+CME ERROR: 516
+2
View File
@@ -0,0 +1,2 @@
+QCSQ: "NR5G",-97,14,-10
OK
+7
View File
@@ -0,0 +1,7 @@
{
"signals": [{
"sinr": 13, "slot": 1, "network_type": "NR5G-SA",
"timestamp": 1782917019, "strength": 4,
"rsrp": -96, "rsrq": -10, "mode": 5
}]
}
+13
View File
@@ -0,0 +1,13 @@
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=55 time=10.0 ms
64 bytes from 1.1.1.1: seq=1 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=2 ttl=55 time=10.0 ms
64 bytes from 1.1.1.1: seq=3 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=4 ttl=55 time=10.0 ms
64 bytes from 1.1.1.1: seq=5 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=6 ttl=55 time=10.0 ms
64 bytes from 1.1.1.1: seq=7 ttl=55 time=20.0 ms
--- 1.1.1.1 ping statistics ---
10 packets transmitted, 8 packets received, 20% packet loss
round-trip min/avg/max = 10.0/15.0/20.0 ms
+15
View File
@@ -0,0 +1,15 @@
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=1 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=2 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=3 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=4 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=5 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=6 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=7 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=8 ttl=55 time=15.0 ms
64 bytes from 1.1.1.1: seq=9 ttl=55 time=15.0 ms
--- 1.1.1.1 ping statistics ---
10 packets transmitted, 10 packets received, 0% packet loss
round-trip min/avg/max = 15.0/15.0/15.0 ms
+6
View File
@@ -0,0 +1,6 @@
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=1 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=2 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=3 ttl=55 time=20.0 ms
64 bytes from 1.1.1.1: seq=4 ttl=55 time=20.0 ms
+4
View File
@@ -0,0 +1,4 @@
PING 1.1.1.1 (1.1.1.1): 56 data bytes
--- 1.1.1.1 ping statistics ---
10 packets transmitted, 0 packets received, 100% packet loss
+13
View File
@@ -0,0 +1,13 @@
{
"dishGetStatus": {
"obstructionStats": {
"fractionObstructed": 0.35,
"currentlyObstructed": true,
"last24hObstructedS": 1200
},
"downlinkThroughputBps": 5000000.0,
"uplinkThroughputBps": 1000000.0,
"popPingLatencyMs": 95.0,
"popPingDropRate": 0.12
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"dishGetStatus": {
"deviceInfo": {
"id": "ut01000000-00000000-00000000",
"hardwareVersion": "rev3_proto3",
"softwareVersion": "2024.01.01.0"
},
"deviceState": {
"uptimeS": "86400"
},
"obstructionStats": {
"fractionObstructed": 0.002,
"currentlyObstructed": false,
"last24hObstructedS": 5
},
"downlinkThroughputBps": 200000000.0,
"uplinkThroughputBps": 10000000.0,
"popPingLatencyMs": 28.5,
"popPingDropRate": 0.0
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"dishGetStatus": {
"obstructionStats": {
"fractionObstructed": 0.0,
"currentlyObstructed": false
},
"downlinkThroughputBps": 0.0,
"uplinkThroughputBps": 0.0,
"popPingLatencyMs": 0.0,
"outage": {
"cause": "NO_SATS",
"startTimestampNs": "1751337600000000000",
"duration": 30
}
}
}