项目文件夹

文件
wehub-resource-sync a2cb1f9821
CI / test (push) Has been cancelled
Wiki / validate (push) Has been cancelled
Deploy Wiki / Deploy to Cloudflare Pages (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:55 +08:00

226 行
7.8 KiB
Python

"""Tests for the ``report_audit`` agent tool.
Covers the markdown data-point extraction (tables + ``label: value`` lines,
Chinese units), sampling (clamp + reproducibility), the verdict logic across
single/two-source cases (incl. the split-source WARN and the single-source
FAIL that the original upstream logic mishandled), the tool's JSON-Schema
contract, ``execute`` happy/error paths, and auto-discovery.
"""
from __future__ import annotations
import json
from typing import Any
import pytest
from src.tools import build_registry
from src.tools.report_audit_tool import (
ReportAuditTool,
_clean_num,
_is_valid_label,
_pct_diff,
extract_data_points,
render_verdict,
sample_points,
)
# ── helpers ───────────────────────────────────────────────────────────────
def test_clean_num_handles_wide_comma() -> None:
assert _clean_num("1,234") == 1234.0
assert _clean_num("1,234") == 1234.0 # wide (full-width) comma
assert _clean_num("abc") is None
def test_is_valid_label_filters_noise() -> None:
assert _is_valid_label("营业收入") is True
assert _is_valid_label("来源") is False # skip-listed
assert _is_valid_label("a") is False # too short
assert _is_valid_label("2024") is False # year only
assert _is_valid_label("+56%") is False # bare growth rate
# ── extract_data_points ───────────────────────────────────────────────────
_REPORT_MD = (
"收入:7518亿元\n"
"毛利率:56%\n"
"\n"
"| 指标 | 2024 | 2023 |\n"
"|------|------|------|\n"
"| 营业收入 | 7518亿 | 6500亿 |\n"
"| 净利润 | 1900亿 | 1600亿 |\n"
)
def test_extract_finds_table_and_kv_points() -> None:
points = extract_data_points(_REPORT_MD)
labels = {p["label"] for p in points}
assert "收入" in labels # KV line
assert "毛利率" in labels # KV line
assert "营业收入 · 2024" in labels # table cell
assert "净利润 · 2024" in labels
for p in points:
assert {"id", "label", "reported_value", "unit", "line_number"} <= set(p)
def test_extract_assigns_unique_ids() -> None:
ids = [p["id"] for p in extract_data_points(_REPORT_MD)]
assert len(ids) == len(set(ids))
# ── sample_points ─────────────────────────────────────────────────────────
def _pts(n: int) -> list[dict[str, Any]]:
return [
{"id": i, "label": f"l{i}", "reported_value": float(i), "unit": "",
"line_number": i, "raw_text": ""}
for i in range(n)
]
def test_sample_returns_all_when_fewer_than_three() -> None:
assert len(sample_points(_pts(2), ratio=0.5)) == 2
def test_sample_clamps_to_max_thirty() -> None:
assert len(sample_points(_pts(500), ratio=1.0)) == 30
def test_sample_is_reproducible_with_seed() -> None:
a = sample_points(_pts(20), ratio=0.5, seed=7)
b = sample_points(_pts(20), ratio=0.5, seed=7)
assert [p["id"] for p in a] == [p["id"] for p in b]
# ── _pct_diff ─────────────────────────────────────────────────────────────
def test_pct_diff() -> None:
assert _pct_diff(100, 101) == 0.01
assert _pct_diff(100, 100) == 0.0
assert _pct_diff(0, 0) == 0.0
assert _pct_diff(0, 5) == float("inf")
# ── render_verdict ────────────────────────────────────────────────────────
def test_verdict_single_source_pass() -> None:
out = render_verdict([{
"id": 1, "label": "rev", "reported_value": 100, "unit": "",
"fetched_value": 100.5, "fetched_source": "m",
}])
assert out["verdict"] == "PASS"
assert out["pass_count"] == 1 and out["fail_count"] == 0
def test_verdict_single_source_fail_fails_report() -> None:
# Regression: a single-source failure must FAIL, not silently WARN.
out = render_verdict([{
"id": 1, "label": "rev", "reported_value": 100, "unit": "",
"fetched_value": 150, "fetched_source": "m",
}])
assert out["verdict"] == "FAIL"
assert out["fail_count"] == 1
assert out["fail_items"][0]["label"] == "rev"
def test_verdict_two_sources_both_pass() -> None:
out = render_verdict([{
"id": 1, "label": "rev", "reported_value": 100, "unit": "",
"fetched_value": 100.5, "fetched_source": "m",
"fetched_value2": 99.8, "fetched_source2": "s",
}])
assert out["verdict"] == "PASS"
assert out["pass_count"] == 1
def test_verdict_two_sources_both_fail() -> None:
out = render_verdict([{
"id": 1, "label": "rev", "reported_value": 100, "unit": "",
"fetched_value": 150, "fetched_source": "m",
"fetched_value2": 200, "fetched_source2": "s",
}])
assert out["verdict"] == "FAIL"
assert out["fail_count"] == 1
def test_verdict_two_sources_split_is_warn_not_fail() -> None:
# One source agrees, one misses -> caliber mismatch, not a hard fail.
out = render_verdict([{
"id": 1, "label": "rev", "reported_value": 100, "unit": "",
"fetched_value": 100.5, "fetched_source": "m",
"fetched_value2": 150, "fetched_source2": "s",
}])
assert out["verdict"] == "PASS"
assert out["warn_count"] == 1 and out["fail_count"] == 0
def test_verdict_skips_points_without_fetched_value() -> None:
out = render_verdict([
{"id": 1, "label": "a", "reported_value": 100, "fetched_value": None},
{"id": 2, "label": "b", "reported_value": 100,
"fetched_value": 100, "fetched_source": "m"},
])
assert out["total"] == 1 # only the verified point counts
assert out["verdict"] == "PASS"
# ── tool contract ─────────────────────────────────────────────────────────
def test_tool_metadata() -> None:
tool = ReportAuditTool()
assert tool.name == "report_audit"
assert tool.is_readonly is True
assert tool.repeatable is True
assert tool.parameters["required"] == ["command"]
assert set(tool.parameters["properties"]["command"]["enum"]) == {"extract", "verdict"}
def test_tool_is_auto_discovered() -> None:
assert "report_audit" in build_registry().tool_names
# ── execute ───────────────────────────────────────────────────────────────
def _run(**kwargs: Any) -> dict[str, Any]:
return json.loads(ReportAuditTool().execute(**kwargs))
def test_execute_extract_happy() -> None:
env = _run(command="extract", report_text=_REPORT_MD, ratio=0.5, seed=42)
assert env["status"] == "ok"
assert env["total_extracted"] >= 4
assert env["sample_size"] >= 1
assert "sample" in env and "hint" in env
def test_execute_verdict_fail() -> None:
env = _run(command="verdict", results=[{
"id": 1, "label": "rev", "reported_value": 100,
"fetched_value": 150, "fetched_source": "m",
}])
assert env["status"] == "ok"
assert env["verdict"] == "FAIL"
def test_execute_missing_report_text_is_error() -> None:
assert _run(command="extract")["status"] == "error"
def test_execute_missing_results_is_error() -> None:
assert _run(command="verdict")["status"] == "error"
def test_execute_unknown_command_is_error() -> None:
env = _run(command="bogus")
assert env["status"] == "error"
assert "unknown command" in env["error"]