aps-agent/tests/golden/test_analytics.py

146 lines
5.9 KiB
Python

# ============================================================
# EX-07 / EX-08 分析视图黄金测试
# ============================================================
from __future__ import annotations
from server.agent_core.intent import parse_fast
from server.aps_domain.analytics import (
build_compare_table,
build_kpi_dashboard,
matrix_from_flex_rows,
)
from server.aps_domain.flex import run_flex_schedule
from server.engines import get_engine
from server.engines.base import EngineParams
from server.state.seed import seed_world
from server.timeutil import add_minutes, fmt_date, today0
class _MemStore:
def __init__(self, data):
self.data = data
def next_id(self, kind: str) -> int:
key = f"_c_{kind}"
self.data[key] = self.data.get(key, 5000) + 1
return self.data[key]
def save(self):
pass
def _run_fixed(world):
start = fmt_date(add_minutes(today0(), 24 * 60))
params = EngineParams(orderIds=[], engineType="RULE", strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14, startDate=start)
counters: dict[str, int] = {}
def next_id(kind: str) -> int:
counters[kind] = counters.get(kind, 0) + 1
return counters[kind]
get_engine("RULE").solve(world, params, next_id)
def test_kpi_dashboard_aggregates():
store = _MemStore(seed_world())
_run_fixed(store.data)
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test")
dash = build_kpi_dashboard(store.data)
assert len(dash["cards"]) >= 5
assert dash["fixed"] and dash["fixed"].get("versionNo")
assert dash["flex"] and dash["flex"].get("versionNo")
assert "total" in dash["conflicts"]
def test_compare_table_has_best_and_deltas():
store = _MemStore(seed_world())
table = build_compare_table(store.data, track="flex")
assert table["sections"]
sec = table["sections"][0]
assert len(sec["columns"]) == 5
assert {column["sortMode"] for column in sec["columns"]} == {
"ASC", "DESC", "BOTTLENECK", "KITTING_FIRST", "SKILL_FIRST",
}
assert sec["recommendation"]
row = next(r for r in sec["rows"] if r["metric"] == "totalTardiness")
assert row["bestIdx"] is not None
assert row["deltas"][row["bestIdx"]] in (0, 0.0)
def test_intent_analytics_phrases():
world = seed_world()
assert parse_fast("打开KPI仪表盘", world).intent == "query.kpi"
assert parse_fast("方案对比表", world).intent == "scenario.compare"
def test_flex_matrix_excludes_degraded_best_kpi_and_marks_trial_only():
rows = [
{"sortMode": "SKILL_FIRST", "label": "技能优先", "strategyStatus": "DEGRADED",
"strategyEvidence": [{"code": "NO_SKILL_LEVEL_VARIATION"}],
"totalTardiness": 0, "conflictCount": 0, "avgUtilization": 90,
"onTimeCount": 10, "orderCount": 10, "vlCount": 10},
{"sortMode": "ASC", "label": "正排", "strategyStatus": "READY",
"totalTardiness": 12, "conflictCount": 1, "avgUtilization": 80,
"onTimeCount": 9, "orderCount": 10, "vlCount": 10},
]
matrix = matrix_from_flex_rows(
rows,
data_quality={
"productionReady": False,
"warnings": [{"message": "设备能力仅覆盖 1/41 台设备"}],
},
)
assert len(matrix["columns"]) == 2
assert matrix["columns"][0]["strategyStatus"] == "DEGRADED"
assert matrix["recommendation"] == "正排"
assert matrix["recommendationId"] == "ASC"
assert matrix["eligibleRecommendationIds"] == ["ASC"]
assert matrix["productionReady"] is False
assert matrix["recommendationStatus"] == "TRIAL_ONLY"
assert "数据待补" in matrix["recommendationReason"]
def test_flex_matrix_without_ready_strategy_is_unavailable():
rows = [
{"sortMode": "KITTING_FIRST", "label": "齐套优先", "strategyStatus": "PARTIAL",
"totalTardiness": 5, "conflictCount": 0, "avgUtilization": 80,
"onTimeCount": 9, "orderCount": 10, "vlCount": 10},
{"sortMode": "SKILL_FIRST", "label": "技能优先", "strategyStatus": "DEGRADED",
"totalTardiness": 0, "conflictCount": 0, "avgUtilization": 90,
"onTimeCount": 10, "orderCount": 10, "vlCount": 10},
]
matrix = matrix_from_flex_rows(rows, data_quality={"productionReady": False})
assert matrix["recommendationStatus"] == "UNAVAILABLE"
assert matrix["recommendation"] is None
assert matrix["recommendationId"] is None
assert matrix["eligibleRecommendationIds"] == []
assert "没有 strategyStatus=READY" in matrix["recommendationReason"]
def test_flex_matrix_excludes_partial_schedule_result_even_when_strategy_is_ready():
rows = [
{"sortMode": "ASC", "label": "正排", "strategyStatus": "READY",
"resultStatus": "PARTIAL", "totalTardiness": 0, "conflictCount": 0,
"avgUtilization": 95, "onTimeCount": 10, "orderCount": 10, "vlCount": 5},
{"sortMode": "DESC", "label": "倒排", "strategyStatus": "READY",
"resultStatus": "WITH_CONFLICTS", "totalTardiness": 10, "conflictCount": 2,
"avgUtilization": 80, "onTimeCount": 8, "orderCount": 10, "vlCount": 10},
]
matrix = matrix_from_flex_rows(rows, data_quality={"productionReady": False})
assert matrix["recommendationId"] == "DESC"
assert matrix["eligibleRecommendationIds"] == ["DESC"]
assert matrix["recommendationStatus"] == "TRIAL_ONLY"
only_partial = matrix_from_flex_rows(rows[:1], data_quality={"productionReady": False})
assert only_partial["recommendationId"] is None
assert only_partial["recommendationStatus"] == "UNAVAILABLE"
def test_compare_table_propagates_flex_data_quality():
table = build_compare_table(seed_world(), track="flex")
section = table["sections"][0]
assert section["productionReady"] is False
assert section["recommendationStatus"] == "TRIAL_ONLY"
assert section["recommendationId"] in section["eligibleRecommendationIds"]