"""core/evolution/diagnose.py 单元测试。

覆盖：
- 7 个规则指标（空输入、边界、典型值）
- extract_json_from_response（三策略 + 拒绝非 dict + 垃圾输入）
- attribute_error 瀑布（4 条路径）
- question_soft_score（空→None）
- aggregate_soft（跳过 None、全 None→None）
"""

from __future__ import annotations

import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock

import pytest

from core.evolution.diagnose import (
    _percentile,
    _trigrams,
    aggregate_d2,
    aggregate_d3,
    aggregate_d4,
    aggregate_d5,
    aggregate_soft,
    attribute_error,
    calc_budget_usage,
    calc_confidence_calibration,
    calc_format_compliance,
    calc_level_jump_pattern,
    calc_repeat_visit_rate,
    calc_search_keyword_repetition,
    calc_tool_usage,
    extract_json_from_response,
    extract_rule_metrics,
    merge_system_packs,
    merge_tool_packs,
    question_soft_score,
    run_diagnosis,
)
from core.evolution.types import (
    CaseSample,
    DiagnosePrompts,
    DiagnosisResult,
    QuestionMetrics,
    SkillStepAdherence,
    SpanMetrics,
    SystemCasePack,
    ToolCasePack,
)

# =========================================================================
# 工厂函数
# =========================================================================


def _make_qm(
    question_id: str = "q1",
    video_id: str = "v1",
    task_type: str = "Action Reasoning",
    correct: bool = False,
    format_compliance: float = 1.0,
    budget_usage: float = 0.5,
    confidence_calibration: str = "calibrated",
    repeat_visit_rate: float = 0.0,
    search_keyword_repetition: float = 0.0,
    level_jump_pattern: str = "",
    tool_usage: dict[str, int] | None = None,
    span_metrics: list[SpanMetrics] | None = None,
    missed_nodes: list[str] | None = None,
    skill_adherence: list[Any] | None = None,
    confirmation_bias: bool | None = None,
    evidence_sufficient: bool | None = None,
    degraded: bool = False,
) -> QuestionMetrics:
    """构造 QuestionMetrics，非关键字段使用合理默认值。"""
    return QuestionMetrics(
        question_id=question_id,
        video_id=video_id,
        task_type=task_type,
        correct=correct,
        format_compliance=format_compliance,
        budget_usage=budget_usage,
        confidence_calibration=confidence_calibration,
        repeat_visit_rate=repeat_visit_rate,
        search_keyword_repetition=search_keyword_repetition,
        level_jump_pattern=level_jump_pattern,
        tool_usage=tool_usage or {},
        span_metrics=span_metrics or [],
        missed_nodes=missed_nodes or [],
        skill_adherence=skill_adherence or [],
        confirmation_bias=confirmation_bias,
        evidence_sufficient=evidence_sufficient,
        degraded=degraded,
    )


def _make_span(
    step: int = 0,
    tool_name: str = "view_node",
    extraction_completeness: float = 0.8,
    hallucination_rate: float = 0.1,
) -> SpanMetrics:
    """构造 SpanMetrics 快捷工厂。"""
    return SpanMetrics(
        step=step,
        tool_name=tool_name,
        extraction_completeness=extraction_completeness,
        hallucination_rate=hallucination_rate,
    )


# =========================================================================
# A. 规则指标测试
# =========================================================================


class TestCalcFormatCompliance:
    """calc_format_compliance 测试。"""

    def test_empty_returns_one(self) -> None:
        """空列表返回 1.0。"""
        assert calc_format_compliance([]) == 1.0

    def test_all_compliant(self) -> None:
        """全部合规返回 1.0。"""
        raw = json.dumps({"reflect": {}, "plan": {}, "action": {}})
        assert calc_format_compliance([raw, raw]) == 1.0

    def test_none_compliant(self) -> None:
        """全部不合规返回 0.0。"""
        assert calc_format_compliance(["not json", '{"foo": 1}']) == 0.0

    def test_partial_compliance(self) -> None:
        """部分合规返回正确比例。"""
        good = json.dumps({"reflect": {}, "plan": {}, "action": {}})
        bad = json.dumps({"reflect": {}, "plan": {}})
        assert calc_format_compliance([good, bad]) == 0.5


