2026-07-23 13:38:43 +08:00
|
|
|
# ============================================================
|
|
|
|
|
# EX-07 / EX-08 分析视图黄金测试
|
|
|
|
|
# ============================================================
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-08-26 00:25:46 +08:00
|
|
|
from server.aps_domain.analytics import (
|
|
|
|
|
build_compare_table,
|
|
|
|
|
build_kpi_dashboard,
|
|
|
|
|
matrix_from_flex_rows,
|
|
|
|
|
)
|
2026-07-23 13:38:43 +08:00
|
|
|
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]
|
2026-08-26 00:25:46 +08:00
|
|
|
assert len(sec["columns"]) == 5
|
|
|
|
|
assert {column["sortMode"] for column in sec["columns"]} == {
|
|
|
|
|
"ASC", "DESC", "BOTTLENECK", "KITTING_FIRST", "SKILL_FIRST",
|
|
|
|
|
}
|
2026-07-23 13:38:43 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-26 00:25:46 +08:00
|
|
|
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"]
|