diff --git a/tests/e2e/test_goal_executor_achieve.py b/tests/e2e/test_goal_executor_achieve.py new file mode 100644 index 0000000..251beeb --- /dev/null +++ b/tests/e2e/test_goal_executor_achieve.py @@ -0,0 +1,159 @@ +""" +GoalExecutor.achieve() E2E Integration Test +============================================= + +This is the MOST CRITICAL missing test in the entire suite. + +GoalExecutor.achieve() is the central autonomous brain — called in EVERY +bot session via bot_flow.py. Until now, it had ZERO E2E coverage. + +The deleted test_e2e_autonomous_session.py was a lying mock that never +called achieve() at all. The production bug it hid (GoalExecutor instantiated +with wrong args → AttributeError) survived for weeks undetected. + +These tests use REAL XML fixtures, real ScreenIdentity, real GoalPlanner, +real ScreenTopology, and real PathMemory. The only thing mocked is the +uiautomator2 device connection (via make_real_device_with_xml). + +Test Strategy: +1. Provide a sequence of XML dumps simulating screen transitions +2. Call achieve() with a goal the HD Map knows how to route +3. Verify achieve() returns True/False based on structural reality +""" + +import os + +import pytest + + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), "fixtures") + + +def _load_fixture(name: str) -> str: + path = os.path.join(FIXTURE_DIR, name) + with open(path, "r", encoding="utf-8") as f: + return f.read() + + +class TestGoalExecutorAchieveNavigation: + """Tests GoalExecutor.achieve() with real XML fixture sequences.""" + + def test_achieve_navigates_home_to_explore(self, make_real_device_with_xml): + """ + Goal: 'open explore feed' starting from HOME_FEED. + + Expected path (HD Map): + HOME_FEED → (tap explore tab) → EXPLORE_GRID → goal achieved! + + dump_hierarchy call sequence: + 1. perceive() → home_feed (initial state) + 2. _execute_action('tap explore tab') → dump for find_best_node + 3. _execute_action verification → explore_grid (post-click) + 4. perceive() on next iteration → explore_grid (goal check) + 5. _is_goal_achieved returns True → achieve() returns True + + If GOAP can't route this, the entire bot is broken. + """ + from GramAddict.core.goap import GoalExecutor + + home_xml = _load_fixture("home_feed_real.xml") + explore_xml = _load_fixture("explore_grid_real.xml") + + # Sequence: perceive → find_node → verify → perceive (goal check) + xml_sequence = [ + home_xml, # 1. perceive(): identify HOME_FEED + home_xml, # 2. _execute_action: dump for find_best_node + explore_xml, # 3. _execute_action: post-click verify + explore_xml, # 4. perceive(): _is_goal_achieved → True + explore_xml, # 5. safety buffer + ] + + device = make_real_device_with_xml(xml_sequence) + + executor = GoalExecutor(device=device, bot_username="testuser") + + result = executor.achieve("open explore feed", max_steps=5) + + assert result is True, ( + "GoalExecutor failed to navigate from HOME_FEED to EXPLORE_GRID! " + "This is the most basic navigation the bot must be able to do." + ) + + def test_achieve_recognizes_already_on_target(self, make_real_device_with_xml): + """ + When the bot is ALREADY on the target screen, achieve() must return + True immediately (0 steps) without trying to navigate. + + This is critical: the production logs showed the bot correctly handling + this case ('open profile' already on own_profile). + """ + from GramAddict.core.goap import GoalExecutor + + explore_xml = _load_fixture("explore_grid_real.xml") + + # Only 1 dump needed: perceive → already on EXPLORE_GRID + xml_sequence = [ + explore_xml, # perceive(): already on target + explore_xml, # safety buffer + ] + + device = make_real_device_with_xml(xml_sequence) + + executor = GoalExecutor(device=device, bot_username="testuser") + + result = executor.achieve("open explore feed", max_steps=5) + + assert result is True, ( + "GoalExecutor couldn't recognize it's ALREADY on EXPLORE_GRID! " + "This causes unnecessary navigation loops." + ) + + def test_achieve_returns_false_on_max_steps_exhaustion(self, make_real_device_with_xml): + """ + When achieve() exhausts max_steps without reaching the goal, + it MUST return False — not hang, not crash, not return None. + + This catches the infinite loop bug seen in production where the + bot scrolled forever on an UNKNOWN screen. + """ + from GramAddict.core.goap import GoalExecutor + + home_xml = _load_fixture("home_feed_real.xml") + + # Provide only HOME_FEED dumps. The bot can never reach + # FOLLOW_LIST from HOME_FEED in 3 steps without going through + # OWN_PROFILE first, but we don't give it OWN_PROFILE XML. + xml_sequence = [home_xml] * 20 # All dumps return HOME_FEED + + device = make_real_device_with_xml(xml_sequence) + + executor = GoalExecutor(device=device, bot_username="testuser") + + result = executor.achieve("open following list", max_steps=3) + + assert result is False, ( + "GoalExecutor did not return False after exhausting max_steps! " + "This means the bot could loop forever in production." + ) + + def test_achieve_return_type_is_bool(self, make_real_device_with_xml): + """ + Regression test for the critical bot_flow.py lie: + achieve() was compared to 'GOAL_ACHIEVED' (string) instead of True. + + This test guarantees the return type contract is enforced. + """ + from GramAddict.core.goap import GoalExecutor + + explore_xml = _load_fixture("explore_grid_real.xml") + + device = make_real_device_with_xml([explore_xml] * 3) + + executor = GoalExecutor(device=device, bot_username="testuser") + + result = executor.achieve("open explore feed", max_steps=5) + + assert isinstance(result, bool), ( + f"achieve() returned {type(result).__name__} instead of bool! " + f"Value: {result!r}. This breaks the bot_flow.py success check." + )