class TestCalcBudgetUsage:
    """calc_budget_usage 测试。"""

    def test_typical(self) -> None:
        """典型值。"""
        assert calc_budget_usage(5, 10) == 0.5

    def test_full_budget(self) -> None:
        """用满预算。"""
        assert calc_budget_usage(10, 10) == 1.0

    def test_zero_steps(self) -> None:
        """未使用步数。"""
        assert calc_budget_usage(0, 10) == 0.0

    def test_zero_max_steps_raises(self) -> None:
        """max_steps=0 应抛出 ZeroDivisionError（P5: 不掩盖错误）。"""
        with pytest.raises(ZeroDivisionError):
            calc_budget_usage(5, 0)


class TestCalcConfidenceCalibration:
    """calc_confidence_calibration 测试。"""

    def test_high_conf_wrong(self) -> None:
        """高置信度答错。"""
        assert calc_confidence_calibration(0.7, correct=False) == "high_conf_wrong"
        assert calc_confidence_calibration(0.9, correct=False) == "high_conf_wrong"

    def test_low_conf_right(self) -> None:
        """低置信度答对。"""
        assert calc_confidence_calibration(0.3, correct=True) == "low_conf_right"
        assert calc_confidence_calibration(0.49, correct=True) == "low_conf_right"

    def test_calibrated(self) -> None:
        """正常校准。"""
        assert calc_confidence_calibration(0.5, correct=True) == "calibrated"
        assert calc_confidence_calibration(0.7, correct=True) == "calibrated"
        assert calc_confidence_calibration(0.3, correct=False) == "calibrated"

    def test_boundary_high(self) -> None:
        """边界值: 0.7 答错。"""
        assert calc_confidence_calibration(0.7, correct=False) == "high_conf_wrong"

    def test_boundary_low(self) -> None:
        """边界值: 0.5 答对不算 low_conf_right。"""
        assert calc_confidence_calibration(0.5, correct=True) == "calibrated"


class TestCalcRepeatVisitRate:
    """calc_repeat_visit_rate 测试。"""

    def test_empty(self) -> None:
        """空列表返回 0.0。"""
        assert calc_repeat_visit_rate([]) == 0.0

    def test_no_repeats(self) -> None:
        """无重复。"""
        assert calc_repeat_visit_rate(["a", "b", "c"]) == 0.0

    def test_all_same(self) -> None:
        """全重复。"""
        rate = calc_repeat_visit_rate(["a", "a", "a"])
        assert abs(rate - (1 - 1 / 3)) < 1e-9

    def test_partial_repeats(self) -> None:
        """部分重复。"""
        rate = calc_repeat_visit_rate(["a", "b", "a"])
        assert abs(rate - (1 - 2 / 3)) < 1e-9


class TestTrigrams:
    """_trigrams 辅助函数测试。"""

    def test_short_string(self) -> None:
        """不足 3 字符返回空集。"""
        assert _trigrams("ab") == set()
        assert _trigrams("") == set()

    def test_exact_three(self) -> None:
        """恰好 3 字符。"""
        assert _trigrams("abc") == {"abc"}

    def test_longer(self) -> None:
        """多字符。"""
        assert _trigrams("abcd") == {"abc", "bcd"}


class TestCalcSearchKeywordRepetition:
    """calc_search_keyword_repetition 测试。"""

    def test_single_query(self) -> None:
        """单条查询返回 0.0。"""
        assert calc_search_keyword_repetition(["hello"]) == 0.0

    def test_empty(self) -> None:
        """空列表返回 0.0。"""
        assert calc_search_keyword_repetition([]) == 0.0

    def test_identical_queries(self) -> None:
        """完全相同的连续查询，Jaccard=1.0。"""
        assert calc_search_keyword_repetition(["hello world", "hello world"]) == 1.0

    def test_disjoint_queries(self) -> None:
        """完全不同的查询，Jaccard 接近 0。"""
        score = calc_search_keyword_repetition(["aaa", "zzz"])
        assert score == 0.0

    def test_takes_max_across_pairs(self) -> None:
        """取连续对的最大值。"""
        score = calc_search_keyword_repetition(["aaa", "zzz", "zzz"])
        assert score == 1.0  # 第二对完全相同


