ouroboros/tests/test_server_runner_absorb_wait.py
Ouroboros fa2977423b fix(e2e_live): reserve the evolution root only for the scenario that absorbs; typed idle reasons relative to the wait
Second adversarial round on the absorb-wait rewrite.

Reservation: after the previous commit only SM1 can promote (SW1/SK1 pin
OUROBOROS_POST_TASK_EVOLUTION=false), yet RunBudget.reservation still
added the evolution root for every attempt under --self-mod, so SW1/SK1
reserved a ceiling they could never spend and run-cap admission refused
or serialized paid attempts on it (the SK1-only mini run at cap 130 was
refused by exactly this over-reservation: 50 x (2 + 1) = 150). The rule is
now per_task x (root_tasks + int(self_mod and absorbs)); admit,
budget_preflight, dispatch_order and run_lane pass the scenario's
expects_absorb. Owner configuration (cap 300, per-task 50, 3 attempts,
self-mod): SM1 100, SK1 100, SW1 50 — realistic spends admit 9/9 for
$159, pessimistic 8/9 for $219 (SK1_a3 refused), every scenario keeping
two; the CI e2e-live arithmetic comment and summary header are rewritten
(full set $225, not $315) inside the D-12 job, and the CI-lane pins
re-derive the numbers with the scenario flag.

Idle reasons: absorb_idle_reason types relative to the wait's start
(history length snapshot) so a resumed campaign's older cycles never
speak for this boundary; a paused/stopped/completed status wins;
no_promotion means an every_n post-task tick was recorded (llm cadences
write none: no_decision). Tests cover the resumed-campaign boundary and
the reason table with the cycle written during the wait.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-06 01:31:11 +00:00

128 lines
6.8 KiB
Python

