""" LLM-based log judgment for system tests. Evaluates logs against expected behavior using LLM semantic analysis. Returns structured pass/fail results with reasoning. """ import json import re import logging from dataclasses import dataclass from .client import LLMClient logger = logging.getLogger(__name__) JUDGE_SYSTEM_PROMPT = """\ You are a test result judge for a CI/CD system called HoneyBiscuitWorkshop. Your job: Evaluate whether actual command output/logs match expected behavior. RULES: 1. You MUST respond with valid JSON in this exact format: {"verdict": "pass" | "fail", "reason": "brief explanation"} 2. "pass" = the output confirms the expected behavior occurred 3. "fail" = the output shows the expected behavior did NOT occur, or shows an error 4. Ignore irrelevant noise (timestamps, debug lines, unrelated warnings) 5. Focus ONLY on whether the specific expected behavior is confirmed or contradicted 6. Be strict: if the evidence is ambiguous, verdict is "fail" 7. Do NOT include any text outside the JSON object""" @dataclass class JudgmentResult: """Result of an LLM log judgment.""" verdict: str # "pass" or "fail" reason: str raw_response: str @property def passed(self) -> bool: return self.verdict == "pass" def _extract_relevant_logs(full_log: str, max_chars: int = 4000) -> str: """Extract relevant portions from logs, removing noise. Keeps the last *max_chars* characters (most recent / relevant). Removes common noise patterns. """ lines = full_log.strip().split("\n") noise_patterns = [ r"^\d{4}-\d{2}-\d{2}T", r"^DEBUG ", r"^TRACE ", ] filtered = [ line for line in lines if not any(re.match(p, line) for p in noise_patterns) ] text = "\n".join(filtered) if len(text) > max_chars: text = "...\n" + text[-max_chars:] return text def judge_log( operation: str, expected_behavior: str, actual_log: str, client: LLMClient | None = None, ) -> JudgmentResult: """Judge whether actual logs match expected behavior. Args: operation: Description of what was attempted (e.g. ``"prebake bootstrap stage"``). expected_behavior: What should have happened (e.g. ``"user vulcan was created"``). actual_log: The actual stdout / stderr output. client: ``LLMClient`` instance (creates default if ``None``). Returns: ``JudgmentResult`` with verdict and reason. """ if client is None: client = LLMClient() if not client.available: logger.warning("No LLM client available, returning fail verdict") return JudgmentResult( verdict="fail", reason="No LLM client available for judgment", raw_response="", ) relevant_log = _extract_relevant_logs(actual_log) prompt = f"""\ ## Operation {operation} ## Expected Behavior {expected_behavior} ## Actual Output (relevant excerpts) ``` {relevant_log} ``` Evaluate: Does the actual output confirm the expected behavior occurred? Respond with JSON only.""" try: response = client.complete( prompt=prompt, system_prompt=JUDGE_SYSTEM_PROMPT, max_tokens=500, ) # Parse JSON response — handle potential markdown code blocks text = response.strip() if text.startswith("```"): lines = text.split("\n") text = "\n".join(lines[1:-1]) if len(lines) > 2 else text result = json.loads(text) return JudgmentResult( verdict=result.get("verdict", "fail"), reason=result.get("reason", "No reason provided"), raw_response=response, ) except json.JSONDecodeError as e: logger.error(f"Failed to parse LLM response as JSON: {e}") return JudgmentResult( verdict="fail", reason=f"LLM response was not valid JSON: {response[:200]}", raw_response=response, ) except Exception as e: logger.error(f"LLM judgment failed: {e}") return JudgmentResult( verdict="fail", reason=f"LLM call failed: {str(e)}", raw_response="", )