class TestCalcLevelJumpPattern:
    """calc_level_jump_pattern 测试。"""

    def test_empty(self) -> None:
        """空列表返回空字符串。"""
        assert calc_level_jump_pattern([]) == ""

    def test_typical(self) -> None:
        """典型节点 ID 序列。"""
        result = calc_level_jump_pattern(["vid_L1_001", "vid_L2_003", "vid_L3_005"])
        assert result == "L1→L2→L3"

    def test_no_match(self) -> None:
        """无匹配的节点 ID 被跳过。"""
        assert calc_level_jump_pattern(["no_level_here"]) == ""

    def test_mixed(self) -> None:
        """混合匹配和非匹配。"""
        result = calc_level_jump_pattern(["vid_L2_001", "bad_id", "vid_L1_003"])
        assert result == "L2→L1"


class TestCalcToolUsage:
    """calc_tool_usage 测试。"""

    def test_empty(self) -> None:
        """空列表返回空字典。"""
        assert calc_tool_usage([]) == {}

    def test_counts(self) -> None:
        """正确计数。"""
        result = calc_tool_usage(["view_node", "search_similar", "view_node"])
        assert result == {"view_node": 2, "search_similar": 1}


class TestExtractRuleMetrics:
    """extract_rule_metrics 测试。"""

    def test_basic_extraction(self) -> None:
        """基本规则指标提取。"""
        prediction = {
            "steps_json": [
                {
                    "tool_call": {
                        "tool": "view_node",
                        "args": {"node_id": "vid_L1_001"},
                    }
                },
                {
                    "tool_call": {
                        "tool": "search_similar",
                        "args": {"query": "test query"},
                    }
                },
            ],
            "correct": True,
        }
        result = extract_rule_metrics(prediction, [], max_steps=10)
        assert result["budget_usage"] == 0.2
        assert result["tool_usage"] == {"view_node": 1, "search_similar": 1}
        assert "L1" in result["level_jump_pattern"]

    def test_empty_prediction(self) -> None:
        """空预测。"""
        result = extract_rule_metrics({}, [], max_steps=10)
        assert result["format_compliance"] == 1.0
        assert result["budget_usage"] == 0.0


# =========================================================================
# B. JSON 提取测试
# =========================================================================


class TestExtractJsonFromResponse:
    """extract_json_from_response 测试。"""

    def test_fenced_block(self) -> None:
        """从 markdown 代码块提取。"""
        raw = '```json\n{"key": "value"}\n```'
        assert extract_json_from_response(raw) == {"key": "value"}

    def test_fenced_block_no_json_tag(self) -> None:
        """从无 json 标签的代码块提取。"""
        raw = '```\n{"key": "value"}\n```'
        assert extract_json_from_response(raw) == {"key": "value"}

    def test_outermost_braces(self) -> None:
        """从最外层花括号提取。"""
        raw = 'Some text before {"result": 42} and after'
        assert extract_json_from_response(raw) == {"result": 42}

    def test_non_dict_raises(self) -> None:
        """非 dict 类型抛出 ValueError。"""
        raw = "[1, 2, 3]"
        with pytest.raises(ValueError, match="无法从 LLM 回复中提取 JSON"):
            extract_json_from_response(raw)

    def test_garbage_raises(self) -> None:
        """完全无法解析的输入抛出 ValueError。"""
        with pytest.raises(ValueError, match="无法从 LLM 回复中提取 JSON"):
            extract_json_from_response("this is not json at all !!!")

    def test_nested_braces(self) -> None:
        """嵌套花括号正确处理。"""
        inner = {"nested": {"deep": True}}
        raw = f"Result: {json.dumps(inner)}"
        assert extract_json_from_response(raw) == inner

    def test_fenced_block_non_dict_falls_through(self) -> None:
        """代码块中是列表时，回退到后续策略。"""
        raw = '```json\n[1,2,3]\n``` {"fallback": true}'
        result = extract_json_from_response(raw)
        assert result == {"fallback": True}


# =========================================================================
# C. question_soft_score / aggregate_soft 测试
# =========================================================================


