Tamid §1.1 wrote the pe'ah's limit (10/min, 100/h per IP); nothing enforced it, on any tier, and every admitted request spends manna. VavPipeline.BigdeiKodesh.Mishmar: fixed minute and hour windows per subject in ETS, the subject stored only as sha256(boot salt, tier, subject) — no IP and no key is ever in memory as itself, the sweep drops closed windows, nothing survives a restart (the Tamid's "not logged beyond the current request", kept in memory too). A refused knock is not counted. 429 RATE_LIMITED with Retry-After and the manna explanation (Shemot 16:18). Pe'ah per peer address; courtyard and chatzer per key, after the gate (a wrong key is 401 and never counted). Numbers: pe'ah 10/100 (Tamid), courtyard 20/200 and chatzer 5/60 (from Construction §2.3; config :rate_limits). Service shorashim-external: externalTrafficPolicy Local. With Cluster the pod saw every caller as the node, 91.92.153.113 (observed via /proc/net/tcp) — a per-IP limit would have been one bucket for the world. Spec: tamid §1.1 (watch, 429 rows, the Service note), KOHEN.md. Doorpost 42b38626… → e4e3d56d…. tablet46_mishmar_test; Elixir 533/0 (L3 on and off). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
136 lines
4.6 KiB
Elixir
136 lines
4.6 KiB
Elixir
defmodule VavPipeline.Tablet46MishmarTest do
|
|
@moduledoc """
|
|
Tablet 46 (2026-10-06) — no gate had a watch.
|
|
|
|
Tamid §1.1 writes the pe'ah's limit in the spec; nothing enforced it,
|
|
on any tier. Every admitted request spends manna; one caller could
|
|
gather the whole omer.
|
|
"""
|
|
|
|
use ExUnit.Case, async: false
|
|
|
|
import Plug.Test
|
|
import Plug.Conn
|
|
|
|
alias VavPipeline.BigdeiKodesh.{Mishmar, Router}
|
|
|
|
@opts Router.init([])
|
|
|
|
setup do
|
|
original = Application.get_env(:vav_pipeline, :rate_limits)
|
|
|
|
Application.put_env(:vav_pipeline, :rate_limits,
|
|
peah: [minute: 3, hour: 5],
|
|
courtyard: [minute: 2, hour: 100],
|
|
chatzer: [minute: 10_000, hour: 100_000]
|
|
)
|
|
|
|
Mishmar.reset()
|
|
|
|
on_exit(fn ->
|
|
Application.put_env(:vav_pipeline, :rate_limits, original)
|
|
Mishmar.reset()
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
describe "Mishmar.knock/3" do
|
|
test "counts the minute, then the hour, per subject; a refusal is not counted" do
|
|
t = 1_000_000 * 60
|
|
|
|
assert {:ok, 2} = Mishmar.knock(:peah, "a", t)
|
|
assert {:ok, 1} = Mishmar.knock(:peah, "a", t + 1)
|
|
assert {:ok, 0} = Mishmar.knock(:peah, "a", t + 2)
|
|
assert {:error, :minute, 57} = Mishmar.knock(:peah, "a", t + 3)
|
|
assert {:error, :minute, 57} = Mishmar.knock(:peah, "a", t + 3)
|
|
|
|
# Another subject is another count.
|
|
assert {:ok, 2} = Mishmar.knock(:peah, "b", t + 3)
|
|
|
|
# The next minute opens — but the hour (5) is nearly spent: 3 so far.
|
|
assert {:ok, 2} = Mishmar.knock(:peah, "a", t + 60)
|
|
assert {:ok, 1} = Mishmar.knock(:peah, "a", t + 61)
|
|
assert {:error, :hour, retry} = Mishmar.knock(:peah, "a", t + 62)
|
|
assert retry == 3600 - rem(t + 62, 3600)
|
|
|
|
# The next hour opens.
|
|
assert {:ok, 2} = Mishmar.knock(:peah, "a", t + 3600)
|
|
end
|
|
|
|
test "tiers are counted apart" do
|
|
t = 2_000_000 * 60
|
|
assert {:ok, 1} = Mishmar.knock(:courtyard, "k", t)
|
|
assert {:ok, 0} = Mishmar.knock(:courtyard, "k", t)
|
|
assert {:error, :minute, _} = Mishmar.knock(:courtyard, "k", t)
|
|
assert {:ok, 2} = Mishmar.knock(:peah, "k", t)
|
|
end
|
|
|
|
test "the sweep drops closed windows and nothing else" do
|
|
t = 3_000_000 * 60
|
|
assert {:ok, _} = Mishmar.knock(:peah, "old", t)
|
|
assert {:ok, _} = Mishmar.knock(:peah, "new", t + 7200)
|
|
:ok = Mishmar.sweep(Mishmar, t + 7200)
|
|
|
|
# "old" has been forgotten entirely; "new" is still counted.
|
|
assert :ets.info(Mishmar, :size) == 2
|
|
assert {:ok, 1} = Mishmar.knock(:peah, "new", t + 7200)
|
|
end
|
|
|
|
test "the table holds hashes, never the subject" do
|
|
:ok = Mishmar.reset()
|
|
{:ok, _} = Mishmar.knock(:peah, "203.0.113.7")
|
|
|
|
for {{id, _, _}, _} <- :ets.tab2list(Mishmar) do
|
|
assert is_binary(id) and byte_size(id) == 32
|
|
refute id =~ "203.0.113"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "at the router" do
|
|
test "the pe'ah: the fourth knock from one address in a minute is 429 with Retry-After and the manna explanation" do
|
|
statuses = for _ <- 1..4, do: ask({203, 0, 113, 9}).status
|
|
assert Enum.take(statuses, 3) |> Enum.all?(&(&1 != 429))
|
|
|
|
conn = ask({203, 0, 113, 9})
|
|
assert conn.status == 429
|
|
[retry] = get_resp_header(conn, "retry-after")
|
|
assert String.to_integer(retry) in 1..60
|
|
body = Jason.decode!(conn.resp_body)
|
|
assert body["error"]["code"] == "RATE_LIMITED"
|
|
assert body["error"]["guidance"]["manna"] =~ "Shemot 16:18"
|
|
|
|
# Another address is not held back by this one.
|
|
refute ask({203, 0, 113, 10}).status == 429
|
|
end
|
|
|
|
test "the courtyard: per key, and only past the gate" do
|
|
# A wrong key knocks all it likes and is never counted — 401, not 429.
|
|
for _ <- 1..5, do: assert(bedikah("Bearer wrong").status == 401)
|
|
|
|
assert bedikah("Bearer test-token").status != 429
|
|
assert bedikah("Bearer test-token").status != 429
|
|
conn = bedikah("Bearer test-token")
|
|
assert conn.status == 429
|
|
assert Jason.decode!(conn.resp_body)["error"]["guidance"]["issue"] =~ "courtyard allows 2 a minute"
|
|
|
|
# The chatzer key is its own hand, even at the same door.
|
|
refute bedikah("Bearer chatzer-test-token").status == 429
|
|
end
|
|
end
|
|
|
|
defp ask(ip) do
|
|
%{conn(:post, "/peah/ask", Jason.encode!(%{question: "x"})) | remote_ip: ip}
|
|
|> put_req_header("content-type", "application/json")
|
|
|> Router.call(@opts)
|
|
end
|
|
|
|
defp bedikah(auth) do
|
|
conn(:post, "/courtyard/bedikah", Jason.encode!(%{output: "", model: "gpt"}))
|
|
|> put_req_header("content-type", "application/json")
|
|
|> put_req_header("authorization", auth)
|
|
|> Router.call(@opts)
|
|
end
|
|
end
|