"""Tests for capture mode formatters.""" from __future__ import annotations import json from typing import TYPE_CHECKING from unittest.mock import MagicMock import pytest from arcade_cli.formatters import ( CAPTURE_FORMATTERS, CaptureHtmlFormatter, CaptureJsonFormatter, CaptureMarkdownFormatter, CaptureTextFormatter, get_capture_formatter, ) if TYPE_CHECKING: from arcade_evals import CaptureResult def _create_mock_capture_result( suite_name: str = "TestSuite", model: str = "gpt-4o", provider: str = "openai", cases: list[dict] | None = None, ) -> CaptureResult: """Create a mock CaptureResult for testing.""" if cases is None: cases = [ { "case_name": "test_case_1", "user_message": "What's the weather?", "tool_calls": [ {"name": "GetWeather", "args": {"city": "NYC", "units": "celsius"}}, ], "system_message": "You are helpful", "additional_messages": [{"role": "user", "content": "Hi"}], } ] # Create mock capture result capture = MagicMock() capture.suite_name = suite_name capture.model = model capture.provider = provider # Create mock captured cases captured_cases = [] for case_data in cases: case = MagicMock() case.case_name = case_data["case_name"] case.user_message = case_data["user_message"] case.system_message = case_data.get("system_message") case.additional_messages = case_data.get("additional_messages", []) # Explicitly set track_name to None unless specified (avoids MagicMock) case.track_name = case_data.get("track_name") # Create mock runs if provided runs = [] for run_data in case_data.get("runs", []): run = MagicMock() run_tool_calls = [] for tc_data in run_data.get("tool_calls", []): tc = MagicMock() tc.name = tc_data["name"] tc.args = tc_data.get("args", {}) run_tool_calls.append(tc) run.tool_calls = run_tool_calls runs.append(run) case.runs = runs # Create mock tool calls tool_calls = [] for tc_data in case_data.get("tool_calls", []): tc = MagicMock() tc.name = tc_data["name"] tc.args = tc_data.get("args", {}) tool_calls.append(tc) case.tool_calls = tool_calls captured_cases.append(case) capture.captured_cases = captured_cases # Mock to_dict method def to_dict(include_context: bool = False) -> dict: result = { "suite_name": capture.suite_name, "model": capture.model, "provider": capture.provider, "captured_cases": [], } for case in captured_cases: case_dict = { "case_name": case.case_name, "user_message": case.user_message, "tool_calls": [{"name": tc.name, "args": tc.args} for tc in case.tool_calls], } if case.runs: case_dict["runs"] = [ {"tool_calls": [{"name": tc.name, "args": tc.args} for tc in run.tool_calls]} for run in case.runs ] if include_context: case_dict["system_message"] = case.system_message case_dict["additional_messages"] = case.additional_messages result["captured_cases"].append(case_dict) return result capture.to_dict = to_dict return capture class TestGetCaptureFormatter: """Tests for get_capture_formatter function.""" def test_get_json_formatter(self) -> None: """Test getting JSON formatter.""" formatter = get_capture_formatter("json") assert isinstance(formatter, CaptureJsonFormatter) def test_get_txt_formatter(self) -> None: """Test getting text formatter.""" formatter = get_capture_formatter("txt") assert isinstance(formatter, CaptureTextFormatter) def test_get_md_formatter(self) -> None: """Test getting markdown formatter.""" formatter = get_capture_formatter("md") assert isinstance(formatter, CaptureMarkdownFormatter) def test_get_html_formatter(self) -> None: """Test getting HTML formatter.""" formatter = get_capture_formatter("html") assert isinstance(formatter, CaptureHtmlFormatter) def test_case_insensitive(self) -> None: """Test that format names are case insensitive.""" assert isinstance(get_capture_formatter("JSON"), CaptureJsonFormatter) assert isinstance(get_capture_formatter("TXT"), CaptureTextFormatter) assert isinstance(get_capture_formatter("MD"), CaptureMarkdownFormatter) assert isinstance(get_capture_formatter("HTML"), CaptureHtmlFormatter) def test_unsupported_format_raises(self) -> None: """Test that unsupported formats raise ValueError.""" with pytest.raises(ValueError, match="Unsupported capture format 'xlsx'"): get_capture_formatter("xlsx") def test_close_match_suggestion(self) -> None: """Test that close matches are suggested.""" with pytest.raises(ValueError, match="Did you mean 'json'"): get_capture_formatter("jsn") class TestCaptureJsonFormatter: """Tests for CaptureJsonFormatter.""" def test_file_extension(self) -> None: """Test file extension is json.""" formatter = CaptureJsonFormatter() assert formatter.file_extension == "json" def test_format_basic(self) -> None: """Test basic JSON formatting.""" formatter = CaptureJsonFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) parsed = json.loads(output) assert "captures" in parsed assert len(parsed["captures"]) == 1 assert parsed["captures"][0]["suite_name"] == "TestSuite" assert parsed["captures"][0]["model"] == "gpt-4o" def test_format_includes_tool_calls(self) -> None: """Test that tool calls are included.""" formatter = CaptureJsonFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) parsed = json.loads(output) case = parsed["captures"][0]["captured_cases"][0] assert len(case["tool_calls"]) == 1 assert case["tool_calls"][0]["name"] == "GetWeather" assert case["tool_calls"][0]["args"]["city"] == "NYC" def test_format_includes_runs(self) -> None: """Test that runs are included when present.""" formatter = CaptureJsonFormatter() capture = _create_mock_capture_result( cases=[ { "case_name": "multi_run_case", "user_message": "Hello", "tool_calls": [], "runs": [ {"tool_calls": [{"name": "A", "args": {"x": 1}}]}, {"tool_calls": [{"name": "B", "args": {"x": 2}}]}, ], } ] ) output = formatter.format([capture]) parsed = json.loads(output) runs = parsed["captures"][0]["captured_cases"][0]["runs"] assert len(runs) == 2 assert runs[0]["tool_calls"][0]["name"] == "A" def test_format_with_context(self) -> None: """Test formatting with context included.""" formatter = CaptureJsonFormatter() capture = _create_mock_capture_result() output = formatter.format([capture], include_context=True) parsed = json.loads(output) case = parsed["captures"][0]["captured_cases"][0] assert "system_message" in case assert case["system_message"] == "You are helpful" def test_format_without_context(self) -> None: """Test formatting without context (default).""" formatter = CaptureJsonFormatter() capture = _create_mock_capture_result() output = formatter.format([capture], include_context=False) parsed = json.loads(output) case = parsed["captures"][0]["captured_cases"][0] assert "system_message" not in case class TestCaptureTextFormatter: """Tests for CaptureTextFormatter.""" def test_file_extension(self) -> None: """Test file extension is txt.""" formatter = CaptureTextFormatter() assert formatter.file_extension == "txt" def test_format_contains_suite_info(self) -> None: """Test that suite info is in output.""" formatter = CaptureTextFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "Suite: TestSuite" in output assert "Model: gpt-4o" in output assert "Provider: openai" in output def test_format_contains_case_info(self) -> None: """Test that case info is in output.""" formatter = CaptureTextFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "Case: test_case_1" in output assert "User Message: What's the weather?" in output def test_format_contains_tool_calls(self) -> None: """Test that tool calls are in output.""" formatter = CaptureTextFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "GetWeather" in output assert "city: NYC" in output def test_format_contains_summary(self) -> None: """Test that summary is in output.""" formatter = CaptureTextFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "Summary: 1 tool calls across 1 cases" in output def test_format_with_context(self) -> None: """Test formatting with context.""" formatter = CaptureTextFormatter() capture = _create_mock_capture_result() output = formatter.format([capture], include_context=True) assert "System Message: You are helpful" in output class TestCaptureMarkdownFormatter: """Tests for CaptureMarkdownFormatter.""" def test_file_extension(self) -> None: """Test file extension is md.""" formatter = CaptureMarkdownFormatter() assert formatter.file_extension == "md" def test_format_has_heading(self) -> None: """Test that markdown has main heading.""" formatter = CaptureMarkdownFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "# Capture Results" in output def test_format_has_suite_heading(self) -> None: """Test that suite has heading.""" formatter = CaptureMarkdownFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "## TestSuite" in output def test_format_has_case_heading(self) -> None: """Test that case has heading.""" formatter = CaptureMarkdownFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "### Case: test_case_1" in output def test_format_has_code_blocks(self) -> None: """Test that tool args are in code blocks.""" formatter = CaptureMarkdownFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "```json" in output assert '"city": "NYC"' in output assert "```" in output def test_format_has_summary(self) -> None: """Test that summary is present.""" formatter = CaptureMarkdownFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "## Summary" in output assert "**Total Cases:** 1" in output assert "**Total Tool Calls:** 1" in output def test_format_includes_runs(self) -> None: """Should include per-run tool calls when runs are present.""" formatter = CaptureMarkdownFormatter() capture = _create_mock_capture_result( cases=[ { "case_name": "multi_run_case", "user_message": "Hello", "tool_calls": [], "runs": [ {"tool_calls": [{"name": "GetWeather", "args": {"city": "NYC"}}]}, {"tool_calls": [{"name": "GetWeather", "args": {"city": "SF"}}]}, ], } ] ) output = formatter.format([capture]) assert "Run 1" in output assert "Run 2" in output assert "`GetWeather`" in output class TestCaptureHtmlFormatter: """Tests for CaptureHtmlFormatter.""" def test_file_extension(self) -> None: """Test file extension is html.""" formatter = CaptureHtmlFormatter() assert formatter.file_extension == "html" def test_format_is_valid_html(self) -> None: """Test that output is valid HTML structure.""" formatter = CaptureHtmlFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "" in output assert "" in output assert "
" in output assert "" in output assert "" in output assert "" in output def test_format_contains_styles(self) -> None: """Test that CSS styles are included.""" formatter = CaptureHtmlFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "" in output def test_format_contains_suite_info(self) -> None: """Test that suite info is in output.""" formatter = CaptureHtmlFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "TestSuite" in output assert "gpt-4o" in output def test_format_contains_tool_calls(self) -> None: """Test that tool calls are in output.""" formatter = CaptureHtmlFormatter() capture = _create_mock_capture_result() output = formatter.format([capture]) assert "GetWeather" in output # Args should be HTML-escaped assert ""city"" in output or '"city"' in output def test_format_escapes_html(self) -> None: """Test that HTML special characters are escaped.""" formatter = CaptureHtmlFormatter() capture = _create_mock_capture_result( cases=[ { "case_name": "Test