class TestQuestionSoftScore:
    """question_soft_score 测试。"""

    def test_empty_returns_none(self) -> None:
        """空 span 列表返回 None。"""
        assert question_soft_score([]) is None

    def test_single_span(self) -> None:
        """单个 span 的计算。"""
        span = _make_span(extraction_completeness=0.8, hallucination_rate=0.2)
        score = question_soft_score([span])
        # (0.8 + (1.0 - 0.2)) / 2 = (0.8 + 0.8) / 2 = 0.8
        assert score is not None
        assert abs(score - 0.8) < 1e-9

    def test_multiple_spans(self) -> None:
        """多个 span 取均值。"""
        span1 = _make_span(extraction_completeness=1.0, hallucination_rate=0.0)
        span2 = _make_span(extraction_completeness=0.6, hallucination_rate=0.4)
        score = question_soft_score([span1, span2])
        # span1: (1.0 + 1.0) / 2 = 1.0
        # span2: (0.6 + 0.6) / 2 = 0.6
        # mean: (1.0 + 0.6) / 2 = 0.8
        assert score is not None
        assert abs(score - 0.8) < 1e-9

    def test_perfect_span(self) -> None:
        """完美 span。"""
        span = _make_span(extraction_completeness=1.0, hallucination_rate=0.0)
        score = question_soft_score([span])
        assert score == 1.0


class TestAggregateSoft:
    """aggregate_soft 测试。"""

    def test_all_none_returns_none(self) -> None:
        """全部 None 返回 None。"""
        assert aggregate_soft([None, None, None]) is None

    def test_empty_returns_none(self) -> None:
        """空列表返回 None。"""
        assert aggregate_soft([]) is None

    def test_skip_none(self) -> None:
        """跳过 None 计算均值。"""
        result = aggregate_soft([0.8, None, 0.6])
        assert result is not None
        assert abs(result - 0.7) < 1e-9

    def test_all_valid(self) -> None:
        """全部有效。"""
        result = aggregate_soft([0.5, 0.7, 0.9])
        assert result is not None
        assert abs(result - 0.7) < 1e-9


# =========================================================================
# D. attribute_error 瀑布测试
# =========================================================================


class TestAttributeError:
    """attribute_error 瀑布规则测试。"""

    def test_extraction_failure_low_completeness(self) -> None:
        """avg completeness < 0.5 → extraction_failure。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(extraction_completeness=0.3, hallucination_rate=0.1),
                _make_span(extraction_completeness=0.4, hallucination_rate=0.1),
            ],
            missed_nodes=["node_1"],  # 有遗漏，但 extraction 优先
        )
        result = attribute_error(qm)
        assert result.error_type == "extraction_failure"
        assert result.question_id == "q1"

    def test_extraction_failure_high_hallucination(self) -> None:
        """max hallucination > 0.5 → extraction_failure。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(extraction_completeness=0.9, hallucination_rate=0.6),
            ],
        )
        result = attribute_error(qm)
        assert result.error_type == "extraction_failure"

    def test_search_failure(self) -> None:
        """有遗漏节点 → search_failure。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(extraction_completeness=0.8, hallucination_rate=0.1),
            ],
            missed_nodes=["node_1", "node_2"],
        )
        result = attribute_error(qm)
        assert result.error_type == "search_failure"

    def test_reasoning_failure(self) -> None:
        """evidence_sufficient=True → reasoning_failure。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(extraction_completeness=0.8, hallucination_rate=0.1),
            ],
            missed_nodes=[],
            evidence_sufficient=True,
        )
        result = attribute_error(qm)
        assert result.error_type == "reasoning_failure"

    def test_mixed_evidence_none(self) -> None:
        """evidence_sufficient=None → mixed。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(extraction_completeness=0.8, hallucination_rate=0.1),
            ],
            missed_nodes=[],
            evidence_sufficient=None,
        )
        result = attribute_error(qm)
        assert result.error_type == "mixed"

    def test_mixed_evidence_false(self) -> None:
        """evidence_sufficient=False → mixed。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(extraction_completeness=0.8, hallucination_rate=0.1),
            ],
            missed_nodes=[],
            evidence_sufficient=False,
        )
        result = attribute_error(qm)
        assert result.error_type == "mixed"

    def test_no_spans_extraction(self) -> None:
        """无 span 时 avg_completeness=0 (< 0.5) → extraction_failure。"""
        qm = _make_qm(span_metrics=[], missed_nodes=["node_x"])
        result = attribute_error(qm)
        # _mean([]) = 0.0 < 0.5 → extraction_failure
        assert result.error_type == "extraction_failure"

    def test_reasoning_failure_type_is_none(self) -> None:
        """attribute_error 不设 reasoning_failure_type（由后续阶段补充）。"""
        qm = _make_qm(evidence_sufficient=True)
        result = attribute_error(qm)
        assert result.reasoning_failure_type is None


