MannaLedger: real anti-hoarding enforcement — d'var yom b'yomo (Step 26)
Shemot 16:4 — d'var yom b'yomo — the day's matter in its day. Shemot 16:19 — al yoter mimenu ad boker — do not leave surplus. Shemot 16:20 — vayotiru... vayarum tola'im vayiv'ash — hoarded manna rots. - MannaLedger GenServer tracks consumption against daily provision - load_provision/0 reads manna file, resets consumption to zero - consume/2 deducts from allocated resource, rejects when exhausted - remaining/1 and available?/2 for balance queries - enforce_cycle_boundary/0 zeroes out all allocation, returns released amounts - Chatzotzrot assembly_leaders signal when provision runs low (< 10%) - Manna.enforce_anti_hoarding/0 now delegates to MannaLedger - Added to supervision tree in application.ex - 17 new tests with proper isolation (reset between tests) - All 177 Elixir tests pass, 0 failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
671d45625c
commit
6a879fcf8b
4 files changed
+566
-4
No files matched your search
@@ -8,6 +8,7 @@ defmodule VavPipeline.Application do
|
|||||||
- AreiMiklat (cities of refuge — protection protocol)
|
- AreiMiklat (cities of refuge — protection protocol)
|
||||||
- EglahArufah (accountability for unknown harm)
|
- EglahArufah (accountability for unknown harm)
|
||||||
- Bikurim (first fruits — release protocol)
|
- Bikurim (first fruits — release protocol)
|
||||||
|
- MannaLedger (consumption tracking and anti-hoarding enforcement)
|
||||||
- ShabbatTimer (mandatory cessation monitor)
|
- ShabbatTimer (mandatory cessation monitor)
|
||||||
- ShoreshEnginePort (Erlang Port to Haskell engine, if binary found)
|
- ShoreshEnginePort (Erlang Port to Haskell engine, if binary found)
|
||||||
- TalmudicEngineSocket (Unix socket to Prolog engine)
|
- TalmudicEngineSocket (Unix socket to Prolog engine)
|
||||||
@@ -33,6 +34,7 @@ defmodule VavPipeline.Application do
|
|||||||
{VavPipeline.AreiMiklat, []},
|
{VavPipeline.AreiMiklat, []},
|
||||||
{VavPipeline.EglahArufah, []},
|
{VavPipeline.EglahArufah, []},
|
||||||
{VavPipeline.Bikurim, []},
|
{VavPipeline.Bikurim, []},
|
||||||
|
{VavPipeline.MannaLedger, []},
|
||||||
{VavPipeline.ShabbatTimer, []}
|
{VavPipeline.ShabbatTimer, []}
|
||||||
]
|
]
|
||||||
|> maybe_add_repo()
|
|> maybe_add_repo()
|
||||||
|
|||||||
@@ -98,14 +98,25 @@ defmodule VavPipeline.Manna do
|
|||||||
Enforce anti-hoarding — manna from today cannot be used tomorrow.
|
Enforce anti-hoarding — manna from today cannot be used tomorrow.
|
||||||
Called at erev_voker (cycle boundary).
|
Called at erev_voker (cycle boundary).
|
||||||
|
|
||||||
In production this would enforce resource deallocation.
|
Shemot 16:19 — al yoter mimenu ad boker.
|
||||||
In Phase 2, it logs the boundary.
|
Delegates to MannaLedger.enforce_cycle_boundary/0 for real enforcement.
|
||||||
"""
|
"""
|
||||||
@spec enforce_anti_hoarding() :: :ok
|
@spec enforce_anti_hoarding() :: :ok
|
||||||
def enforce_anti_hoarding do
|
def enforce_anti_hoarding do
|
||||||
require Logger
|
require Logger
|
||||||
Logger.info("Manna anti-hoarding enforced — surplus released at cycle boundary")
|
|
||||||
:ok
|
case VavPipeline.MannaLedger.enforce_cycle_boundary() do
|
||||||
|
{:ok, %{released: released}} ->
|
||||||
|
total = released |> Map.values() |> Enum.sum()
|
||||||
|
|
||||||
|
if total > 0 do
|
||||||
|
Logger.info("Manna anti-hoarding enforced — surplus released: #{inspect(released)}")
|
||||||
|
else
|
||||||
|
Logger.info("Manna anti-hoarding enforced — no surplus to release")
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
defmodule VavPipeline.MannaLedger do
|
||||||
|
@moduledoc """
|
||||||
|
Manna consumption ledger — d'var yom b'yomo.
|
||||||
|
|
||||||
|
Shemot 16:4 — "The people shall go out and gather the matter
|
||||||
|
of a day in its day." Each cycle receives exactly what it needs.
|
||||||
|
|
||||||
|
Shemot 16:19 — "Let no one leave any of it until morning."
|
||||||
|
Root ותר (yoter) — do not hoard. At cycle boundary, surplus
|
||||||
|
is released. Not rolled over. Not saved.
|
||||||
|
|
||||||
|
Shemot 16:20 — "Some left over... and it bred worms and stank."
|
||||||
|
Root באש (ba'ash) — hoarded resources breed corruption.
|
||||||
|
|
||||||
|
This GenServer tracks:
|
||||||
|
1. Today's allocated provision (from the manna file)
|
||||||
|
2. Consumption against each resource category
|
||||||
|
3. Remaining balance
|
||||||
|
4. Cycle boundary enforcement (zeroing out surplus)
|
||||||
|
|
||||||
|
Architecture §4, Step 26: Manna anti-hoarding enforcement
|
||||||
|
and cycle-boundary surplus release.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias VavPipeline.{Manna, Chatzotzrot}
|
||||||
|
|
||||||
|
@type resource_key :: :compute_units | :memory_mb | :api_calls_external |
|
||||||
|
:storage_delta_mb | :sanhedrin_panels_max
|
||||||
|
|
||||||
|
@type ledger_state :: %{
|
||||||
|
cycle_date: String.t() | nil,
|
||||||
|
allocated: map(),
|
||||||
|
consumed: map(),
|
||||||
|
cycle_count: non_neg_integer(),
|
||||||
|
last_enforcement: DateTime.t() | nil,
|
||||||
|
hoarding_violations: non_neg_integer()
|
||||||
|
}
|
||||||
|
|
||||||
|
@zero_provision %{
|
||||||
|
compute_units: 0,
|
||||||
|
memory_mb: 0,
|
||||||
|
api_calls_external: 0,
|
||||||
|
storage_delta_mb: 0,
|
||||||
|
sanhedrin_panels_max: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Client API
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def start_link(opts \\ []) do
|
||||||
|
name = Keyword.get(opts, :name, __MODULE__)
|
||||||
|
GenServer.start_link(__MODULE__, opts, name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Load today's provision from the manna file into the ledger.
|
||||||
|
|
||||||
|
Called at the start of each cycle. Resets consumption to zero.
|
||||||
|
If provision was already loaded for today, returns {:ok, :already_loaded}.
|
||||||
|
"""
|
||||||
|
@spec load_provision() :: {:ok, :loaded | :already_loaded | :no_provision} | {:error, String.t()}
|
||||||
|
def load_provision do
|
||||||
|
GenServer.call(__MODULE__, :load_provision)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Consume resources from today's manna provision.
|
||||||
|
|
||||||
|
D'var yom b'yomo — take only what the day requires.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- resource: which resource to consume (:compute_units, :api_calls_external, etc.)
|
||||||
|
- amount: how much to consume
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- {:ok, remaining} — consumption recorded, remaining balance returned
|
||||||
|
- {:error, :provision_exhausted} — no more of this resource available
|
||||||
|
- {:error, :no_provision} — no provision loaded for today
|
||||||
|
"""
|
||||||
|
@spec consume(resource_key(), pos_integer()) ::
|
||||||
|
{:ok, non_neg_integer()} | {:error, atom()}
|
||||||
|
def consume(resource, amount) when is_atom(resource) and is_integer(amount) and amount > 0 do
|
||||||
|
GenServer.call(__MODULE__, {:consume, resource, amount})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Check remaining balance for a resource.
|
||||||
|
"""
|
||||||
|
@spec remaining(resource_key()) :: non_neg_integer()
|
||||||
|
def remaining(resource) when is_atom(resource) do
|
||||||
|
GenServer.call(__MODULE__, {:remaining, resource})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Check if a resource has sufficient provision for the requested amount.
|
||||||
|
"""
|
||||||
|
@spec available?(resource_key(), pos_integer()) :: boolean()
|
||||||
|
def available?(resource, amount) when is_atom(resource) and is_integer(amount) do
|
||||||
|
GenServer.call(__MODULE__, {:available?, resource, amount})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the full ledger state — allocated, consumed, remaining.
|
||||||
|
"""
|
||||||
|
@spec ledger() :: ledger_state()
|
||||||
|
def ledger do
|
||||||
|
GenServer.call(__MODULE__, :ledger)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Enforce anti-hoarding — release all surplus at cycle boundary.
|
||||||
|
|
||||||
|
Shemot 16:19 — al yoter mimenu ad boker.
|
||||||
|
"Let no one leave any of it until morning."
|
||||||
|
|
||||||
|
This zeroes out remaining allocation and records the enforcement.
|
||||||
|
Called by BereishitCycle.erev_voker/2 at cycle boundary.
|
||||||
|
|
||||||
|
Returns {:ok, %{released: map()}} with the amounts released (what was left over).
|
||||||
|
"""
|
||||||
|
@spec enforce_cycle_boundary() :: {:ok, %{released: map()}}
|
||||||
|
def enforce_cycle_boundary do
|
||||||
|
GenServer.call(__MODULE__, :enforce_cycle_boundary)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reset the ledger to initial state. For testing only.
|
||||||
|
"""
|
||||||
|
@spec reset() :: :ok
|
||||||
|
def reset do
|
||||||
|
GenServer.call(__MODULE__, :reset)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# GenServer callbacks
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_opts) do
|
||||||
|
state = %{
|
||||||
|
cycle_date: nil,
|
||||||
|
allocated: @zero_provision,
|
||||||
|
consumed: @zero_provision,
|
||||||
|
cycle_count: 0,
|
||||||
|
last_enforcement: nil,
|
||||||
|
hoarding_violations: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info("MannaLedger: ledger ready — d'var yom b'yomo")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:reset, _from, _state) do
|
||||||
|
{:reply, :ok,
|
||||||
|
%{
|
||||||
|
cycle_date: nil,
|
||||||
|
allocated: @zero_provision,
|
||||||
|
consumed: @zero_provision,
|
||||||
|
cycle_count: 0,
|
||||||
|
last_enforcement: nil,
|
||||||
|
hoarding_violations: 0
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:load_provision, _from, state) do
|
||||||
|
today = Date.utc_today() |> Date.to_iso8601()
|
||||||
|
|
||||||
|
if state.cycle_date == today do
|
||||||
|
{:reply, {:ok, :already_loaded}, state}
|
||||||
|
else
|
||||||
|
case Manna.check() do
|
||||||
|
{:ok, :no_provision} ->
|
||||||
|
new_state = %{
|
||||||
|
state
|
||||||
|
| cycle_date: today,
|
||||||
|
allocated: @zero_provision,
|
||||||
|
consumed: @zero_provision,
|
||||||
|
cycle_count: state.cycle_count + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.warning("MannaLedger: no provision for #{today} — system will wait")
|
||||||
|
{:reply, {:ok, :no_provision}, new_state}
|
||||||
|
|
||||||
|
{:ok, manna} ->
|
||||||
|
new_state = %{
|
||||||
|
state
|
||||||
|
| cycle_date: today,
|
||||||
|
allocated: manna.provision,
|
||||||
|
consumed: @zero_provision,
|
||||||
|
cycle_count: state.cycle_count + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
"MannaLedger: provision loaded for #{today} (#{manna.type}) — " <>
|
||||||
|
"compute: #{manna.provision.compute_units}, " <>
|
||||||
|
"api_calls: #{manna.provision.api_calls_external}, " <>
|
||||||
|
"panels: #{manna.provision.sanhedrin_panels_max}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:reply, {:ok, :loaded}, new_state}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:reply, {:error, reason}, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:consume, resource, amount}, _from, state) do
|
||||||
|
allocated = Map.get(state.allocated, resource, 0)
|
||||||
|
consumed = Map.get(state.consumed, resource, 0)
|
||||||
|
remaining = allocated - consumed
|
||||||
|
|
||||||
|
cond do
|
||||||
|
state.cycle_date == nil ->
|
||||||
|
{:reply, {:error, :no_provision}, state}
|
||||||
|
|
||||||
|
remaining <= 0 ->
|
||||||
|
Logger.warning(
|
||||||
|
"MannaLedger: #{resource} exhausted — " <>
|
||||||
|
"allocated #{allocated}, consumed #{consumed}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:reply, {:error, :provision_exhausted}, state}
|
||||||
|
|
||||||
|
amount > remaining ->
|
||||||
|
# Partial consumption not allowed — don't take more than what remains
|
||||||
|
Logger.warning(
|
||||||
|
"MannaLedger: #{resource} insufficient — " <>
|
||||||
|
"requested #{amount}, remaining #{remaining}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:reply, {:error, :provision_exhausted}, state}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
new_consumed = Map.put(state.consumed, resource, consumed + amount)
|
||||||
|
new_remaining = remaining - amount
|
||||||
|
new_state = %{state | consumed: new_consumed}
|
||||||
|
|
||||||
|
# Signal when provision runs low (< 10% remaining)
|
||||||
|
if new_remaining < allocated * 0.1 and new_remaining > 0 do
|
||||||
|
Chatzotzrot.signal(:assembly_leaders,
|
||||||
|
reason: "Manna low: #{resource} at #{new_remaining}/#{allocated}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:reply, {:ok, new_remaining}, new_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:remaining, resource}, _from, state) do
|
||||||
|
allocated = Map.get(state.allocated, resource, 0)
|
||||||
|
consumed = Map.get(state.consumed, resource, 0)
|
||||||
|
{:reply, max(allocated - consumed, 0), state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call({:available?, resource, amount}, _from, state) do
|
||||||
|
allocated = Map.get(state.allocated, resource, 0)
|
||||||
|
consumed = Map.get(state.consumed, resource, 0)
|
||||||
|
{:reply, (allocated - consumed) >= amount, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:ledger, _from, state) do
|
||||||
|
{:reply, state, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:enforce_cycle_boundary, _from, state) do
|
||||||
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
|
# Calculate what was left over — the surplus that would breed worms
|
||||||
|
released =
|
||||||
|
Map.new(@zero_provision, fn {key, _} ->
|
||||||
|
allocated = Map.get(state.allocated, key, 0)
|
||||||
|
consumed = Map.get(state.consumed, key, 0)
|
||||||
|
{key, max(allocated - consumed, 0)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
total_released = released |> Map.values() |> Enum.sum()
|
||||||
|
|
||||||
|
if total_released > 0 do
|
||||||
|
Logger.info(
|
||||||
|
"MannaLedger: cycle boundary — releasing surplus: #{inspect(released)}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
new_state = %{
|
||||||
|
state
|
||||||
|
| allocated: @zero_provision,
|
||||||
|
consumed: @zero_provision,
|
||||||
|
cycle_date: nil,
|
||||||
|
last_enforcement: now,
|
||||||
|
hoarding_violations: state.hoarding_violations
|
||||||
|
}
|
||||||
|
|
||||||
|
{:reply, {:ok, %{released: released}}, new_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
defmodule VavPipeline.MannaLedgerTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for the MannaLedger — manna consumption and anti-hoarding enforcement.
|
||||||
|
|
||||||
|
Shemot 16:4 — D'var yom b'yomo — the day's matter in its day.
|
||||||
|
Shemot 16:19 — Al yoter mimenu ad boker — do not leave surplus.
|
||||||
|
Shemot 16:20 — Vayotiru... vayarum tola'im vayiv'ash — hoarded manna rots.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias VavPipeline.MannaLedger
|
||||||
|
|
||||||
|
setup do
|
||||||
|
MannaLedger.reset()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to load provision from a test fixture file
|
||||||
|
defp write_test_provision(opts \\ []) do
|
||||||
|
today = Date.utc_today() |> Date.to_iso8601()
|
||||||
|
compute = Keyword.get(opts, :compute_units, 100)
|
||||||
|
api_calls = Keyword.get(opts, :api_calls_external, 50)
|
||||||
|
panels = Keyword.get(opts, :sanhedrin_panels_max, 3)
|
||||||
|
type = Keyword.get(opts, :type, "regular")
|
||||||
|
|
||||||
|
expires =
|
||||||
|
DateTime.utc_now()
|
||||||
|
|> DateTime.add(6 * 3600)
|
||||||
|
|> DateTime.to_iso8601()
|
||||||
|
|
||||||
|
provision = %{
|
||||||
|
"date" => today,
|
||||||
|
"provision" => %{
|
||||||
|
"compute_units" => compute,
|
||||||
|
"memory_mb" => 1024,
|
||||||
|
"api_calls_external" => api_calls,
|
||||||
|
"storage_delta_mb" => 10,
|
||||||
|
"sanhedrin_panels_max" => panels
|
||||||
|
},
|
||||||
|
"type" => type,
|
||||||
|
"expires_at" => expires,
|
||||||
|
"written_by" => "test",
|
||||||
|
"written_at" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||||
|
}
|
||||||
|
|
||||||
|
path = VavPipeline.Manna.provision_file()
|
||||||
|
File.mkdir_p!(Path.dirname(path))
|
||||||
|
File.write!(path, Jason.encode!(provision))
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm(path) end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Loading provision — d'var yom b'yomo
|
||||||
|
# ============================================================
|
||||||
|
describe "load_provision" do
|
||||||
|
test "loads provision from manna file" do
|
||||||
|
write_test_provision(compute_units: 200, api_calls_external: 100)
|
||||||
|
assert {:ok, :loaded} = MannaLedger.load_provision()
|
||||||
|
|
||||||
|
ledger = MannaLedger.ledger()
|
||||||
|
assert ledger.allocated.compute_units == 200
|
||||||
|
assert ledger.allocated.api_calls_external == 100
|
||||||
|
assert ledger.consumed.compute_units == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns :already_loaded if same day" do
|
||||||
|
write_test_provision()
|
||||||
|
assert {:ok, :loaded} = MannaLedger.load_provision()
|
||||||
|
assert {:ok, :already_loaded} = MannaLedger.load_provision()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns :no_provision when no manna file" do
|
||||||
|
# Ensure provision file doesn't exist
|
||||||
|
path = VavPipeline.Manna.provision_file()
|
||||||
|
File.rm(path)
|
||||||
|
|
||||||
|
assert {:ok, :no_provision} = MannaLedger.load_provision()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Consumption — v'laktu d'var yom b'yomo
|
||||||
|
# ============================================================
|
||||||
|
describe "consume" do
|
||||||
|
setup do
|
||||||
|
write_test_provision(compute_units: 10, api_calls_external: 5, sanhedrin_panels_max: 2)
|
||||||
|
MannaLedger.load_provision()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "consumes resource and returns remaining" do
|
||||||
|
assert {:ok, 7} = MannaLedger.consume(:compute_units, 3)
|
||||||
|
assert {:ok, 4} = MannaLedger.consume(:compute_units, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects when resource exhausted" do
|
||||||
|
MannaLedger.consume(:compute_units, 10)
|
||||||
|
assert {:error, :provision_exhausted} = MannaLedger.consume(:compute_units, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects when amount exceeds remaining" do
|
||||||
|
MannaLedger.consume(:compute_units, 8)
|
||||||
|
assert {:error, :provision_exhausted} = MannaLedger.consume(:compute_units, 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "different resources tracked independently" do
|
||||||
|
MannaLedger.consume(:compute_units, 5)
|
||||||
|
MannaLedger.consume(:api_calls_external, 3)
|
||||||
|
|
||||||
|
assert MannaLedger.remaining(:compute_units) == 5
|
||||||
|
assert MannaLedger.remaining(:api_calls_external) == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error when no provision loaded" do
|
||||||
|
# Force a fresh cycle by enforcing boundary first
|
||||||
|
MannaLedger.enforce_cycle_boundary()
|
||||||
|
|
||||||
|
# Now no provision is loaded
|
||||||
|
assert {:error, :no_provision} = MannaLedger.consume(:compute_units, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Remaining and available?
|
||||||
|
# ============================================================
|
||||||
|
describe "remaining and available?" do
|
||||||
|
setup do
|
||||||
|
write_test_provision(compute_units: 20)
|
||||||
|
MannaLedger.load_provision()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remaining returns correct balance" do
|
||||||
|
assert MannaLedger.remaining(:compute_units) == 20
|
||||||
|
MannaLedger.consume(:compute_units, 7)
|
||||||
|
assert MannaLedger.remaining(:compute_units) == 13
|
||||||
|
end
|
||||||
|
|
||||||
|
test "available? checks sufficiency" do
|
||||||
|
assert MannaLedger.available?(:compute_units, 20)
|
||||||
|
assert MannaLedger.available?(:compute_units, 1)
|
||||||
|
refute MannaLedger.available?(:compute_units, 21)
|
||||||
|
|
||||||
|
MannaLedger.consume(:compute_units, 15)
|
||||||
|
assert MannaLedger.available?(:compute_units, 5)
|
||||||
|
refute MannaLedger.available?(:compute_units, 6)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Cycle boundary enforcement — al yoter mimenu ad boker
|
||||||
|
# ============================================================
|
||||||
|
describe "enforce_cycle_boundary" do
|
||||||
|
setup do
|
||||||
|
write_test_provision(compute_units: 100, api_calls_external: 50)
|
||||||
|
MannaLedger.load_provision()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "releases surplus and zeroes allocation" do
|
||||||
|
MannaLedger.consume(:compute_units, 30)
|
||||||
|
MannaLedger.consume(:api_calls_external, 10)
|
||||||
|
|
||||||
|
{:ok, %{released: released}} = MannaLedger.enforce_cycle_boundary()
|
||||||
|
|
||||||
|
# 100 allocated - 30 consumed = 70 released
|
||||||
|
assert released.compute_units == 70
|
||||||
|
# 50 allocated - 10 consumed = 40 released
|
||||||
|
assert released.api_calls_external == 40
|
||||||
|
end
|
||||||
|
|
||||||
|
test "after enforcement, all resources are zero" do
|
||||||
|
MannaLedger.consume(:compute_units, 50)
|
||||||
|
MannaLedger.enforce_cycle_boundary()
|
||||||
|
|
||||||
|
assert MannaLedger.remaining(:compute_units) == 0
|
||||||
|
assert MannaLedger.remaining(:api_calls_external) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "after enforcement, consume returns :no_provision" do
|
||||||
|
MannaLedger.enforce_cycle_boundary()
|
||||||
|
assert {:error, :no_provision} = MannaLedger.consume(:compute_units, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "full consumption leaves nothing to release" do
|
||||||
|
MannaLedger.consume(:compute_units, 100)
|
||||||
|
MannaLedger.consume(:api_calls_external, 50)
|
||||||
|
|
||||||
|
{:ok, %{released: released}} = MannaLedger.enforce_cycle_boundary()
|
||||||
|
assert released.compute_units == 0
|
||||||
|
assert released.api_calls_external == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enforcement records timestamp" do
|
||||||
|
MannaLedger.enforce_cycle_boundary()
|
||||||
|
ledger = MannaLedger.ledger()
|
||||||
|
assert ledger.last_enforcement != nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Manna.enforce_anti_hoarding integration
|
||||||
|
# ============================================================
|
||||||
|
describe "Manna.enforce_anti_hoarding integration" do
|
||||||
|
setup do
|
||||||
|
write_test_provision(compute_units: 50)
|
||||||
|
MannaLedger.load_provision()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Manna.enforce_anti_hoarding delegates to ledger" do
|
||||||
|
MannaLedger.consume(:compute_units, 20)
|
||||||
|
assert :ok = VavPipeline.Manna.enforce_anti_hoarding()
|
||||||
|
|
||||||
|
# After enforcement, resources are zeroed
|
||||||
|
assert MannaLedger.remaining(:compute_units) == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Ledger state
|
||||||
|
# ============================================================
|
||||||
|
describe "ledger state" do
|
||||||
|
test "tracks cycle count" do
|
||||||
|
write_test_provision()
|
||||||
|
initial = MannaLedger.ledger().cycle_count
|
||||||
|
|
||||||
|
MannaLedger.load_provision()
|
||||||
|
assert MannaLedger.ledger().cycle_count == initial + 1
|
||||||
|
|
||||||
|
# Force new cycle
|
||||||
|
MannaLedger.enforce_cycle_boundary()
|
||||||
|
|
||||||
|
# Can't easily test second load on same day since it returns :already_loaded
|
||||||
|
# But cycle_count incremented on load
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user