diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index 626583c..a438f57 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -92,9 +92,8 @@ def check_production_integrity(): """ import sys - # If we are in a pytest session, we expect and allow mocks - if "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ: - return + # We no longer skip this in tests. Production integrity must hold everywhere. + pass try: from unittest.mock import MagicMock diff --git a/GramAddict/core/config.py b/GramAddict/core/config.py index a86c8d3..c3fafa1 100644 --- a/GramAddict/core/config.py +++ b/GramAddict/core/config.py @@ -17,13 +17,7 @@ class Config: self.args = kwargs self.module = True else: - # Avoid parsing sys.argv if we are running in a test environment (pytest) - # as pytest arguments will cause argparse to fail with SystemExit: 2 - is_pytest = "pytest" in sys.modules - if is_pytest: - self.args = [] - else: - self.args = list(sys.argv) + self.args = list(sys.argv) self.module = False if not self.module and "--config" not in self.args: diff --git a/GramAddict/core/perception/intent_resolver.py b/GramAddict/core/perception/intent_resolver.py index 0b05e57..281ea54 100644 --- a/GramAddict/core/perception/intent_resolver.py +++ b/GramAddict/core/perception/intent_resolver.py @@ -69,10 +69,11 @@ class IntentResolver: # - "tap profile tab", "tap home tab", "explore tab" # - NOT "exclude bottom tabs", "tabbar", random mentions import re + _TAB_PATTERN = re.compile( - r"\btap\s+\w+\s+tab\b" # "tap profile tab", "tap home tab" - r"|\b\w+\s+tab\b" # "profile tab", "explore tab" - r"|^tab\b", # "tab" at start of intent + r"\btap\s+\w+\s+tab\b" # "tap profile tab", "tap home tab" + r"|\b\w+\s+tab\b" # "profile tab", "explore tab" + r"|^tab\b", # "tab" at start of intent re.IGNORECASE, ) filtered = [] @@ -277,6 +278,27 @@ class IntentResolver: logger.info(f"🎯 [Structural Fast-Path] Found first post/item: {rid}") return node + # --- Navigation Tab Fast-Paths --- + # Deterministically identify bottom navigation tabs to prevent VLM confusion + tab_map = { + "home tab": "feed_tab", + "feed tab": "feed_tab", + "reels tab": "clips_tab", + "clips tab": "clips_tab", + "explore tab": "search_tab", + "search tab": "search_tab", + "profile tab": "profile_tab", + "message tab": "direct_tab", + "direct tab": "direct_tab", + } + for intent_key, resource_suffix in tab_map.items(): + if intent_key in intent_lower: + for node in candidates: + rid = (node.resource_id or "").lower() + if rid.endswith(f":id/{resource_suffix}"): + logger.info(f"🎯 [Structural Fast-Path] Found {intent_key}: {rid}") + return node + # --- Semantic Match Guard --- # If the intent explicitly quotes a target (e.g., "tap 'New Message'"), # we strictly filter candidates to those whose text or content_desc contains the quote. diff --git a/GramAddict/core/persistent_list.py b/GramAddict/core/persistent_list.py index e3c9f82..f721cbf 100644 --- a/GramAddict/core/persistent_list.py +++ b/GramAddict/core/persistent_list.py @@ -13,7 +13,8 @@ class PersistentList(list): self.load() def load(self): - path = f"accounts/{self.filename}.json" + base_dir = os.environ.get("GRAMADDICT_ACCOUNTS_DIR", "accounts") + path = f"{base_dir}/{self.filename}.json" if os.path.exists(path): try: with open(path, "r") as f: @@ -27,9 +28,8 @@ class PersistentList(list): self.persist() def persist(self, directory=None): - if os.environ.get("PYTEST_CURRENT_TEST"): - return - folder = f"accounts/{directory}" if directory else "accounts" + base_dir = os.environ.get("GRAMADDICT_ACCOUNTS_DIR", "accounts") + folder = f"{base_dir}/{directory}" if directory else base_dir os.makedirs(folder, exist_ok=True) path = f"{folder}/{self.filename}.json" try: diff --git a/GramAddict/core/situational_awareness.py b/GramAddict/core/situational_awareness.py index 66c64dd..a50077d 100644 --- a/GramAddict/core/situational_awareness.py +++ b/GramAddict/core/situational_awareness.py @@ -536,7 +536,9 @@ class SituationalAwarenessEngine: "- If you see a dismiss/close/cancel/skip/not now button, click it\n" "- If the Situation type is obstacle_locked_screen, action must be 'unlock'\n" "- If the Situation type is obstacle_foreign_app, action must be 'kill_foreign_apps'\n" - "- If the Situation type is obstacle_system, look for 'Deny', 'Don't allow', or 'Cancel' and click it. If none exist, action must be 'back'\n" + "- If the Situation type is obstacle_system, you MUST look for 'Deny', 'Don't allow', or 'Cancel' and click it. \n" + " NEVER click 'Allow', 'OK', or 'Confirm' on system permissions.\n" + " If no negative action button exists, action must be 'back'\n" "- If there is NO obstacle and the screen is a normal Instagram view (false positive), action must be 'false_positive'\n" "- If nothing else works, suggest 'app_start' to force-reopen Instagram\n" "- NEVER click 'OK'/'Confirm'/'Accept' on surveys or prompts\n" diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 0f0eb88..a3206b0 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -16,7 +16,6 @@ import time import pytest -from GramAddict.core import utils from GramAddict.core.session_state import SessionState # ═══════════════════════════════════════════════════════ @@ -135,22 +134,18 @@ def iteration_guard(): # ═══════════════════════════════════════════════════════ -@pytest.fixture(scope="function", autouse=True) -def isolated_screen_memory(monkeypatch): - """Ensures we use a separate Qdrant collection for E2E tests and clean it. - This replaces the old Qdrant mock so tests use the REAL database.""" - from GramAddict.core.qdrant_memory import ScreenMemoryDB +@pytest.fixture(scope="session", autouse=True) +def session_env(tmp_path_factory): + """ + Forces production parity by using real logic with local-only backends. + """ + # 1. Honest Qdrant: Use real QdrantClient with in-memory storage + os.environ["QDRANT_URL"] = ":memory:" - def test_init(self, *args, **kwargs): - super(ScreenMemoryDB, self).__init__(collection_name="test_e2e_screens") - - monkeypatch.setattr(ScreenMemoryDB, "__init__", test_init) - - db = ScreenMemoryDB() - if db.is_connected: - db.wipe_collection() - - yield db + # 2. Honest Persistence: Use a temporary directory for accounts + accounts_dir = tmp_path_factory.mktemp("accounts") + os.environ["GRAMADDICT_ACCOUNTS_DIR"] = str(accounts_dir) + os.makedirs(os.path.join(str(accounts_dir), "testuser"), exist_ok=True) @pytest.fixture(scope="function", autouse=True) @@ -470,31 +465,7 @@ def _patch_module_delays(monkeypatch, module_path: str, sleep_fn, random_sleep_f monkeypatch.setattr(mod.random, "uniform", lambda a, b: float(a)) -@pytest.fixture(autouse=True) -def mock_all_delays(monkeypatch, request): - """Replaces all humanized hardware delays with no-ops.""" - if request.config.getoption("--live"): - return - - def money_sleep(*args, **kwargs): - pass - - def random_sleep(*args, **kwargs): - pass - - monkeypatch.setattr(time, "sleep", money_sleep) - monkeypatch.setattr(utils, "random_sleep", random_sleep) - monkeypatch.setattr(utils, "sleep", money_sleep) - if hasattr(utils, "random"): - monkeypatch.setattr(utils.random, "uniform", lambda a, b: float(a)) - - # Each module gets its own try-block so a missing attribute in one - # doesn't prevent patching the others. - _patch_module_delays(monkeypatch, "GramAddict.core.bot_flow", money_sleep, random_sleep) - _patch_module_delays(monkeypatch, "GramAddict.core.q_nav_graph", money_sleep, random_sleep) - _patch_module_delays(monkeypatch, "GramAddict.core.goap", money_sleep, random_sleep) - _patch_module_delays(monkeypatch, "GramAddict.core.device_facade", money_sleep, random_sleep) - _patch_module_delays(monkeypatch, "GramAddict.core.darwin_engine", money_sleep, random_sleep) +# Note: mock_all_delays removed to favor production 'speed_multiplier' logic. # ═══════════════════════════════════════════════════════ @@ -526,7 +497,7 @@ def e2e_configs(): stories_percentage=100, working_hours=[0.0, 24.0], time_delta_session=0, - speed_multiplier=1.0, + speed_multiplier=100.0, disable_filters=False, interaction_users_amount="1", scrape_profiles=False, diff --git a/tests/e2e/test_system_integrity_contracts.py b/tests/e2e/test_system_integrity_contracts.py index 81acee8..b24ffc1 100644 --- a/tests/e2e/test_system_integrity_contracts.py +++ b/tests/e2e/test_system_integrity_contracts.py @@ -205,10 +205,7 @@ class TestProductionParityContract: # Known, audited divergence points. Any NEW divergence must be added here # with a justification, or the test fails. KNOWN_DIVERGENCES = { - # (file_basename, line_number): "justification" - ("persistent_list.py", 30): "Prevents test side-effects on disk. Covered by TestPersistenceContract.", - ("bot_flow.py", 96): "check_production_integrity: validates no MagicMocks in prod. Not needed in tests.", - ("config.py", 22): "Prevents pytest args from being parsed by configargparse. Structural necessity.", + # No known divergences. 100% production parity achieved. } def test_all_test_divergence_points_are_audited(self):