# =========================================================================
# E. D2-D5 聚合测试
# =========================================================================


class TestPercentile:
    """_percentile 辅助函数测试。"""

    def test_empty_returns_zero(self) -> None:
        """空列表返回 0.0。"""
        assert _percentile([], 0.5) == 0.0

    def test_single_element(self) -> None:
        """单元素返回该元素。"""
        assert _percentile([42.0], 0.5) == 42.0

    def test_median_two_elements(self) -> None:
        """两元素中位数。"""
        assert _percentile([1.0, 3.0], 0.5) == 2.0

    def test_quartiles(self) -> None:
        """四分位线性插值。"""
        values = [1.0, 2.0, 3.0, 4.0, 5.0]
        assert _percentile(values, 0.0) == 1.0
        assert _percentile(values, 1.0) == 5.0
        p25 = _percentile(values, 0.25)
        assert abs(p25 - 2.0) < 1e-9


class TestAggregation:
    """D2-D5 聚合函数测试。"""

    def test_d2_empty(self) -> None:
        """空输入返回空字典。"""
        assert aggregate_d2([]) == {}

    def test_d2_groups_by_tool(self) -> None:
        """按工具名分组聚合。"""
        qm = _make_qm(
            span_metrics=[
                _make_span(
                    step=0,
                    tool_name="view_node",
                    extraction_completeness=0.8,
                    hallucination_rate=0.2,
                ),
                _make_span(
                    step=1,
                    tool_name="view_node",
                    extraction_completeness=0.6,
                    hallucination_rate=0.1,
                ),
                _make_span(
                    step=2,
                    tool_name="search_similar",
                    extraction_completeness=0.9,
                    hallucination_rate=0.0,
                ),
            ]
        )
        result = aggregate_d2([qm])
        assert "view_node" in result
        assert "search_similar" in result
        assert result["view_node"]["n_calls"] == 2
        assert result["search_similar"]["n_calls"] == 1
        assert abs(result["view_node"]["avg_completeness"] - 0.7) < 1e-9

    def test_d3_empty(self) -> None:
        """空输入返回空字典。"""
        assert aggregate_d3([]) == {}

    def test_d3_correct_vs_incorrect(self) -> None:
        """按正误拆分。"""
        qm_correct = _make_qm(correct=True, task_type="T1", budget_usage=0.5)
        qm_wrong = _make_qm(correct=False, task_type="T1", budget_usage=0.8)
        result = aggregate_d3([qm_correct, qm_wrong])
        assert "T1" in result
        assert result["T1"]["correct"]["n_questions"] == 1
        assert result["T1"]["incorrect"]["n_questions"] == 1
        # avg_steps 存储 budget_usage 均值
        assert result["T1"]["correct"]["avg_steps"] == 0.5
        assert result["T1"]["incorrect"]["avg_steps"] == 0.8

    def test_d4_empty(self) -> None:
        """空输入返回空字典。"""
        assert aggregate_d4([]) == {}

    def test_d4_adherence_rate(self) -> None:
        """技能遵循率计算。"""
        qm = _make_qm(
            task_type="T1",
            correct=True,
            skill_adherence=[
                SkillStepAdherence(step_label="S1", adhered=True, description=""),
                SkillStepAdherence(step_label="S1", adhered=False, description=""),
            ],
        )
        result = aggregate_d4([qm])
        assert "T1" in result
        assert result["T1"]["overall_adherence"] == 0.5

    def test_d5_empty_returns_zero_structure(self) -> None:
        """空输入返回完整零结构。"""
        result = aggregate_d5([])
        assert "early_submit_rate" in result
        assert result["early_submit_rate"] == 0.0
        assert "format_compliance_rate" in result
        assert "budget_usage_median" in result
        assert "confirmation_bias_rate" in result
        assert "per_type_bias" in result
        assert result["per_type_bias"] == {}

    def test_d5_with_data(self) -> None:
        """有数据时正确计算。"""
        qm1 = _make_qm(
            correct=True,
            budget_usage=0.5,
            format_compliance=1.0,
            confidence_calibration="calibrated",
            confirmation_bias=False,
        )
        qm2 = _make_qm(
            correct=False,
            budget_usage=0.2,
            format_compliance=0.8,
            confidence_calibration="high_conf_wrong",
            confirmation_bias=True,
        )
        result = aggregate_d5([qm1, qm2])
        assert result["format_compliance_rate"] == 0.9
        assert result["high_conf_wrong_rate"] == 0.5
        assert result["early_submit_rate"] == 1.0  # 1 wrong with budget<0.3


