feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
@@ -12,11 +9,8 @@ from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.db.base import Base
|
||||
from app.models.agent_asset import AgentAsset, AgentAssetTestRun
|
||||
from app.models.employee import Employee
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.models.golden_case import GoldenCase
|
||||
from app.services.risk_rule_golden_evaluator import (
|
||||
GoldenEvalReport,
|
||||
RiskRuleGoldenEvaluator,
|
||||
_aggregate,
|
||||
_run_single_case,
|
||||
@@ -141,19 +135,36 @@ def test_aggregate_with_failure() -> None:
|
||||
|
||||
results = [
|
||||
GoldenCaseResult("1", "a", True, True, "high", "high", True),
|
||||
GoldenCaseResult("2", "b", True, False, "high", "none", False), # FP
|
||||
GoldenCaseResult("2", "b", True, False, "high", "none", False), # FN
|
||||
]
|
||||
report = _aggregate(results)
|
||||
assert report.passed_count == 1
|
||||
assert report.failed_count == 1
|
||||
assert report.accuracy == 0.5
|
||||
assert report.all_passed is False
|
||||
assert report.precision == 0.5 # 1/(1+1)
|
||||
assert report.precision == 1.0
|
||||
assert report.recall == 0.5 # 1/(1+1)
|
||||
|
||||
|
||||
def test_aggregate_false_positive_reduces_precision_not_recall() -> None:
|
||||
from app.services.risk_rule_golden_evaluator import GoldenCaseResult
|
||||
|
||||
report = _aggregate(
|
||||
[
|
||||
GoldenCaseResult("1", "a", True, True, "high", "high", True),
|
||||
GoldenCaseResult("2", "b", False, True, "", "high", False),
|
||||
]
|
||||
)
|
||||
|
||||
assert report.precision == 0.5
|
||||
assert report.recall == 1.0
|
||||
|
||||
|
||||
def test_evaluate_for_rule_empty_returns_passed() -> None:
|
||||
with _build_session() as db:
|
||||
report = RiskRuleGoldenEvaluator().evaluate_for_rule(db, _keyword_manifest(), "risk.test.keyword")
|
||||
report = RiskRuleGoldenEvaluator().evaluate_for_rule(
|
||||
db, _keyword_manifest(), "risk.test.keyword"
|
||||
)
|
||||
assert report.total == 0
|
||||
assert report.all_passed is True
|
||||
|
||||
@@ -163,7 +174,9 @@ def test_evaluate_for_rule_all_pass() -> None:
|
||||
db.add(_golden_case("g1", reason="虚假发票", expected_hit=True))
|
||||
db.add(_golden_case("g2", reason="正常报销", expected_hit=False))
|
||||
db.commit()
|
||||
report = RiskRuleGoldenEvaluator().evaluate_for_rule(db, _keyword_manifest(), "risk.test.keyword")
|
||||
report = RiskRuleGoldenEvaluator().evaluate_for_rule(
|
||||
db, _keyword_manifest(), "risk.test.keyword"
|
||||
)
|
||||
assert report.total == 2
|
||||
assert report.all_passed is True
|
||||
assert report.accuracy == 1.0
|
||||
@@ -172,9 +185,11 @@ def test_evaluate_for_rule_all_pass() -> None:
|
||||
def test_evaluate_for_rule_with_failure() -> None:
|
||||
with _build_session() as db:
|
||||
db.add(_golden_case("g1", reason="虚假发票", expected_hit=False)) # 期望不命中但实际命中
|
||||
db.add(_golden_case("g2", reason="正常报销", expected_hit=True)) # 期望命中但实际不命中
|
||||
db.add(_golden_case("g2", reason="正常报销", expected_hit=True)) # 期望命中但实际不命中
|
||||
db.commit()
|
||||
report = RiskRuleGoldenEvaluator().evaluate_for_rule(db, _keyword_manifest(), "risk.test.keyword")
|
||||
report = RiskRuleGoldenEvaluator().evaluate_for_rule(
|
||||
db, _keyword_manifest(), "risk.test.keyword"
|
||||
)
|
||||
assert report.total == 2
|
||||
assert report.all_passed is False
|
||||
assert report.failed_count == 2
|
||||
@@ -206,6 +221,7 @@ def test_require_pass_passes_when_all_green() -> None:
|
||||
# 应写一条 test_type='golden' 记录
|
||||
run = db.query(AgentAssetTestRun).filter_by(asset_id="a1", test_type="golden").one()
|
||||
assert run.passed is True
|
||||
assert run.status == "passed"
|
||||
|
||||
|
||||
def test_require_pass_raises_on_failure() -> None:
|
||||
@@ -220,18 +236,22 @@ def test_require_pass_raises_on_failure() -> None:
|
||||
)
|
||||
run = db.query(AgentAssetTestRun).filter_by(asset_id="a2", test_type="golden").one()
|
||||
assert run.passed is False
|
||||
assert run.status == "failed"
|
||||
assert run.result_json["failure_reason"] == "golden_case_regression"
|
||||
|
||||
|
||||
def test_require_pass_empty_golden_set_passes() -> None:
|
||||
def test_require_pass_empty_golden_set_fails_closed() -> None:
|
||||
with _build_session() as db:
|
||||
asset = _asset("a3", "R3")
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
report = RiskRuleGoldenEvaluator().require_pass(
|
||||
db, asset, "v1", _keyword_manifest(), "risk.test.keyword", actor="tester"
|
||||
)
|
||||
assert report.total == 0
|
||||
assert report.all_passed is True
|
||||
with pytest.raises(PermissionError, match="缺少必要"):
|
||||
RiskRuleGoldenEvaluator().require_pass(
|
||||
db, asset, "v1", _keyword_manifest(), "risk.test.keyword", actor="tester"
|
||||
)
|
||||
run = db.query(AgentAssetTestRun).filter_by(asset_id="a3", test_type="golden").one()
|
||||
assert run.passed is False
|
||||
assert run.result_json["failure_reason"] == "missing_active_golden_cases"
|
||||
|
||||
|
||||
def test_require_pass_respects_feature_flag(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -246,17 +266,45 @@ def test_require_pass_respects_feature_flag(monkeypatch: pytest.MonkeyPatch) ->
|
||||
db, asset, "v1", _keyword_manifest(), "risk.test.keyword", actor="tester"
|
||||
)
|
||||
assert report.total == 0
|
||||
assert report.gate_status == "skipped"
|
||||
run = db.query(AgentAssetTestRun).filter_by(asset_id="a4", test_type="golden").one()
|
||||
assert run.status == "skipped"
|
||||
assert run.result_json["failure_reason"] == "gate_explicitly_disabled"
|
||||
|
||||
|
||||
def test_require_pass_swallows_evaluator_exception() -> None:
|
||||
def test_require_pass_records_and_blocks_precondition_error() -> None:
|
||||
with _build_session() as db:
|
||||
asset = _asset("a-precondition", "R-PRECONDITION")
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(PermissionError, match="前置条件失败"):
|
||||
RiskRuleGoldenEvaluator().require_pass(
|
||||
db,
|
||||
asset,
|
||||
"v1",
|
||||
{},
|
||||
"",
|
||||
actor="tester",
|
||||
precondition_error="missing_rule_document",
|
||||
)
|
||||
|
||||
run = db.query(AgentAssetTestRun).filter_by(asset_id=asset.id).one()
|
||||
assert run.status == "failed"
|
||||
assert run.result_json["failure_reason"] == "missing_rule_document"
|
||||
|
||||
|
||||
def test_require_pass_fails_closed_on_evaluator_exception() -> None:
|
||||
with _build_session() as db:
|
||||
asset = _asset("a5", "R5")
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
evaluator = RiskRuleGoldenEvaluator()
|
||||
with patch.object(evaluator, "evaluate_for_rule", side_effect=RuntimeError("boom")):
|
||||
report = evaluator.require_pass(
|
||||
db, asset, "v1", _keyword_manifest(), "risk.test.keyword", actor="tester"
|
||||
)
|
||||
assert report.total == 0
|
||||
assert report.all_passed is True # 降级放行
|
||||
with pytest.raises(PermissionError, match="fail-closed"):
|
||||
evaluator.require_pass(
|
||||
db, asset, "v1", _keyword_manifest(), "risk.test.keyword", actor="tester"
|
||||
)
|
||||
run = db.query(AgentAssetTestRun).filter_by(asset_id="a5", test_type="golden").one()
|
||||
assert run.passed is False
|
||||
assert run.result_json["failure_reason"] == "evaluation_error:RuntimeError"
|
||||
|
||||
Reference in New Issue
Block a user