defmodule VavPipeline.ChatzotzrotTest do @moduledoc """ Tests for the Chatzotzrot signaling system. Bamidbar 10:2 — Aseh lekha shtei chatzotz'rot kesef, mikshah ta'aseh otam. "Make for yourself two trumpets of silver; of hammered work you shall make them." Five signals. Five patterns. Routing by recipient group. Every signal recorded in the zikaron (remembrance). """ use ExUnit.Case, async: false alias VavPipeline.Chatzotzrot # ============================================================ # Signal Type → Pattern Mapping (Bamidbar 10, Architecture §3.12) # ============================================================ describe "signal patterns" do test "assembly_all → tekiah" do signal = Chatzotzrot.signal(:assembly_all) assert signal.signal_type == :assembly_all assert signal.pattern == :tekiah assert signal.source == :chatzotzrot assert signal.timestamp != nil end test "assembly_leaders → teruah" do signal = Chatzotzrot.signal(:assembly_leaders) assert signal.signal_type == :assembly_leaders assert signal.pattern == :teruah end test "breaking_camp → tekiah_teruah" do signal = Chatzotzrot.signal(:breaking_camp) assert signal.signal_type == :breaking_camp assert signal.pattern == :tekiah_teruah end test "war → teruah_teruah" do signal = Chatzotzrot.signal(:war) assert signal.signal_type == :war assert signal.pattern == :teruah_teruah end test "moadim → tekiah_tekiah" do signal = Chatzotzrot.signal(:moadim) assert signal.signal_type == :moadim assert signal.pattern == :tekiah_tekiah end end # ============================================================ # Urgency (Construction §3.5) # ============================================================ describe "urgency" do test "breaking_camp is immediate" do signal = Chatzotzrot.signal(:breaking_camp) assert signal.details.urgency == :immediate end test "war is immediate" do signal = Chatzotzrot.signal(:war) assert signal.details.urgency == :immediate end test "assembly_all is next_cycle" do signal = Chatzotzrot.signal(:assembly_all) assert signal.details.urgency == :next_cycle end test "assembly_leaders is informational" do signal = Chatzotzrot.signal(:assembly_leaders) assert signal.details.urgency == :informational end test "moadim is informational" do signal = Chatzotzrot.signal(:moadim) assert signal.details.urgency == :informational end test "urgency can be overridden" do signal = Chatzotzrot.signal(:moadim, urgency: :immediate) assert signal.details.urgency == :immediate end end # ============================================================ # Custom reason (Construction §3.5) # ============================================================ describe "reason" do test "default reason is the signal type name" do signal = Chatzotzrot.signal(:war) assert signal.details.reason == "war" end test "custom reason is preserved" do signal = Chatzotzrot.signal(:war, reason: "DDoS detected on API layer") assert signal.details.reason == "DDoS detected on API layer" end end # ============================================================ # Subscription and Routing (Architecture §3.12) # ============================================================ describe "subscription and routing" do test "subscriber to :all receives assembly_all signal" do Chatzotzrot.subscribe(:all) Chatzotzrot.signal(:assembly_all) assert_receive {:chatzotzrot, %{signal_type: :assembly_all, pattern: :tekiah}} end test "subscriber to :all receives breaking_camp signal" do Chatzotzrot.subscribe(:all) Chatzotzrot.signal(:breaking_camp) assert_receive {:chatzotzrot, %{signal_type: :breaking_camp, pattern: :tekiah_teruah}} end test "subscriber to :all receives moadim signal" do Chatzotzrot.subscribe(:all) Chatzotzrot.signal(:moadim) assert_receive {:chatzotzrot, %{signal_type: :moadim, pattern: :tekiah_tekiah}} end test "subscriber to :all does NOT receive assembly_leaders" do Chatzotzrot.subscribe(:all) Chatzotzrot.signal(:assembly_leaders) refute_receive {:chatzotzrot, %{signal_type: :assembly_leaders}}, 100 end test "subscriber to :all does NOT receive war" do Chatzotzrot.subscribe(:all) Chatzotzrot.signal(:war) refute_receive {:chatzotzrot, %{signal_type: :war}}, 100 end test "subscriber to :kohanim receives assembly_leaders" do Chatzotzrot.subscribe(:kohanim) Chatzotzrot.signal(:assembly_leaders) assert_receive {:chatzotzrot, %{signal_type: :assembly_leaders, pattern: :teruah}} end test "subscriber to :kohanim receives war" do Chatzotzrot.subscribe(:kohanim) Chatzotzrot.signal(:war) assert_receive {:chatzotzrot, %{signal_type: :war, pattern: :teruah_teruah}} end test "subscriber to :operators receives war" do Chatzotzrot.subscribe(:operators) Chatzotzrot.signal(:war) assert_receive {:chatzotzrot, %{signal_type: :war, pattern: :teruah_teruah}} end test "subscriber to :operators does NOT receive assembly_all" do Chatzotzrot.subscribe(:operators) Chatzotzrot.signal(:assembly_all) refute_receive {:chatzotzrot, _}, 100 end test "process subscribed to multiple groups receives signal once" do # War routes to both :operators and :kohanim Chatzotzrot.subscribe(:operators) Chatzotzrot.subscribe(:kohanim) Chatzotzrot.signal(:war) assert_receive {:chatzotzrot, %{signal_type: :war}} refute_receive {:chatzotzrot, %{signal_type: :war}}, 100 end test "unsubscribe stops signal delivery" do Chatzotzrot.subscribe(:all) Chatzotzrot.signal(:assembly_all) assert_receive {:chatzotzrot, %{signal_type: :assembly_all}} Chatzotzrot.unsubscribe(:all) Chatzotzrot.signal(:assembly_all) refute_receive {:chatzotzrot, _}, 100 end end # ============================================================ # Zikaron — Remembrance (Bamidbar 10:10) # ============================================================ describe "zikaron (signal history)" do test "signals are recorded in zikaron" do initial_count = Chatzotzrot.count() Chatzotzrot.signal(:assembly_all, reason: "test zikaron") Chatzotzrot.signal(:war, reason: "test zikaron war") history = Chatzotzrot.zikaron() assert length(history) >= 2 # Newest first [newest | _] = history assert newest.signal_type == :war assert newest.details.reason == "test zikaron war" assert Chatzotzrot.count() >= initial_count + 2 end test "zikaron can be filtered by type" do Chatzotzrot.signal(:moadim, reason: "zikaron filter test") Chatzotzrot.signal(:war, reason: "zikaron filter test") moadim_history = Chatzotzrot.zikaron(type: :moadim) assert Enum.all?(moadim_history, &(&1.signal_type == :moadim)) end test "zikaron can be limited" do Chatzotzrot.signal(:assembly_all) Chatzotzrot.signal(:assembly_all) Chatzotzrot.signal(:assembly_all) limited = Chatzotzrot.zikaron(limit: 2) assert length(limited) == 2 end test "zikaron can be filtered and limited" do Chatzotzrot.signal(:moadim, reason: "combined filter 1") Chatzotzrot.signal(:moadim, reason: "combined filter 2") Chatzotzrot.signal(:moadim, reason: "combined filter 3") result = Chatzotzrot.zikaron(type: :moadim, limit: 1) assert length(result) == 1 assert hd(result).signal_type == :moadim end end # ============================================================ # Dead subscriber cleanup # ============================================================ describe "dead subscriber cleanup" do test "dead processes are removed from subscriber list" do # Spawn a process that subscribes then dies pid = spawn(fn -> Chatzotzrot.subscribe(:all) receive do :stop -> :ok end end) # Give it time to subscribe Process.sleep(50) send(pid, :stop) Process.sleep(50) # Signal should not crash — dead subscriber is cleaned up signal = Chatzotzrot.signal(:assembly_all) assert signal.signal_type == :assembly_all end end # ============================================================ # Construction §3.5 — signal format compliance # ============================================================ describe "signal format (Construction §3.5)" do test "signal record has all required fields" do signal = Chatzotzrot.signal(:assembly_all, reason: "format test") assert Map.has_key?(signal, :signal_type) assert Map.has_key?(signal, :pattern) assert Map.has_key?(signal, :timestamp) assert Map.has_key?(signal, :source) assert Map.has_key?(signal, :details) assert Map.has_key?(signal.details, :reason) assert Map.has_key?(signal.details, :urgency) assert signal.source == :chatzotzrot assert is_binary(signal.timestamp) end end end