--[[ test_state.lua — Watchdog state machine unit tests Run: lua5.1 tests/test_state.lua ]] package.path = package.path .. ";./src/?.lua" local M = require("balancerlite.state") local state = M.state local function assert_eq(a, b, msg) if a ~= b then error(((msg or "") .. ": expected " .. tostring(b) .. ", got " .. tostring(a)), 2) end end local TBASE = 1000000000 local function feed_ok(s, wan_id, n, base) base = base or TBASE for i = 1, (n or 1) do M.feed(s, wan_id, {ok=true, rtt_ms=5, ts=base+i}) end end local function feed_fail(s, wan_id, n, base) base = base or TBASE for i = 1, (n or 1) do M.feed(s, wan_id, {ok=false, ts=base+i}) end end print("=== State machine tests ===") -- TEST 1: INIT → WAN_A_PRIMARY local S = M.new({ down_threshold=3, up_threshold=3, flap_threshold=99, flap_window_s=300, flap_recovery=300 }) assert_eq(S.cur_state, state.INIT, "initial state") feed_ok(S, "wan-a", 3) local ev = M.advance(S) assert_eq(S.cur_state, state.WAN_A_PRIMARY, "wan-a up → WAN_A_PRIMARY") assert_eq(ev.type, "state.init", "event type") print(" TEST 1: INIT→WAN_A_PRIMARY: OK") -- TEST 2: Failover wan-a → wan-b S = M.new({ down_threshold=3, up_threshold=3, flap_threshold=99, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 3); M.advance(S) -- WAN_A_PRIMARY assert_eq(S.cur_state, state.WAN_A_PRIMARY, "setup: wan-a primary") feed_ok(S, "wan-b", 3); M.advance(S) -- wan-b is also up feed_fail(S, "wan-a", 3) ev = M.advance(S) assert_eq(S.cur_state, state.SWITCHING_TO_B, "wan-a down + wan-b up → SWITCHING_TO_B") assert_eq(ev.type, "failover.switch", "event type") ev = M.advance(S) assert_eq(S.cur_state, state.WAN_B_PRIMARY, "confirmed → WAN_B_PRIMARY") print(" TEST 2: wan-a→wan-b failover: OK") -- TEST 3: BOTH_DOWN when both fail S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=99, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 2); M.advance(S) feed_fail(S, "wan-a", 2); feed_fail(S, "wan-b", 2) ev = M.advance(S) assert_eq(S.cur_state, state.BOTH_DOWN, "both fail → BOTH_DOWN") assert_eq(ev.type, "state.both_down", "event type") print(" TEST 3: BOTH_DOWN: OK") -- TEST 4: Recovery from BOTH_DOWN → WAN_B_PRIMARY (direct, no SWITCHING) S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=99, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 2); M.advance(S) feed_fail(S, "wan-a", 2); feed_fail(S, "wan-b", 2) M.advance(S) -- BOTH_DOWN assert_eq(S.cur_state, state.BOTH_DOWN, "setup: BOTH_DOWN") feed_ok(S, "wan-b", 2) ev = M.advance(S) assert_eq(S.cur_state, state.WAN_B_PRIMARY, "wan-b recovers → WAN_B_PRIMARY") assert_eq(ev.type, "state.recovered", "event is state.recovered") print(" TEST 4: BOTH_DOWN recovery: OK") -- TEST 5: Hysteresis (up_thr > down_thr) S = M.new({ down_threshold=2, up_threshold=5, flap_threshold=99, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 5); M.advance(S) assert_eq(S.cur_state, state.WAN_A_PRIMARY, "setup: wan-a primary") feed_fail(S, "wan-a", 2); feed_fail(S, "wan-b", 2) ev = M.advance(S) assert_eq(S.cur_state, state.BOTH_DOWN, "both fail (2 fails each) → BOTH_DOWN") print(" TEST 5: hysteresis (up=5, down=2): OK") -- TEST 6: DEGRADED on flap (3 switches needed) -- Each switch: stable → failure+ok → SWITCHING → advance → stable (records switch). -- 3 switches = 3 advance() calls from stable states that changed = DEGRADED. S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=3, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 2); M.advance(S) -- → WAN_A_PRIMARY [switch 1] assert_eq(S.cur_state, state.WAN_A_PRIMARY) -- Switch 1: A→B feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2) M.advance(S) -- → SWITCHING_TO_B M.advance(S) -- → WAN_B_PRIMARY [switch 2] -- Switch 2: B→A feed_fail(S, "wan-b", 2); feed_ok(S, "wan-a", 2) M.advance(S) -- → SWITCHING_TO_A M.advance(S) -- → WAN_A_PRIMARY [switch 3] -- Switch 3: A→B — 3rd switch → DEGRADED feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2) M.advance(S) -- → SWITCHING_TO_B ev = M.advance(S) -- → DEGRADED (nswitches=3 >= flap_threshold=3) assert_eq(S.cur_state, state.DEGRADED, "3 prior switches → DEGRADED") assert_eq(ev.type, "flap.alert", "event is flap.alert") print(" TEST 6: flap → DEGRADED: OK") -- TEST 7: DEGRADED persists until flap_recovery (use flap_threshold=2 so BOTH_DOWN -- doesn't keep accumulating; BOTH_DOWN clears switch_times, so only count stable switches) S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=2, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 2); M.advance(S) -- → WAN_A_PRIMARY feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2) M.advance(S); M.advance(S) -- → WAN_B_PRIMARY [switch 1] assert_eq(S.cur_state, state.WAN_B_PRIMARY) -- Switch 2: B→A → DEGRADED (flap_threshold=2, 2nd switch triggers DEGRADED) feed_fail(S, "wan-b", 2); feed_ok(S, "wan-a", 2) M.advance(S); -- → SWITCHING_TO_A ev = M.advance(S) -- → WAN_A_PRIMARY [switch 2] → DEGRADED assert_eq(S.cur_state, state.DEGRADED, "2 switches → DEGRADED") assert_eq(ev.type, "flap.alert", "event is flap.alert") ev = M.advance(S) -- no new switches, no calm period → stays DEGRADED assert_eq(S.cur_state, state.DEGRADED, "still DEGRADED (no calm period)") print(" TEST 7: DEGRADED persists until calm: OK") -- TEST 8: Switch-back to preferred (A recovers while on B) S = M.new({ down_threshold=2, up_threshold=2, flap_threshold=99, flap_window_s=300, flap_recovery=300 }) feed_ok(S, "wan-a", 2); M.advance(S) -- WAN_A_PRIMARY feed_fail(S, "wan-a", 2); feed_ok(S, "wan-b", 2) M.advance(S) -- → SWITCHING_TO_B M.advance(S) -- → WAN_B_PRIMARY (now on wan-b) assert_eq(S.cur_state, state.WAN_B_PRIMARY, "on wan-b") feed_ok(S, "wan-a", 2) -- wan-a recovers ev = M.advance(S) assert_eq(S.cur_state, state.SWITCHING_TO_A, "wan-a recovered → SWITCHING_TO_A") ev = M.advance(S) assert_eq(S.cur_state, state.WAN_A_PRIMARY, "confirmed → WAN_A_PRIMARY") print(" TEST 8: switch-back to preferred: OK") print("PASS: state")