# =========================================================================
# F. Merge 函数测试
# =========================================================================


class TestMerge:
    """merge_system_packs / merge_tool_packs 测试。"""

    def test_merge_system_packs_none_on_empty(self) -> None:
        """空列表返回 None。"""
        assert merge_system_packs([]) is None

    def test_merge_system_packs_wraps_stats(self) -> None:
        """stats 包裹为 per_step 列表。"""
        pack = SystemCasePack(stats={"a": 1}, failure_cases=[], success_cases=[])
        merged = merge_system_packs([pack, pack])
        assert merged is not None
        assert "per_step" in merged.stats
        assert len(merged.stats["per_step"]) == 2

    def test_merge_system_packs_concats_cases(self) -> None:
        """failure/success cases 拼接。"""
        case = CaseSample(
            question_id="q1",
            video_id="v1",
            task_type="T1",
            question="q",
            options=[],
            answer="a",
            prediction="b",
            correct=False,
            error_type="mixed",
            selection_reason="test",
            metrics={},
            trace=[],
        )
        p1 = SystemCasePack(stats={}, failure_cases=[case], success_cases=[])
        p2 = SystemCasePack(stats={}, failure_cases=[case], success_cases=[case])
        merged = merge_system_packs([p1, p2])
        assert merged is not None
        assert len(merged.failure_cases) == 2
        assert len(merged.success_cases) == 1

    def test_merge_tool_packs_empty(self) -> None:
        """空列表返回空字典。"""
        assert merge_tool_packs([]) == {}

    def test_merge_tool_packs_groups_by_name(self) -> None:
        """同名工具合并。"""
        p1 = ToolCasePack(
            tool_name="view_node",
            target_files=["f1.md"],
            stats={"x": 1},
            failure_spans=[{"a": 1}],
            success_spans=[],
        )
        p2 = ToolCasePack(
            tool_name="view_node",
            target_files=["f1.md"],
            stats={"x": 2},
            failure_spans=[{"b": 2}],
            success_spans=[{"c": 3}],
        )
        merged = merge_tool_packs([p1, p2])
        assert "view_node" in merged
        vn = merged["view_node"]
        assert len(vn.failure_spans) == 2
        assert len(vn.success_spans) == 1
        assert "per_step" in vn.stats


# =========================================================================
# G. run_diagnosis 入口测试
# =========================================================================


class TestRunDiagnosis:
    """run_diagnosis 入口测试。"""

    def test_empty_predictions_returns_empty_result(self) -> None:
        """无预测时返回空 DiagnosisResult。"""
        import asyncio

        mock_log = AsyncMock()
        mock_log.get_predictions.return_value = []
        mock_log.get_traces.return_value = []
        mock_llm = AsyncMock()
        mock_store = MagicMock()
        mock_store.list_skill_files.return_value = []
        prompts = DiagnosePrompts(
            defect_vs_lapse="",
            reasoning_sub="",
            span_eval_system="",
            span_eval_user="",
            missed_nodes="",
            skill_adherence="",
            confirmation_bias="",
            evidence_sufficiency="",
        )
        result = asyncio.run(
            run_diagnosis(
                "run1",
                [],
                {},
                mock_llm,
                mock_log,
                mock_store,
                prompts,
                concurrency=1,
            )
        )
        assert isinstance(result, DiagnosisResult)
        assert result.run_id == "run1"
        assert result.error_attributions == []
        assert result.degraded_count == 0
