"""Act identity must never be decided on a thin margin. Bug 28: detect_current_act committed to whatever town marker won, and in Act 4 the A5_TOWN_1 template scored 0.636 on corner scenery while the genuine A4_TOWN_5 scored 0.619. A 1.7pp gap decided the act, and the bot ran A5 pathing inside Act 4 after every Halbu repair trip. That was fixed in detect_current_act and NOT in wait_for_town_spawn, which runs first every game and sets the act for everything after it. On 2026-08-28 the character spawned in Harrogath and it reported a4_town_start every game. These tests pin the invariant for BOTH, because the bug has now been the same twice: a fix applied to one call site while its twin kept the defect. """ import inspect import pytest ACT_DECIDING_FUNCS = ["detect_current_act", "wait_for_town_spawn"] @pytest.mark.parametrize("func_name", ACT_DECIDING_FUNCS) def test_act_decision_requires_a_margin(func_name): from town.town_manager import TownManager src = inspect.getsource(getattr(TownManager, func_name)) assert "_ACT_DETECT_MARGIN" in src, ( f"{func_name} decides an act without requiring a margin over the best " "marker from another act — a marginal winner is not evidence" ) assert "TOWN_MARKERS_ACT" in src, f"{func_name} does not separate own-act from rival markers" @pytest.mark.parametrize("func_name", ACT_DECIDING_FUNCS) def test_act_decision_scores_both_sides_on_one_image(func_name): """Comparing scores from two different grabs is not a comparison. The character can move between them, so the margin becomes meaningless. """ from town.town_manager import TownManager src = inspect.getsource(getattr(TownManager, func_name)) grabs = src.count("grab(force_new=True)") + src.count("grab()") assert grabs <= 1, ( f"{func_name} grabs {grabs} images; own-act and rival scores must come " "from the same frame to be comparable" ) def test_margin_is_meaningfully_large(): from town.town_manager import TownManager assert TownManager._ACT_DETECT_MARGIN >= 0.03, "margin too small to separate a phantom from a real marker" def test_ambiguous_detection_returns_none_not_a_guess(): """Refusing to answer is the safe outcome. Callers treat None as "keep the assumed act", which makes go_to_act actually travel rather than wrongly early-returning. A confident wrong answer skips the recovery path entirely. """ from town.town_manager import TownManager src = inspect.getsource(TownManager.detect_current_act) assert "refusing to guess" in src assert src.rstrip().endswith("return None")