from __future__ import annotations import json from collections import defaultdict from datetime import datetime from pathlib import Path from tests.pipeline_setup_v4.core.models import V4CaseResult class ArtifactLayout: def __init__(self, *, run_name: str, started_at: datetime) -> None: self._run_name = run_name self._stamp = started_at.strftime("%Y%m%d_%H%M%S") def run_dir_for(self, source_file: Path) -> Path: path = source_file.parent / "test_runs" / self._run_name / self._stamp path.mkdir(parents=True, exist_ok=True) return path class ArtifactWriter: def __init__(self, layout: ArtifactLayout) -> None: self._layout = layout def write_case(self, result: V4CaseResult) -> Path: run_dir = self._layout.run_dir_for(result.case.source_file) stem = f"{result.case.source_file.stem}_{result.case.case_id}" json_path = run_dir / f"{stem}.json" md_path = run_dir / f"{stem}.md" json_path.write_text(json.dumps(self._json_payload(result), ensure_ascii=False, indent=2), encoding="utf-8") md_path.write_text(self._markdown_payload(result), encoding="utf-8") return md_path def write_summaries(self, results: list[V4CaseResult]) -> list[Path]: grouped: dict[Path, list[V4CaseResult]] = defaultdict(list) for result in results: grouped[self._layout.run_dir_for(result.case.source_file)].append(result) paths: list[Path] = [] for run_dir, items in sorted(grouped.items(), key=lambda item: item[0].as_posix()): path = run_dir / "summary.md" path.write_text(SummaryComposer().compose(items), encoding="utf-8") paths.append(path) return paths def _json_payload(self, result: V4CaseResult) -> dict: return { "meta": { "case_id": result.case.case_id, "component": result.case.component, "source_file": result.case.source_file.as_posix(), "passed": result.passed, "mismatches": result.mismatches, }, "actual": result.actual, "details": result.details, } def _markdown_payload(self, result: V4CaseResult) -> str: lines = [ f"# {result.case.case_id}", "", f"- component: {result.case.component}", f"- source_file: {result.case.source_file.as_posix()}", f"- passed: {result.passed}", "", "## Input", result.case.display_input, "", "## Actual", "```json", json.dumps(result.actual, ensure_ascii=False, indent=2), "```", "", "## Details", "```json", json.dumps(result.details, ensure_ascii=False, indent=2), "```", "", "## Mismatches", *([f"- {item}" for item in result.mismatches] or ["- none"]), ] return "\n".join(lines) class SummaryComposer: def compose(self, results: list[V4CaseResult]) -> str: passed = sum(1 for item in results if item.passed) lines = [ "# pipeline_setup_v4 summary", "", f"Passed: {passed}/{len(results)}", "", "| Case | Component | Query | Intent | Sub-intent | Pass |", "|------|-----------|-------|--------|------------|------|", ] for item in results: lines.append( f"| {item.case.case_id} | {item.case.component} | {self._cell(item.case.display_input)} | " f"{item.actual.get('intent') or '—'} | {item.actual.get('sub_intent') or '—'} | " f"{'✓' if item.passed else '✗'} |" ) failures = [item for item in results if not item.passed] if failures: lines.extend(["", "## Failures"]) for item in failures: lines.append(f"- **{item.case.case_id}**: {'; '.join(item.mismatches)}") return "\n".join(lines) def _cell(self, text: str, limit: int = 140) -> str: compact = " ".join(text.split()).replace("|", "\\|") if len(compact) <= limit: return compact return compact[: limit - 1].rstrip() + "…"