"""``IsolatedServer.wait_for_absorb`` may answer ``absorbed=False`` early only with PROOF that no cycle is
pending (rc.15 stand, adversarial finding of 2026-09-06): a cycle that committed keeps its campaign transaction
as ``waiting_for_restart`` through the synchronous supervisor restart (queue idle, counter unchanged) and the
re-exec'd server answers ``/api/state`` with zero counts before its supervisor is ready; one idle sample used to
end the wait as ``no_promotion`` right there. The typed idle reasons come from the durable campaign state."""
from __future__ import annotations
import json
import pathlib
import pytest
from devtools.benchmarks.common import server_runner
class _Clock:
def __init__(self) -> None:
self.now = 0.0
def time(self) -> float:
return self.now
def sleep(self, seconds: float) -> None:
self.now += float(seconds)
def _server(tmp_path: pathlib.Path, states: list, monkeypatch, *, sha: str = "aaaaaaaa") -> server_runner.IsolatedServer:
"""``states`` feeds the idle poll only (one read per poll after the grace); the served sha is fixed."""
clock = _Clock()
monkeypatch.setattr(server_runner, "time", clock)
srv = object.__new__(server_runner.IsolatedServer)
srv.data_root = tmp_path
srv.base_url = "http://127.0.0.1:1"
calls = {"n": 0}
def _state(timeout: float = 5) -> dict:
st = states[min(calls["n"], len(states) - 1)]
calls["n"] += 1
if isinstance(st, Exception):
raise st
return st
srv._state = _state
srv.current_sha = lambda: sha
srv.wait_for_health = lambda timeout=180: True
return srv
def _campaign(root: pathlib.Path, **fields) -> None:
(root / "state").mkdir(parents=True, exist_ok=True)
(root / "state" / "evolution_campaign.json").write_text(json.dumps({"schema_version": 1, **fields}), encoding="utf-8")
IDLE = {"sha": "aaaaaaaa", "pending_count": 0, "running_count": 0, "supervisor_ready": True}
BOOTING = {"sha": "", "pending_count": 0, "running_count": 0, "supervisor_ready": False}
BUSY = {"sha": "aaaaaaaa", "pending_count": 0, "running_count": 1, "supervisor_ready": True}
def test_an_idle_queue_with_a_transaction_waiting_for_restart_is_not_a_declined_promotion(tmp_path, monkeypatch):
_campaign(tmp_path, status="active", source="post_task", absorbed_cycles_done=0,
active_transaction={"commit_sha": "c" * 40, "cycle_outcome": "waiting_for_restart"})
srv = _server(tmp_path, [IDLE], monkeypatch)
out = srv.wait_for_absorb("aaaaaaaa", 0, timeout=200, idle_grace=10, idle_polls=2)
assert out["reason"] == "timeout" and out["absorbed"] is False and out["campaign"]["active_transaction"] is True
def test_the_booting_reexecd_server_and_a_single_idle_sample_do_not_end_the_wait(tmp_path, monkeypatch):
_campaign(tmp_path, status="active", source="post_task", absorbed_cycles_done=0, transaction_history=[])
# after the grace: booting, idle, busy, idle, idle -> the streak of two idle polls only forms on the fifth poll
srv = _server(tmp_path, [BOOTING, IDLE, BUSY, IDLE, IDLE], monkeypatch)
out = srv.wait_for_absorb("aaaaaaaa", 0, timeout=10_000, idle_grace=10, idle_polls=2)
assert out["reason"] == "cycle_not_enqueued" and out["absorbed"] is False
assert server_runner.time.now == 35 # polls at t=15,20,25,30,35: the streak completes on the fifth
def test_a_pending_request_file_blocks_the_early_exit(tmp_path, monkeypatch):
(tmp_path / "state").mkdir()
(tmp_path / "state" / "post_task_evolution_request.json").write_text("{}", encoding="utf-8")
srv = _server(tmp_path, [IDLE], monkeypatch)
assert srv.wait_for_absorb("aaaaaaaa", 0, timeout=100, idle_grace=10, idle_polls=1)["reason"] == "timeout"
def test_the_absorb_is_confirmed_when_the_counter_and_the_served_sha_move(tmp_path, monkeypatch):
_campaign(tmp_path, status="active", source="post_task", absorbed_cycles_done=1,
transaction_history=[{"cycle_outcome": "absorbed"}])
srv = _server(tmp_path, [IDLE], monkeypatch, sha="bbbbbbbb")
out = srv.wait_for_absorb("aaaaaaaa", 0, timeout=100, idle_grace=10)
assert out["absorbed"] is True and out["new_sha"] == "bbbbbbbb" and out["campaign"]["newest_outcome"] == "absorbed"
@pytest.mark.parametrize("campaign,counter,expected", [
(None, 1, "no_promotion"), # an every_n tick ran, nothing promoted
(None, 0, "no_decision"), # no tick recorded (llm cadence / ineligible)
({"status": "active", "transaction_history": [{"cycle_outcome": "no_op"}]}, 1, "cycle_no_op"),
({"status": "active", "transaction_history": [{"cycle_outcome": "abandoned"}]}, 1, "cycle_not_absorbed"),
({"status": "paused", "transaction_history": [{"cycle_outcome": "no_op"}]}, 1, "campaign_paused"),
({"status": "active"}, 1, "cycle_not_enqueued"),
])
def test_idle_reasons_are_typed_from_the_durable_campaign_state(tmp_path, monkeypatch, campaign, counter, expected):
"""The cycle written DURING the wait decides ``cycle_*``: the campaign file is created empty before the wait
and the cycle's transaction lands after it started (a fresh one-shot campaign); a paused status wins."""
(tmp_path / "state").mkdir(exist_ok=True)
if counter:
(tmp_path / "state" / "post_task_evolution_counter.json").write_text(json.dumps({"n": counter}), encoding="utf-8")
srv = _server(tmp_path, [IDLE], monkeypatch)
if campaign is not None:
# the campaign appears after the wait's first summary: the first poll writes it, so its history is "new"
original = srv._state
def _state_then_campaign(timeout: float = 5) -> dict:
_campaign(tmp_path, **campaign)
return original(timeout)
srv._state = _state_then_campaign
out = srv.wait_for_absorb("aaaaaaaa", 0, timeout=1_000, idle_grace=10, idle_polls=3)
assert out["reason"] == expected and out["absorbed"] is False
assert out["campaign"]["post_task_counter"] == counter and out["campaign"]["present"] is (campaign is not None)
def test_older_cycles_of_a_resumed_campaign_never_speak_for_this_boundary(tmp_path, monkeypatch):
"""A campaign resumed across instances (CLB stateful, evolve_smoke --tasks N) carries the previous cycle's
``no_op``/``absorbed`` outcome in its history: a boundary that attaches no new cycle is ``cycle_not_enqueued``,
not the older cycle's outcome."""
_campaign(tmp_path, status="active", source="benchmark", absorbed_cycles_done=1,
transaction_history=[{"cycle_outcome": "absorbed"}, {"cycle_outcome": "no_op"}])
srv = _server(tmp_path, [IDLE], monkeypatch)
out = srv.wait_for_absorb("aaaaaaaa", 1, timeout=1_000, idle_grace=10, idle_polls=2)
assert out["reason"] == "cycle_not_enqueued" and out["campaign"]["history_len"] == 2