Chatzotzrot: silver trumpets with BEAM routing, subscription, and zikaron
Bamidbar 10:2 — Aseh lekha shtei chatzotz'rot kesef, mikshah ta'aseh otam.
Two trumpets, hammered from a single piece. Five signals, five patterns.
- Upgraded from simple function module to GenServer with state
- Recipient routing: :all (assembly_all, breaking_camp, moadim),
:kohanim (assembly_leaders, war), :operators (war)
- PubSub via BEAM messaging: subscribe/1, unsubscribe/1
Signals delivered as {:chatzotzrot, record} messages
- Zikaron (remembrance — Bamidbar 10:10): signal history with
type filtering and limit. V'hayu lakhem l'zikaron.
- Dead subscriber cleanup via Process.monitor/1
- Added to supervision tree in application.ex
- 30 new tests: patterns, urgency, routing, subscription,
zikaron history, dead process cleanup, format compliance
- All 125 Elixir tests pass, 0 failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d34e2799ac
commit
7fe8025434
3 files changed
+533
-24
No files matched your search
@@ -0,0 +1,288 @@
|
||||
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
|
||||
Reference in new issue
Block a user