# frozen_string_literal: true # Zero-dependency tests for the reverse-channel command protocol (PC -> device call control). # Run: ruby components/backend/test/test_commands.rb require_relative "../lib/helpdesk/domain" require_relative "../seed" $pass = 0; $fail = 0 def ok(desc); $pass += 1; puts " ok #{desc}"; end def bad(desc, got = nil); $fail += 1; puts " FAIL #{desc}#{got.nil? ? '' : " (got: #{got.inspect})"}"; end def eq(a, b, desc); a == b ? ok(desc) : bad(desc, a); end def truthy(x, desc); x ? ok(desc) : bad(desc, x); end NOW = Time.utc(2026, 7, 2, 9, 0, 0) CID = "22222222-2222-4222-8222-222222222222" # a service with one ringing call on device "dev1" def ringing(device: "dev1") s = Helpdesk::Seed.call(Helpdesk::Service.new(Helpdesk::Store.new, clock: -> { NOW })) s.incoming(call_id: CID, ts: NOW.iso8601, number: "601 234 567", presentation: "allowed", dialed_did: "+420800111222", device_id: device) s end def cmds(s, dev = "dev1") = s.store.commands[dev] puts "issue_command enqueues for the call's device" s = ringing r = s.issue_command(call_id: CID, verb: "answer") eq(r[:verb], "answer", "returns the queued verb") eq(r[:queued_for], "dev1", "queued for the call's device") eq(cmds(s).size, 1, "one command pending on dev1") truthy(cmds(s).first[:id], "command carries an idempotency id") puts "validation" eq(s.issue_command(call_id: "nope", verb: "answer"), nil, "unknown call -> nil") eq(s.issue_command(call_id: CID, verb: "explode")[:error], "unknown_verb", "unknown verb -> error") eq(s.issue_command(call_id: CID, verb: "dtmf", arg: "X")[:error], "bad_arg", "bad dtmf arg -> error") truthy(s.issue_command(call_id: CID, verb: "dtmf", arg: "5"), "valid dtmf arg accepted") eq(cmds(s).select { |c| c[:verb] == "explode" }.size, 0, "invalid verb never enqueued") puts "drain returns then clears, and is per-device" s = ringing s.issue_command(call_id: CID, verb: "answer") drained = s.drain_commands("dev1") eq(drained.size, 1, "drain returns pending") eq(s.drain_commands("dev1").size, 0, "second drain is empty (cleared)") s.issue_command(call_id: CID, verb: "answer") eq(s.drain_commands("other-device").size, 0, "a different device drains nothing") puts "apply: answer/hangup/reject drive the real state machine" s = ringing s.issue_command(call_id: CID, verb: "answer") s.drain_commands("dev1").each { |c| s.apply_command(c) } eq(s.store.event_by_call_id(CID)[:state], "answered", "answer command -> answered") s = ringing s.issue_command(call_id: CID, verb: "hangup") res = s.apply_command(s.drain_commands("dev1").first) eq(s.store.event_by_call_id(CID)[:state], "ended", "hangup command -> ended") truthy(res[:applied], "apply result marks applied") s = ringing s.issue_command(call_id: CID, verb: "reject") s.apply_command(s.drain_commands("dev1").first) ev = s.store.event_by_call_id(CID) eq(ev[:state], "ended", "reject command -> ended") eq(ev[:disconnect_cause], "rejected", "reject sets disconnect_cause=rejected") puts "apply: live toggles set attributes reflected in event_view" s = ringing s.apply_command({ id: "x", call_id: CID, verb: "answer" }) s.apply_command({ id: "x", call_id: CID, verb: "mute" }) eq(s.event_view(s.store.event_by_call_id(CID))[:muted], true, "mute -> muted:true in view") s.apply_command({ id: "x", call_id: CID, verb: "unmute" }) eq(s.event_view(s.store.event_by_call_id(CID))[:muted], false, "unmute -> muted:false") s.apply_command({ id: "x", call_id: CID, verb: "hold" }) eq(s.event_view(s.store.event_by_call_id(CID))[:on_hold], true, "hold -> on_hold:true") s.apply_command({ id: "x", call_id: CID, verb: "resume" }) eq(s.event_view(s.store.event_by_call_id(CID))[:on_hold], false, "resume -> on_hold:false") s.apply_command({ id: "x", call_id: CID, verb: "route", arg: "bluetooth" }) eq(s.event_view(s.store.event_by_call_id(CID))[:audio_route], "bluetooth", "route -> audio_route set") s.apply_command({ id: "x", call_id: CID, verb: "dtmf", arg: "7" }) eq(s.store.event_by_call_id(CID)[:dtmf_log], ["7"], "dtmf appended to dtmf_log") puts "idempotency / monotonic guard" s = ringing 2.times { s.apply_command({ id: "x", call_id: CID, verb: "answer" }) } eq(s.store.event_by_call_id(CID)[:state], "answered", "double answer stays answered (idempotent)") s.apply_command({ id: "x", call_id: CID, verb: "hangup" }) s.apply_command({ id: "x", call_id: CID, verb: "answer" }) # late answer after ended eq(s.store.event_by_call_id(CID)[:state], "ended", "answer after ended is ignored (monotonic)") puts "long-poll: drain blocks then returns the INSTANT a command is issued (~1 RTT, not the full wait)" s = ringing got = nil; elapsed = nil mono = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } th = Thread.new do t0 = mono.call got = s.drain_commands("dev1", wait_s: 5) # blocks up to 5s elapsed = mono.call - t0 end sleep 0.15 # let the drain reach @cmd_cond.wait s.issue_command(call_id: CID, verb: "mute") # another thread enqueues -> must wake the drain th.join(3) truthy(got && got.length == 1, "long-poll returned the enqueued command") eq(got && got.first[:verb], "mute", "delivered the mute command") truthy(elapsed && elapsed < 2.0, "woke fast, not after the full 5s wait (#{elapsed && elapsed.round(3)}s)") puts "long-poll: returns [] on timeout when no command arrives" s = ringing t0 = mono.call got = s.drain_commands("dev1", wait_s: 1) dt = mono.call - t0 eq(got, [], "empty result after the wait") truthy(dt >= 0.9 && dt < 2.5, "waited ~the full 1s timeout (#{dt.round(3)}s)") puts "long-poll: wait_s 0 (default) stays an instant plain drain (dev endpoints + tests)" s = ringing s.issue_command(call_id: CID, verb: "answer") t0 = mono.call got = s.drain_commands("dev1") dt = mono.call - t0 eq(got.length, 1, "plain drain returns the queued command") truthy(dt < 0.2, "plain drain is instant (#{dt.round(3)}s)") puts "device presence: the command poll is the heartbeat for 'waiting for the phone to connect'" s = ringing st = s.device_status eq(st[:connected], false, "no poll yet -> not connected (console shows 'waiting')") eq(st[:devices], [], "no devices seen") s.drain_commands("dev1") # a poll from the phone st = s.device_status eq(st[:connected], true, "after a poll -> connected") truthy(st[:devices].any? { |d| d[:device_id] == "dev1" && d[:connected] }, "dev1 present with fresh last-seen") truthy(st[:devices].first[:last_seen_ago_s] < 5, "last-seen is recent") puts "\n#{$pass} passed, #{$fail} failed" exit($fail.zero? ? 0 : 1)