diff --git a/tests/e2e/test_goap_trap_recovery.py b/tests/e2e/test_goap_trap_recovery.py new file mode 100644 index 0000000..e2fa7c1 --- /dev/null +++ b/tests/e2e/test_goap_trap_recovery.py @@ -0,0 +1,75 @@ +""" +E2E: GOAP Trap Recovery +======================== +Validates that the GOAP engine can properly escape "completely trapped" states +by forcing an app restart, and that the internal failure states are correctly +purged so it doesn't immediately trap itself again on the next iteration. +""" + +from GramAddict.core.goap import GoalExecutor +from GramAddict.core.screen_topology import ScreenTopology +from tests.e2e.conftest import load_fixture_xml + +def test_goap_recovers_from_trapped_state_with_restart(e2e_device, monkeypatch): + """ + Simulates a broken UI where an action repeatedly fails. + Verifies that GOAP triggers 'force start instagram', and successfully + clears its tracking arrays (action_failures, etc.) to start fresh, + avoiding an infinite restart loop. + """ + xml = load_fixture_xml("home_feed_real.xml") + + # We create a device that ALWAYS returns the same home_feed XML, + # no matter what action is taken. This forces the UI to never change, + # which causes actions to fail repeatedly. + device = e2e_device([xml] * 20) + + goap = GoalExecutor.get_instance(device, bot_username="testuser") + goap.action_failures.clear() + + # We intercept app_start to track how many times it was forced to restart + restart_count = 0 + def mock_app_start(*args, **kwargs): + nonlocal restart_count + restart_count += 1 + + monkeypatch.setattr(device, "app_start", mock_app_start) + + # We want to ask it to reach REELS_FEED. + # Since the UI never changes, 'tap reels tab' will fail. + # It will fail twice, get masked, GOAP gets trapped, triggers restart. + # We set max_steps to a small number so it doesn't loop forever in the test. + # We just want to see that AFTER the restart, it tries 'tap reels tab' again, + # meaning it cleared the state. + + # Let's mock _execute_action slightly just to spy on it without changing behavior + original_execute = goap._execute_action + executed_actions = [] + + def spy_execute(action, goal=None): + executed_actions.append(action) + return original_execute(action, goal) + + monkeypatch.setattr(goap, "_execute_action", spy_execute) + + goap.achieve("open reels", max_steps=6) + + # It should have tried 'tap reels tab' twice, failed both times, + # then triggered 'force start instagram'. + # Because of the fix, after the restart, it should have cleared failures + # and tried 'tap reels tab' AGAIN. + + assert restart_count > 0, "GOAP never attempted to force restart Instagram when trapped!" + + # Count how many times it tried the action + reels_attempts = executed_actions.count("tap reels tab") + + assert reels_attempts > 2, ( + f"GOAP got trapped, restarted, but never tried the action again! " + f"It only tried {reels_attempts} times, meaning the memory leak is still there. " + f"Actions executed: {executed_actions}" + ) + + # If the bug was present, it would only try 'tap reels tab' 2 times, mask it forever, + # and then spam 'force start instagram' for the remaining steps. + # With the fix, it tries 2 times, restarts, tries 2 times, restarts, etc.