diff --git a/tests/test_advisor_comparison_models.py b/tests/test_advisor_comparison_models.py new file mode 100644 index 0000000..e548a54 --- /dev/null +++ b/tests/test_advisor_comparison_models.py @@ -0,0 +1,50 @@ +"""Tests for the AdvisorComparison Pydantic models and their default wiring.""" +from __future__ import annotations + +from webapp.models import AdvisorComparison, AdvisorComparisonEntry, ReportData + + +def test_advisor_comparison_entry_accepts_all_statuses(): + for status in ("resolved", "regressed", "still_triggered", "new_metric"): + entry = AdvisorComparisonEntry( + metric="faithfulness", + status=status, + previous_score=0.5, + previous_severity="warning", + current_score=0.9, + current_severity=None, + ) + assert entry.status == status + + +def test_advisor_comparison_entry_rejects_unknown_status(): + import pydantic + + try: + AdvisorComparisonEntry(metric="faithfulness", status="unknown_status") + raised = False + except pydantic.ValidationError: + raised = True + assert raised + + +def test_advisor_comparison_defaults(): + comparison = AdvisorComparison( + previous_run_id="run-1", + previous_finished_at="2026-01-01T00:00:00+00:00", + ) + assert comparison.previous_judge_model == "" + assert comparison.current_judge_model == "" + assert comparison.judge_model_changed is False + assert comparison.entries == [] + + +def test_report_data_defaults_advisor_comparison_to_none(): + report = ReportData() + assert report.advisor_comparison is None + + +def test_report_data_accepts_advisor_comparison(): + comparison = AdvisorComparison(previous_run_id="run-1", previous_finished_at="t") + report = ReportData(advisor_comparison=comparison) + assert report.advisor_comparison == comparison diff --git a/webapp/models.py b/webapp/models.py index fe25208..4982af4 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import Any +from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -77,6 +77,28 @@ class SampleScore(BaseModel): ) +class AdvisorComparisonEntry(BaseModel): + """One metric's diagnosis delta between the current run and its predecessor.""" + + metric: str + status: Literal["resolved", "regressed", "still_triggered", "new_metric"] + previous_score: float | None = None + previous_severity: str | None = None + current_score: float | None = None + current_severity: str | None = None + + +class AdvisorComparison(BaseModel): + """Advisor-diagnosis comparison against the same scenario's previous run.""" + + previous_run_id: str + previous_finished_at: str + previous_judge_model: str = "" + current_judge_model: str = "" + judge_model_changed: bool = False + entries: list[AdvisorComparisonEntry] = Field(default_factory=list) + + class ReportData(BaseModel): """Aggregated report payload rendered by the report detail page.""" @@ -103,6 +125,10 @@ class ReportData(BaseModel): default_factory=dict, description="按模型累计的 token 用量:{model: {input_tokens, output_tokens, calls}}。", ) + advisor_comparison: AdvisorComparison | None = Field( + default=None, + description="相比同场景上一次运行的顾问诊断差异;无可比对象时为 None。", + ) class RunDetail(BaseModel):