diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index 9c293c2..980d2ef 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -445,56 +445,68 @@ def start_bot(**kwargs): has_scanned_own_profile = True while not dopamine.is_app_session_over(): - # 1. Ask the Growth Brain for a Strategic Objective - success_rates = getattr(session_state, "successfulInteractions", {}) - current_goal = growth_brain.get_current_goal( - dopamine, getattr(configs.args, "goals", []), success_rates=success_rates + # ── 1. Generate available tasks from mission + plugins ── + from GramAddict.core.goal_decomposer import GoalDecomposer + + decomposer = GoalDecomposer( + plugins=configs.config.get("plugins", {}) if configs.config else {}, + actions={ + k: getattr(configs.args, k, None) + for k in ("feed", "explore", "reels") + if getattr(configs.args, k, None) + }, + mission=configs.config.get("mission", {}) if configs.config else {}, ) + available_tasks = decomposer.generate_tasks() - if current_goal == "ShiftContext": - logger.info("🧠 [Free Will] Boredom critical. Forcing app restart to clear context.") - device.app_stop(device.app_id) - random_sleep(2.0, 4.0) - device.app_start(device.app_id, use_monkey=True) - random_sleep(4.0, 6.0) - dopamine.boredom = max(0.0, dopamine.boredom * 0.2) - continue + if not available_tasks: + # No plugins enabled = nothing to do. Fall back to legacy desire system. + current_desire = growth_brain.get_current_desire(dopamine) + if current_desire == "ShiftContext": + logger.info("🧠 [Free Will] Boredom critical. Forcing app restart.") + device.app_stop(device.app_id) + random_sleep(2.0, 4.0) + device.app_start(device.app_id, use_monkey=True) + random_sleep(4.0, 6.0) + dopamine.boredom = max(0.0, dopamine.boredom * 0.2) + continue - # 2. Execution: GOAP Plan & Execute (Autonomous Mode) - if getattr(configs.args, "goals", None): - logger.info(f"🤖 Autonomous Mode Active. Delegating to GoalExecutor for: {current_goal}") + # Legacy desire → target mapping (kept for backward compatibility) + target_map = { + "DiscoverNewContent": ["ExploreFeed", "ReelsFeed"], + "NurtureCommunity": ["HomeFeed", "StoriesFeed"], + "SocialReciprocity": ["FollowingList"], + } - goal_executor = GoalExecutor(device=device, bot_username=getattr(configs.args, "username", "")) + dm_config = configs.get_plugin_config("dm_reply") + if dm_config.get("enabled", False): + target_map["SocialReciprocity"].append("MessageInbox") - result = goal_executor.achieve(current_goal) + import secrets - if result: - logger.info("✅ Goal achieved autonomously!") - else: - logger.warning(f"⚠️ Goal execution failed for: {current_goal}") + options = target_map.get(current_desire, ["HomeFeed"]) + current_target = secrets.choice(options) + else: + # ── 2. Select a concrete Task ── + selected_task = growth_brain.select_task(dopamine, available_tasks) - continue # The GoalExecutor handles navigation internally + if selected_task is None: + # ShiftContext signal from high boredom + logger.info("🧠 [Free Will] Boredom critical. Forcing app restart to clear context.") + device.app_stop(device.app_id) + random_sleep(2.0, 4.0) + device.app_start(device.app_id, use_monkey=True) + random_sleep(4.0, 6.0) + dopamine.boredom = max(0.0, dopamine.boredom * 0.2) + continue - # --- LEGACY PROCEDURAL FALLBACK (For config without goals) --- - current_desire = current_goal + current_target = selected_task.target_screen + logger.info( + f"🎯 [GoalDecomposer] Task: {selected_task.intent} " + f"→ {current_target} (budget={selected_task.budget_posts})" + ) - # 2. Map Desire to Sub-Feed - target_map = { - "DiscoverNewContent": ["ExploreFeed", "ReelsFeed"], - "NurtureCommunity": ["HomeFeed", "StoriesFeed"], - "SocialReciprocity": ["FollowingList"], - } - - dm_config = configs.get_plugin_config("dm_reply") - if dm_config.get("enabled", False): - target_map["SocialReciprocity"].append("MessageInbox") - - import secrets - - options = target_map.get(current_desire, ["HomeFeed"]) - current_target = secrets.choice(options) - - logger.info(f"🧠 [Agent Orchestrator] Desire '{current_desire}' -> Routed to {current_target}") + logger.info(f"🧠 [Agent Orchestrator] Routed to {current_target}") logger.info(f"⚡ Navigating to {current_target}") success = nav_graph.navigate_to(current_target, zero_engine) diff --git a/tests/tdd/test_goal_decomposer.py b/tests/tdd/test_goal_decomposer.py index 16a2eb0..544cdaa 100644 --- a/tests/tdd/test_goal_decomposer.py +++ b/tests/tdd/test_goal_decomposer.py @@ -317,3 +317,86 @@ class TestGrowthBrainTaskSelection: # With aggressive_growth, ExploreFeed (weight 0.45) should dominate assert len(counts) > 1, "Selection must pick from multiple screens" + + +class TestOrchestratorTaskRouting: + """Tests that every Task produced by GoalDecomposer is routable by ScreenTopology.""" + + def test_task_to_screen_topology_mapping(self): + """Each Task.target_screen MUST exist in ScreenTopology.SCREEN_NAME_MAP.""" + from GramAddict.core.goal_decomposer import GoalDecomposer + from GramAddict.core.screen_topology import ScreenTopology + + plugins = { + "likes": {"percentage": 100}, + "follow": {"percentage": 100}, + "comment": {"percentage": 40}, + "story_view": {"percentage": 80}, + "dm_reply": {"enabled": True}, + } + actions = {"feed": "5-10", "explore": "5-10", "reels": "3-5"} + mission = {"strategy": "aggressive_growth"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + + assert len(tasks) > 0 + + for task in tasks: + assert task.target_screen in ScreenTopology.SCREEN_NAME_MAP, ( + f"Task '{task.verb}' → '{task.target_screen}' has no ScreenTopology mapping!" + ) + + def test_all_task_screens_reachable_from_home(self): + """Every Task target must be reachable via BFS from HOME_FEED.""" + from GramAddict.core.goal_decomposer import GoalDecomposer + from GramAddict.core.screen_topology import ScreenTopology + from GramAddict.core.perception.screen_identity import ScreenType + + plugins = { + "likes": {"percentage": 100}, + "story_view": {"percentage": 80}, + "dm_reply": {"enabled": True}, + } + actions = {"feed": "5-10", "explore": "5-10"} + mission = {"strategy": "community_builder"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + + for task in tasks: + target_type = ScreenTopology.SCREEN_NAME_MAP.get(task.target_screen) + assert target_type is not None + + if target_type == ScreenType.HOME_FEED: + continue # Already there + + route = ScreenTopology.find_route(ScreenType.HOME_FEED, target_type) + assert route is not None, ( + f"No HD Map route from HOME_FEED to {task.target_screen}! " + f"The bot cannot navigate there." + ) + + def test_task_target_screen_to_goal_string_conversion(self): + """Every task target_screen must convert to a valid GOAP goal string.""" + from GramAddict.core.goal_decomposer import GoalDecomposer + from GramAddict.core.screen_topology import ScreenTopology + + plugins = {"likes": {"percentage": 100}, "dm_reply": {"enabled": True}} + actions = {"feed": "5-10", "explore": "5-10"} + mission = {"strategy": "aggressive_growth"} + + decomposer = GoalDecomposer(plugins=plugins, actions=actions, mission=mission) + tasks = decomposer.generate_tasks() + + for task in tasks: + goal_str = ScreenTopology.screen_name_to_goal(task.target_screen) + assert goal_str, ( + f"Task '{task.target_screen}' cannot be converted to GOAP goal!" + ) + # The goal string should resolve back to a target screen + target = ScreenTopology.goal_to_target_screen(goal_str) + assert target is not None, ( + f"Goal string '{goal_str}' doesn't resolve to any ScreenType!" + ) + diff --git a/tests/unit/test_bot_flow_autonomy.py b/tests/unit/test_bot_flow_autonomy.py index 6d9261d..cc4b02e 100644 --- a/tests/unit/test_bot_flow_autonomy.py +++ b/tests/unit/test_bot_flow_autonomy.py @@ -1,23 +1,30 @@ -def test_bot_flow_prioritizes_goals_over_desires(): +def test_bot_flow_uses_goal_decomposer_not_abstract_goals(): """ - Test that when goals are present in config, the bot uses GoalExecutor - instead of the legacy desire mapping. - This should fail (RED) before we refactor bot_flow.py. + Test that bot_flow.py uses GoalDecomposer for task-based navigation + instead of the abstract goals string system. + + The old system passed abstract strings like "Nurture my community" + to GoalExecutor.achieve() — which caused endless scrolling because + the LLM had no semantic bridge to concrete plugin actions. + + The new system: + 1. GoalDecomposer reads mission + plugins → generates concrete Tasks + 2. GrowthBrain.select_task() picks a Task with weighted random + 3. The Task's target_screen routes through nav_graph to feed loops """ - - # We won't run the whole start_bot (it's massive), - # we'll just test the core orchestrator loop extraction if we can, - # or we can test the behavior by mocking the device and config. - - # Actually, a better way is to test that the goal string is passed to achieve. - # Since we can't easily mock the massive `start_bot`, we will test the - # conceptual behavior by just ensuring the code in bot_flow contains - # GoalExecutor.achieve logic. - - # Let's import the file and check for GoalExecutor usage with open("GramAddict/core/bot_flow.py", "r") as f: content = f.read() - # This assertion will fail (RED) because GoalExecutor is not in the original bot_flow.py - assert "GoalExecutor" in content, "bot_flow.py does not use GoalExecutor for autonomous goals" - assert "goal_executor.achieve(current_goal)" in content, "bot_flow.py does not execute goals autonomously" + # New system MUST be present + assert "GoalDecomposer" in content, "bot_flow.py must use GoalDecomposer" + assert "select_task" in content, "bot_flow.py must use GrowthBrain.select_task()" + assert "available_tasks" in content, "bot_flow.py must generate available_tasks" + + # Old abstract goals path MUST be gone + assert "goal_executor.achieve(current_goal)" not in content, ( + "bot_flow.py still uses old abstract goal_executor.achieve()! " + "This causes endless scrolling." + ) + assert 'getattr(configs.args, "goals"' not in content, ( + "bot_flow.py still reads abstract goals from config!" + )