diff --git a/src/ai_client.py b/src/ai_client.py index 8b737193..68162d43 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -2970,17 +2970,22 @@ def _get_llama_cost_tracking() -> bool: #region: Tier 4 Analysis -def run_tier4_analysis(stderr: str) -> str: +def _run_tier4_analysis_result(stderr: str) -> Result[str]: + """Tier 4 QA agent: analyze stderr and propose a fix in ~20 words. + + Returns Result(data=analysis) on success, Result(data="", errors=[ErrorInfo]) + on SDK failure. The legacy caller (run_tier4_analysis) returns result.data + (preserving the original str signature; failures surface as empty string + to keep the qa_callback contract). """ - """ - genai = _require_warmed("google.genai") - types = genai.types if not stderr or not stderr.strip(): - return "" + return Result(data="") try: _ensure_gemini_client() if not _gemini_client: - return "" + return Result(data="") + genai = _require_warmed("google.genai") + types = genai.types prompt = ( f"You are a Tier 4 QA Agent specializing in error analysis.\n" f"Analyze the following stderr output from a PowerShell command:\n\n" @@ -2997,15 +3002,29 @@ def run_tier4_analysis(stderr: str) -> str: ) ) analysis = resp.text.strip() if resp.text else "" - return analysis + return Result(data=analysis) except Exception as e: - return f"[QA ANALYSIS FAILED] {e}" + return Result( + data="", + errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"[QA ANALYSIS FAILED] {e}", source="ai_client._run_tier4_analysis_result", original=e)], + ) + + +def run_tier4_analysis(stderr: str) -> str: + return _run_tier4_analysis_result(stderr).data #endregion: Tier 4 Analysis #region: Session & Public API -def run_tier4_patch_callback(stderr: str, base_dir: str) -> Optional[str]: +def _run_tier4_patch_callback_result(stderr: str, base_dir: str) -> Result[Optional[str]]: + """Tier 4 QA agent: propose a unified-diff patch for the stderr. + + Returns Result(data=patch) when a valid diff is produced, Result(data=None) + when no valid diff, Result(data=None, errors=[ErrorInfo]) on SDK failure. + The legacy caller (run_tier4_patch_callback) returns result.data + (preserving the original Optional[str] signature). + """ try: file_items = project_manager.get_current_file_items() file_context = "" @@ -3015,23 +3034,34 @@ def run_tier4_patch_callback(stderr: str, base_dir: str) -> Optional[str]: file_context += f"\n\nFile: {path}\n```\n{content}\n```\n" patch = run_tier4_patch_generation(stderr, file_context) if patch and "---" in patch and "+++" in patch: - return patch - return None + return Result(data=patch) + return Result(data=None) except Exception as e: - return None + return Result( + data=None, + errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"tier4 patch callback failed: {e}", source="ai_client._run_tier4_patch_callback_result", original=e)], + ) -def run_tier4_patch_generation(error: str, file_context: str) -> str: + +def run_tier4_patch_callback(stderr: str, base_dir: str) -> Optional[str]: + return _run_tier4_patch_callback_result(stderr, base_dir).data + +def _run_tier4_patch_generation_result(error: str, file_context: str) -> Result[str]: + """Tier 4 QA agent: generate a unified-diff patch for the given error. + + Returns Result(data=patch) on success, Result(data="", errors=[ErrorInfo]) + on SDK failure. The legacy caller (run_tier4_patch_generation) returns + result.data (preserving the original str signature; failures surface as + empty string to keep callers' downstream code working). """ - [C: src/gui_2.py:App.request_patch_from_tier4, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_calls_ai, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_empty_error, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_returns_diff] - """ - genai = _require_warmed("google.genai") - types = genai.types if not error or not error.strip(): - return "" + return Result(data="") try: _ensure_gemini_client() if not _gemini_client: - return "" + return Result(data="") + genai = _require_warmed("google.genai") + types = genai.types prompt = ( f"{mma_prompts.TIER4_PATCH_PROMPT}\n\n" f"Error:\n```\n{error}\n```\n\n" @@ -3047,9 +3077,19 @@ def run_tier4_patch_generation(error: str, file_context: str) -> str: ) ) patch = resp.text.strip() if resp.text else "" - return patch + return Result(data=patch) except Exception as e: - return f"[PATCH GENERATION FAILED] {e}" + return Result( + data="", + errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"[PATCH GENERATION FAILED] {e}", source="ai_client._run_tier4_patch_generation_result", original=e)], + ) + + +def run_tier4_patch_generation(error: str, file_context: str) -> str: + """ + [C: src/gui_2.py:App.request_patch_from_tier4, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_calls_ai, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_empty_error, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_returns_diff] + """ + return _run_tier4_patch_generation_result(error, file_context).data def get_token_stats(md_content: str) -> dict[str, Any]: """ diff --git a/tests/tier2/phase10_sites789_test.py b/tests/tier2/phase10_sites789_test.py new file mode 100644 index 00000000..588ab9d3 --- /dev/null +++ b/tests/tier2/phase10_sites789_test.py @@ -0,0 +1,54 @@ +"""Phase 10 sites 7+8+9: run_tier4_* Result helpers. + +Site 7 (run_tier4_analysis): returns str with '[QA ANALYSIS FAILED]' on error. +Site 8 (run_tier4_patch_callback): returns Optional[str] with None on error. +Site 9 (run_tier4_patch_generation): returns str with '[PATCH GENERATION FAILED]' on error. + +All 3 follow the same pattern: + try: ...AI call... + except Exception as e: return "[XXX FAILED] {e}" (or None) + +Migrate via Result[str] / Result[Optional[str]] helpers. +""" +import sys +sys.path.insert(0, ".") + + +def test_phase10_sites789_run_tier4_analysis_result_exists(): + import src.ai_client + assert hasattr(src.ai_client, "_run_tier4_analysis_result"), \ + "_run_tier4_analysis_result helper missing" + + +def test_phase10_sites789_run_tier4_patch_callback_result_exists(): + import src.ai_client + assert hasattr(src.ai_client, "_run_tier4_patch_callback_result"), \ + "_run_tier4_patch_callback_result helper missing" + + +def test_phase10_sites789_run_tier4_patch_generation_result_exists(): + import src.ai_client + assert hasattr(src.ai_client, "_run_tier4_patch_generation_result"), \ + "_run_tier4_patch_generation_result helper missing" + + +def test_phase10_sites789_all_helpers_return_result(): + import src.ai_client + import inspect + for name in ("_run_tier4_analysis_result", + "_run_tier4_patch_callback_result", + "_run_tier4_patch_generation_result"): + fn = getattr(src.ai_client, name) + sig = inspect.signature(fn) + assert "Result" in str(sig.return_annotation), \ + f"{name} return must be Result, got {sig.return_annotation}" + + +def test_phase10_sites789_legacy_unchanged(): + """Legacy functions must still exist + be callable.""" + import src.ai_client + for name in ("run_tier4_analysis", + "run_tier4_patch_callback", + "run_tier4_patch_generation"): + assert hasattr(src.ai_client, name), f"{name} missing" + assert callable(getattr(src.ai_client, name)), f"{name} not callable" \ No newline at end of file