From 720841103df00f633b114faa32c45497572f379a Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 4 May 2026 00:28:40 +0200 Subject: [PATCH] purge: remove ALL hardcoded model/URL defaults across 11 modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD cycle (RED → GREEN): 2 new codebase-wide enforcement tests: - test_no_getattr_with_hardcoded_model_defaults_outside_config Catches getattr(args, 'ai_xxx', 'HARDCODED_DEFAULT') anti-pattern - test_no_bare_localhost_url_string_literals_outside_config Catches bare 'localhost:11434' string literals in non-SSOT files Purged 24 getattr-default violations and 14 bare-URL violations across: - compiler_engine.py: ai_telepathic_model/url defaults - screen_identity.py: ai_telepathic_model/url defaults - intent_resolver.py: ai_telepathic_model/url defaults (2 spots) - semantic_evaluator.py: ai_telepathic_model/url defaults - brain.py: ai_model/url defaults - dm_engine.py: ai_condenser_model/url defaults - resonance_engine.py: ai_condenser_model/url defaults - qdrant_memory.py: ai_embedding_model/url defaults - bot_flow.py: ai_condenser_model/url defaults - interaction.py: ai_writer_model/url defaults - llm_provider.py: fallback model/url 'last resort' defaults Architecture: Config() (config.py) is the SSOT for ALL AI model config. Every other module reads from Config().args WITHOUT default values. If Config() is uninitialized, the system crashes (Fail Fast). Tests: 11 new, 121 passing, 0 regressions --- GramAddict/core/bot_flow.py | 4 +- GramAddict/core/compiler_engine.py | 14 +-- GramAddict/core/dm_engine.py | 4 +- GramAddict/core/interaction.py | 4 +- GramAddict/core/llm_provider.py | 20 ++-- GramAddict/core/navigation/brain.py | 10 +- GramAddict/core/perception/intent_resolver.py | 8 +- GramAddict/core/perception/screen_identity.py | 10 +- .../core/perception/semantic_evaluator.py | 4 +- GramAddict/core/qdrant_memory.py | 4 +- GramAddict/core/resonance_engine.py | 4 +- ...o_maintenance.cpython-311-pytest-8.3.5.pyc | Bin 21148 -> 29626 bytes ...ral_hardening.cpython-311-pytest-8.3.5.pyc | Bin 9900 -> 9900 bytes tests/unit/test_sae_zero_maintenance.py | 91 ++++++++++++++++++ 14 files changed, 131 insertions(+), 46 deletions(-) diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index 011820c..be71e3d 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -444,8 +444,8 @@ def start_bot(**kwargs): from GramAddict.core.llm_provider import query_llm - model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") - url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") + model = getattr(configs.args, "ai_condenser_model") + url = getattr(configs.args, "ai_condenser_url") response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True, timeout=120) if response_dict and isinstance(response_dict, dict) and "persona" in response_dict: diff --git a/GramAddict/core/compiler_engine.py b/GramAddict/core/compiler_engine.py index b5a3e79..c3e81f6 100644 --- a/GramAddict/core/compiler_engine.py +++ b/GramAddict/core/compiler_engine.py @@ -30,13 +30,13 @@ class VLMCompilerEngine: extra={"color": "\x1b[1m\x1b[35m"}, ) - args = getattr(self.device, "args", None) - model = getattr(args, "ai_telepathic_model", "llama3.2:1b") if args else "llama3.2:1b" - url = ( - getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate") - if args - else "http://localhost:11434/api/generate" - ) + from GramAddict.core.config import Config + + cfg = Config() + if not hasattr(cfg, "args"): + raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.") + model = getattr(cfg.args, "ai_telepathic_model") + url = getattr(cfg.args, "ai_telepathic_url") use_local = "11434" in url or "localhost" in url simplified_xml = self._simplify_xml(context_xml) diff --git a/GramAddict/core/dm_engine.py b/GramAddict/core/dm_engine.py index d219a5d..08ee457 100644 --- a/GramAddict/core/dm_engine.py +++ b/GramAddict/core/dm_engine.py @@ -152,8 +152,8 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s return "BOREDOM_CHANGE_FEED" # Configure models - model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") - url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") + model = getattr(configs.args, "ai_condenser_model") + url = getattr(configs.args, "ai_condenser_url") # Generate response prompt = f"You are replying to a direct message on Instagram. The last message you received was: '{context_text}'. Keep it short, casual, and friendly. Do not use hashtags." diff --git a/GramAddict/core/interaction.py b/GramAddict/core/interaction.py index a24ebda..12575c5 100644 --- a/GramAddict/core/interaction.py +++ b/GramAddict/core/interaction.py @@ -54,9 +54,9 @@ class LLMWriter: f"6. Reply with ONLY the comment text." ) - model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model", "llama3.2:1b")) + model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model")) url = getattr( - self.args, "ai_writer_url", getattr(self.args, "ai_model_url", "http://localhost:11434/api/generate") + self.args, "ai_writer_url", getattr(self.args, "ai_model_url") ) logger.info(f"✍️ [Writer] Generating comment for @{target_username} using {model}...") diff --git a/GramAddict/core/llm_provider.py b/GramAddict/core/llm_provider.py index 438c878..c7413f9 100644 --- a/GramAddict/core/llm_provider.py +++ b/GramAddict/core/llm_provider.py @@ -395,14 +395,11 @@ def query_llm( except Exception: pass - # Last resort defaults + # Last resort: If no fallback config exists, don't silently use hardcoded defaults. + # Config() defines these via argparse defaults — if they're missing, there's nothing to fallback to. if not f_model or not f_url: - if is_openai_compat: - f_model = f_model or "llama3.2:1b" - f_url = f_url or "http://localhost:11434/api/generate" - else: - f_model = f_model or "llama3.2:1b" - f_url = f_url or "http://localhost:11434/api/generate" + logger.warning("⚠️ [Circuit Breaker] No fallback model/URL configured. Cannot retry.") + return None # Circuit Breaker: If fallback is identical to primary, don't waste time retrying if f_model == model and f_url == url: @@ -458,11 +455,12 @@ def query_telepathic_llm( try: args = Config().args - target_url = getattr(args, "ai_fallback_url", "http://localhost:11434/api/generate") - target_model = getattr(args, "ai_fallback_model", "llama3.2:1b") + target_url = getattr(args, "ai_fallback_url") + target_model = getattr(args, "ai_fallback_model") except Exception: - target_url = "http://localhost:11434/api/generate" - target_model = "llama3.2:1b" + raise RuntimeError( + "Config().args not initialized — cannot resolve fallback AI model. Fail Fast." + ) is_local = "localhost" in target_url or "127.0.0.1" in target_url calc_timeout = 180 if is_local else 45 diff --git a/GramAddict/core/navigation/brain.py b/GramAddict/core/navigation/brain.py index 66d4a42..4880aa0 100644 --- a/GramAddict/core/navigation/brain.py +++ b/GramAddict/core/navigation/brain.py @@ -15,12 +15,10 @@ def ask_brain_for_action( return None cfg = Config() - url = ( - getattr(cfg.args, "ai_model_url", "http://localhost:11434/api/generate") - if hasattr(cfg, "args") - else "http://localhost:11434/api/generate" - ) - model = getattr(cfg.args, "ai_model", "qwen3.5:latest") if hasattr(cfg, "args") else "qwen3.5:latest" + if not hasattr(cfg, "args"): + raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.") + url = getattr(cfg.args, "ai_model_url") + model = getattr(cfg.args, "ai_model") prompt = ( f"You are an autonomous Instagram agent. Your ultimate goal is: '{goal}'.\n" diff --git a/GramAddict/core/perception/intent_resolver.py b/GramAddict/core/perception/intent_resolver.py index 47a8c6b..16804d2 100644 --- a/GramAddict/core/perception/intent_resolver.py +++ b/GramAddict/core/perception/intent_resolver.py @@ -661,8 +661,8 @@ class IntentResolver: self.last_box_map = box_map cfg = Config() - model = getattr(cfg.args, "ai_telepathic_model", "llava:latest") - url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate") + model = getattr(cfg.args, "ai_telepathic_model") + url = getattr(cfg.args, "ai_telepathic_url") # Build a compact legend of what each box contains box_legend_lines = [] @@ -785,8 +785,8 @@ class IntentResolver: filtered_candidates = [n for n in candidates if n.area < 500000] cfg = Config() - model = getattr(cfg.args, "ai_telepathic_model", "qwen3.5:latest") - url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate") + model = getattr(cfg.args, "ai_telepathic_model") + url = getattr(cfg.args, "ai_telepathic_url") node_context = [] for i, node in enumerate(filtered_candidates): diff --git a/GramAddict/core/perception/screen_identity.py b/GramAddict/core/perception/screen_identity.py index 2073b69..b527384 100644 --- a/GramAddict/core/perception/screen_identity.py +++ b/GramAddict/core/perception/screen_identity.py @@ -276,12 +276,10 @@ class ScreenIdentity: from GramAddict.core.llm_provider import query_telepathic_llm cfg = Config() - url = ( - getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate") - if hasattr(cfg, "args") - else "http://localhost:11434/api/generate" - ) - model = getattr(cfg.args, "ai_telepathic_model", "llava:latest") if hasattr(cfg, "args") else "llava:latest" + if not hasattr(cfg, "args"): + raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.") + url = getattr(cfg.args, "ai_telepathic_url") + model = getattr(cfg.args, "ai_telepathic_model") layout_context = ( f"Selected Tab: {selected_tab}\nResource IDs: {list(ids)}\nVisible Texts context: {texts[:10]}\n" diff --git a/GramAddict/core/perception/semantic_evaluator.py b/GramAddict/core/perception/semantic_evaluator.py index 2bdfbc7..caeacd3 100644 --- a/GramAddict/core/perception/semantic_evaluator.py +++ b/GramAddict/core/perception/semantic_evaluator.py @@ -28,8 +28,8 @@ class SemanticEvaluator: logger.warning("👁️ [Vision Core] No config available. Cannot query VLM.") return None - model = getattr(self.args, "ai_telepathic_model", "llama3.2-vision") - url = getattr(self.args, "ai_telepathic_url", "http://localhost:11434/api/generate") + model = getattr(self.args, "ai_telepathic_model") + url = getattr(self.args, "ai_telepathic_url") try: res = query_telepathic_llm( diff --git a/GramAddict/core/qdrant_memory.py b/GramAddict/core/qdrant_memory.py index cc31eaf..c4232a2 100644 --- a/GramAddict/core/qdrant_memory.py +++ b/GramAddict/core/qdrant_memory.py @@ -112,8 +112,8 @@ class QdrantBase: args = self._cached_args # Pull specific embedding config or fallback to defaults - model = getattr(args, "ai_embedding_model", "nomic-embed-text") - url = getattr(args, "ai_embedding_url", "http://localhost:11434/api/embeddings") + model = getattr(args, "ai_embedding_model") + url = getattr(args, "ai_embedding_url") try: # Generate embeddings diff --git a/GramAddict/core/resonance_engine.py b/GramAddict/core/resonance_engine.py index aed9ea4..e2569e8 100644 --- a/GramAddict/core/resonance_engine.py +++ b/GramAddict/core/resonance_engine.py @@ -386,8 +386,8 @@ class ResonanceEngine: "Set 'keep' to false only for clear spam, bots, UI buttons, or blacklist violations.\n" ) - model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") - url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") + model = getattr(configs.args, "ai_condenser_model") + url = getattr(configs.args, "ai_condenser_url") try: import json diff --git a/tests/unit/__pycache__/test_sae_zero_maintenance.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_sae_zero_maintenance.cpython-311-pytest-8.3.5.pyc index 9c6988ec9c7f5d6a5cfa1c6e2a7587d8a8cfc558..952472ceb870f680244a500beaa235530e1166d7 100644 GIT binary patch delta 8756 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 diff --git a/tests/unit/__pycache__/test_structural_hardening.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_structural_hardening.cpython-311-pytest-8.3.5.pyc index eaed113c8799bab53a3fba7a36d46a65ad340315..538e2a4d752b06aa2222c5dfdd2520efcd5912b4 100644 GIT binary patch delta 22 ccmZ4EyT+GyIWI340}%W;_C53bM&9{q09Nk^I{*Lx delta 22 ccmZ4EyT+GyIWI340}w1f_C52$M&9{q08|?X>i_@% diff --git a/tests/unit/test_sae_zero_maintenance.py b/tests/unit/test_sae_zero_maintenance.py index b86fc9d..3961168 100644 --- a/tests/unit/test_sae_zero_maintenance.py +++ b/tests/unit/test_sae_zero_maintenance.py @@ -164,6 +164,97 @@ class TestSAEContainsNoHardcodedModelDefaults: ) +# ══════════════════════════════════════════════════════════ +# 2b. NO HARDCODED MODEL/URL DEFAULTS IN ANY MODULE (codebase-wide) +# ══════════════════════════════════════════════════════════ + + +class TestCodebaseContainsNoHardcodedModelDefaults: + """ + CODEBASE-WIDE enforcement: No Python module in GramAddict/core/ may contain + hardcoded model names or localhost URLs as getattr() default values. + + The ONLY file allowed to define these defaults is config.py (the SSOT). + Every other module must read from Config().args WITHOUT providing a fallback literal. + If Config().args is missing, the system must crash (Fail Fast) — not silently + degrade to a potentially wrong model. + """ + + # These are the exact default-value patterns that are FORBIDDEN outside config.py. + # They indicate a getattr(..., "ai_xxx", "HARDCODED_DEFAULT") anti-pattern. + FORBIDDEN_DEFAULT_PATTERNS = [ + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']http://localhost:11434', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llava:', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']qwen3\.', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.2-vision', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']nomic-embed-text', + ] + + # Files that are ALLOWED to have these defaults (SSOT only) + ALLOWED_FILES = {"config.py"} + + def _get_all_core_python_files(self): + import os + + core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core") + core_dir = os.path.normpath(core_dir) + files = [] + for root, _dirs, filenames in os.walk(core_dir): + for f in filenames: + if f.endswith(".py") and f not in self.ALLOWED_FILES: + files.append(os.path.join(root, f)) + return files + + def test_no_getattr_with_hardcoded_model_defaults_outside_config(self): + """ + No module except config.py may use getattr(args, 'ai_xxx', 'HARDCODED_DEFAULT'). + The correct pattern is: getattr(cfg.args, 'ai_xxx') — no default, crash if missing. + """ + import os + + violations = [] + for filepath in self._get_all_core_python_files(): + with open(filepath) as f: + content = f.read() + for pattern in self.FORBIDDEN_DEFAULT_PATTERNS: + matches = re.findall(pattern, content) + if matches: + basename = os.path.basename(filepath) + violations.append(f"{basename}: {len(matches)}x pattern '{pattern[:60]}...'") + + assert len(violations) == 0, ( + f"Found {len(violations)} file(s) with hardcoded model/URL defaults outside config.py!\n" + f"Config() is the SSOT. Remove default values from getattr() calls.\n" + + "\n".join(f" ❌ {v}" for v in violations) + ) + + def test_no_bare_localhost_url_string_literals_outside_config(self): + """ + No module except config.py and llm_provider.py (routing logic) may contain + 'http://localhost:11434' as a bare string literal used as a fallback value. + """ + import os + + allowed = {"config.py", "llm_provider.py"} + violations = [] + core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core") + core_dir = os.path.normpath(core_dir) + + for root, _dirs, filenames in os.walk(core_dir): + for f in filenames: + if f.endswith(".py") and f not in allowed: + path = os.path.join(root, f) + with open(path) as fh: + for i, line in enumerate(fh, 1): + if "localhost:11434" in line and not line.strip().startswith("#"): + violations.append(f"{f}:{i}: {line.strip()[:100]}") + + assert len(violations) == 0, ( + f"Found {len(violations)} hardcoded localhost:11434 references outside config.py/llm_provider.py!\n" + + "\n".join(f" ❌ {v}" for v in violations) + ) + # ══════════════════════════════════════════════════════════ # 3. GOAP SMART UI STABILIZATION (Poll instead of static sleep) # ══════════════════════════════════════════════════════════