Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions tests/test_loop_turn_controller_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,100 @@ def test_conjunction_does_not_depend_on_json_object_key_order(monkeypatch):
for row, outcomes in _ROWS:
for route, expected in zip(_ROUTES, outcomes, strict=True):
test_independent_full_decision_cells(row, route, expected)


# The controller evaluates `rules` in order and stops at the first match, so a
# rule that refines another must stay in front of it. Key order inside one rule
# is immaterial (see the conjunction test above); this sequence is not.
_RULE_SEQUENCE = (
"check_envelope",
"check_decision_actor",
"check_receipt_binding",
"capability",
"check_initial_terminal",
"initial_terminal",
"check_initial_todo",
"initial_route",
"check_completion_terminal",
"completion_terminal",
"check_completion_todo",
"completion_successor",
"completion_active_goal",
"check_receipt_todo",
"decision_user",
"check_progress_budget",
"progress_exhausted",
"progress_route",
"receipt_replan_required",
"receipt_repair_required",
"receipt_user_action_required",
"receipt_wait",
"receipt_iteration_failed",
"host_replan",
"host_repair",
"check_host_retry",
"host_retry",
"host_exhausted",
"failed_receipt",
)


def test_contract_rule_sequence_is_pinned():
"""A reordered contract is a different program; regeneration must not hide it."""

rules = controller._LOOP_CONTROLLER_CONTRACT["rules"]
assert tuple(rule["id"] for rule in rules) == _RULE_SEQUENCE


def _decisions_for(contract, monkeypatch):
"""Every cell's operator-visible decision: the disposition and its reason.

Two rules can share a disposition and still explain it differently, so the
reason is part of the observable decision, not commentary.
"""

monkeypatch.setattr(controller, "_LOOP_CONTROLLER_CONTRACT", contract)
observed = {}
for row, outcomes in _ROWS:
for route in _ROUTES:
arguments = _cell(row, route)
try:
payload = controller.decide_loop_disposition(**arguments)
except ValueError:
observed[(row, route)] = ("reject", "reject")
else:
observed[(row, route)] = (
payload["disposition"],
str(payload["reason"]),
)
return observed


@pytest.mark.parametrize(
"specific, general",
[
("progress_exhausted", "progress_route"),
("host_replan", "failed_receipt"),
("host_repair", "failed_receipt"),
("host_retry", "failed_receipt"),
("host_exhausted", "failed_receipt"),
],
)
def test_a_refining_rule_must_precede_the_rule_it_refines(monkeypatch, specific, general):
"""Moving the specific rule behind the general one must change a real decision.

This is what `_RULE_SEQUENCE` protects: each pinned precedence is load
bearing, so the pin is not a restatement of the shipped file.
"""

contract = deepcopy(controller._LOOP_CONTROLLER_CONTRACT)
ids = [rule["id"] for rule in contract["rules"]]
assert ids.index(specific) < ids.index(general)
moved = next(rule for rule in contract["rules"] if rule["id"] == specific)
rules = [rule for rule in contract["rules"] if rule["id"] != specific]
rules.insert([rule["id"] for rule in rules].index(general) + 1, moved)
contract["rules"] = rules

shipped = _decisions_for(deepcopy(controller._LOOP_CONTROLLER_CONTRACT), monkeypatch)
observed = _decisions_for(contract, monkeypatch)
assert observed != shipped
Loading