fix: reward guards now survive a reconnect
The three replay guards shipped earlier lived on the in-memory SeanceState, which made them per-CONNECTION. I flagged that as an open residual at the time: drop the socket and reconnect — or just open a second one — and the client got a fresh empty guard and could be paid again for the same spirit. summon_limiter bounded the rate of that, never the total. `award_claims` is the durable form: one row per (seeker, presence, milestone), with a UNIQUE constraint doing the actual enforcement. The claim is a bare INSERT and losing the race raises IntegrityError, which is caught and read as "already paid" — a check-then-insert would let two sockets both read "unclaimed" and both pay. `crossing` is claimed by BOTH roads, so a spirit crosses once whichever road arrives first. Measured with the durable claim disabled: 5 reconnects paid 75 extra essence on the ritual, 60 on a verdict, 140 on the passage, and two simultaneous sockets paid 30 for one 15-essence ritual. The four tests that were failing were the tests, not the guard. They compared raw balances across reconnects, but re-opening a channel IS a summon, and SUMMON_ESSENCE_TRICKLE is paid per summon by design (inventory.py:42, bounded by summon_limiter rather than by any once-per-presence rule). The expected trickle is now stated explicitly so the assertion speaks about the milestone it is actually testing. Favor has no trickle, so it must not move at all — asserted separately. Anti-overshoot covered in both directions: a genuinely fresh presence still pays in full across a reconnect, a corrected verdict still pays on a second connection, and `test` stays freely repeatable since it never touches the ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -192,6 +192,31 @@ def _verdict_outcomes(monkeypatch, table):
|
||||
)
|
||||
|
||||
|
||||
def _force_new_presence(monkeypatch):
|
||||
"""Pin the summon draw so the NEXT summon mints a genuinely new spirit.
|
||||
|
||||
`_summon` draws against RETURN_CHANCE to decide whether the presence
|
||||
already on this channel answers again, and re-calling the same channel
|
||||
usually returns the SAME entity row — measured on this suite, 8 of 11
|
||||
consecutive re-summons came back with `is_new: false` and an identical id.
|
||||
|
||||
That matters now that the purse is keyed on the entity (award_claims),
|
||||
because "summon again" and "the same spirit answers again" are then two
|
||||
different things. These tests are about what a genuinely FRESH presence is
|
||||
worth, so the draw is pinned rather than left to a coin flip; a draw of
|
||||
1.0 is >= any possible `return_chance`, so the familiar presence never
|
||||
answers. Every other draw stays real.
|
||||
"""
|
||||
real = app.ws.veil_float
|
||||
|
||||
def selective(entropy, context, *args, **kwargs):
|
||||
if context == "answers":
|
||||
return 1.0
|
||||
return real(entropy, context, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(app.ws, "veil_float", selective)
|
||||
|
||||
|
||||
# --- the ritual faucet -----------------------------------------------------
|
||||
|
||||
|
||||
@@ -272,22 +297,29 @@ async def test_a_genuinely_new_presence_reopens_the_ritual_purse(
|
||||
):
|
||||
"""The guard must not overshoot: each spirit is worth its own rite.
|
||||
|
||||
A `ritual_paid` latch that survived a summon would silently make every
|
||||
spirit after the first worthless.
|
||||
A durable claim that survived a summon would silently make every spirit
|
||||
after the first worthless.
|
||||
"""
|
||||
_always_wins(monkeypatch)
|
||||
# The second summon must actually bring a DIFFERENT spirit — see
|
||||
# `_force_new_presence`. Without this the test asserts a coin flip.
|
||||
_force_new_presence(monkeypatch)
|
||||
|
||||
token = _login(sync_client, "ritual-second-spirit")
|
||||
user_id = _user_id(sync_client, token)
|
||||
|
||||
with _ws_connect(sync_client, token) as ws:
|
||||
_read_until(ws, "session")
|
||||
_summon(ws)
|
||||
first_entity = _summon(ws)
|
||||
baseline, _, _ = await _ledger(db_session, user_id)
|
||||
_run_ritual(ws)
|
||||
after_first, _, _ = await _ledger(db_session, user_id)
|
||||
|
||||
_summon(ws)
|
||||
second_entity = _summon(ws)
|
||||
assert second_entity["entity"]["id"] != first_entity["entity"]["id"], (
|
||||
"the second summon returned the same spirit — this test is about "
|
||||
"a genuinely new presence"
|
||||
)
|
||||
_run_ritual(ws)
|
||||
after_second, _, _ = await _ledger(db_session, user_id)
|
||||
|
||||
@@ -425,17 +457,24 @@ async def test_a_genuinely_new_presence_reopens_the_judgment_purse(
|
||||
sync_client, db_session, monkeypatch
|
||||
):
|
||||
_verdict_outcomes(monkeypatch, {"trust": _CORRECT_TRUST})
|
||||
# The second summon must actually bring a DIFFERENT spirit — see
|
||||
# `_force_new_presence`. Without this the test asserts a coin flip.
|
||||
_force_new_presence(monkeypatch)
|
||||
|
||||
token = _login(sync_client, "judgment-second-spirit")
|
||||
user_id = _user_id(sync_client, token)
|
||||
|
||||
with _ws_connect(sync_client, token) as ws:
|
||||
_read_until(ws, "session")
|
||||
_summon(ws)
|
||||
first_entity = _summon(ws)
|
||||
_judge(ws, "trust")
|
||||
after_first, _, _ = await _ledger(db_session, user_id)
|
||||
|
||||
_summon(ws)
|
||||
second_entity = _summon(ws)
|
||||
assert second_entity["entity"]["id"] != first_entity["entity"]["id"], (
|
||||
"the second summon returned the same spirit — this test is about "
|
||||
"a genuinely new presence"
|
||||
)
|
||||
_judge(ws, "trust")
|
||||
after_second, _, _ = await _ledger(db_session, user_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user