补充真实 MOM 数据验证

This commit is contained in:
ssk 2026-09-03 13:27:27 +08:00
parent 9aec353c73
commit 818885951c
2 changed files with 69 additions and 3 deletions

View File

@ -1043,6 +1043,8 @@ def _record_rejected_candidate(
closed_loop: ClosedLoopProblem, closed_loop: ClosedLoopProblem,
report: Any, report: Any,
candidate_result: Mapping[str, Any], candidate_result: Mapping[str, Any],
*,
engine_type: str = "CLOSED_LOOP",
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Persist validation diagnostics without leaking candidate WO/VL artifacts.""" """Persist validation diagnostics without leaking candidate WO/VL artifacts."""
@ -1053,12 +1055,21 @@ def _record_rejected_candidate(
version_id = next_id("flexScheduleVersion") version_id = next_id("flexScheduleVersion")
version_no = f"FV{closed_loop.business_date.replace('-', '')}-{len(world['flexScheduleVersions']) + 1:03d}" version_no = f"FV{closed_loop.business_date.replace('-', '')}-{len(world['flexScheduleVersions']) + 1:03d}"
violations = list(report.hardViolations) violations = list(report.hardViolations)
normalized_engine = (
"OPTIMIZE"
if str(engine_type or "CLOSED_LOOP").strip().upper() == "OPTIMIZE"
else "CLOSED_LOOP"
)
version = { version = {
"id": version_id, "id": version_id,
"versionNo": version_no, "versionNo": version_no,
"versionName": f"闭环排产校验失败 {closed_loop.business_date}", "versionName": f"闭环排产校验失败 {closed_loop.business_date}",
"sortMode": "CLOSED_LOOP", "sortMode": "CLOSED_LOOP",
"engineType": "CLOSED_LOOP", "engineType": normalized_engine,
"solverId": candidate_result.get("solverId"),
"solverVersion": candidate_result.get("solverVersion"),
"algorithmId": candidate_result.get("algorithmId"),
"algorithmVersion": candidate_result.get("algorithmVersion"),
"status": "DRAFT", "status": "DRAFT",
"solveStatus": "REJECTED", "solveStatus": "REJECTED",
"planningProblemId": closed_loop.problem_id, "planningProblemId": closed_loop.problem_id,
@ -1094,7 +1105,11 @@ def _record_rejected_candidate(
return { return {
"versionId": version_id, "versionId": version_id,
"versionNo": version_no, "versionNo": version_no,
"engineType": "CLOSED_LOOP", "engineType": normalized_engine,
"solverId": version.get("solverId"),
"solverVersion": version.get("solverVersion"),
"algorithmId": version.get("algorithmId"),
"algorithmVersion": version.get("algorithmVersion"),
"status": "DRAFT", "status": "DRAFT",
"solveStatus": "REJECTED", "solveStatus": "REJECTED",
"orderCount": version["orderCount"], "orderCount": version["orderCount"],
@ -1226,7 +1241,14 @@ def run_closed_loop_candidate(
solved["projectedFlexOrders"] = projected solved["projectedFlexOrders"] = projected
if not report.valid or solution.solveStatus != SolveStatus.FEASIBLE: if not report.valid or solution.solveStatus != SolveStatus.FEASIBLE:
rejected = _record_rejected_candidate(world, next_id, closed_loop, report, solved) rejected = _record_rejected_candidate(
world,
next_id,
closed_loop,
report,
solved,
engine_type=normalized_engine,
)
return {**rejected, **base, "projectedFlexOrders": projected} return {**rejected, **base, "projectedFlexOrders": projected}
for key in ( for key in (

View File

@ -1,9 +1,17 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import os
from pathlib import Path
import pytest
from server.aps_domain.closed_loop_runtime import run_closed_loop_candidate from server.aps_domain.closed_loop_runtime import run_closed_loop_candidate
from server.aps_domain.kangni_intake import apply_site_payload_to_world, build_site_payload_from_data_dir
from server.engines import get_engine from server.engines import get_engine
from server.engines.optimize_engine import normalize_dispatch_rule from server.engines.optimize_engine import normalize_dispatch_rule
from server.engines.pool_engine import PoolEngine from server.engines.pool_engine import PoolEngine
from server.state.seed import seed_world
from tests.golden.test_closed_loop_runtime import BUSINESS_DATE, _ready_world from tests.golden.test_closed_loop_runtime import BUSINESS_DATE, _ready_world
@ -73,3 +81,39 @@ def test_optimize_blocker_keeps_engine_identity_and_zero_artifacts():
assert result["woCount"] == 0 assert result["woCount"] == 0
assert world["flexScheduleVersions"][-1]["engineType"] == "OPTIMIZE" assert world["flexScheduleVersions"][-1]["engineType"] == "OPTIMIZE"
assert world["flexWorkOrders"] == [] assert world["flexWorkOrders"] == []
@pytest.mark.skipif(
not os.environ.get("APS_KANGNI_DATA_DIR"),
reason="set APS_KANGNI_DATA_DIR to run the external Kangni/MOM workbook test",
)
def test_real_kangni_workbooks_run_through_optimize_closed_loop_without_source_mutation():
data_dir = Path(os.environ["APS_KANGNI_DATA_DIR"])
files = sorted(data_dir.glob("*.xlsx"))
assert files, f"no .xlsx workbooks found in {data_dir}"
before = {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in files}
payload = build_site_payload_from_data_dir(data_dir, station_count=4)
world = seed_world()
intake = apply_site_payload_to_world(world, payload, clear_all=True)
result = run_closed_loop_candidate(
world,
_next_id_factory(),
business_date=BUSINESS_DATE,
engine_type="OPTIMIZE",
sort_mode="EDD",
strict=True,
)
after = {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in files}
assert before == after
assert intake["orderCount"] == 10
assert intake["routingRecordCount"] == 72
assert intake["bomCount"] == 823
assert result["engineType"] == "OPTIMIZE"
assert result["solveStatus"] == "REJECTED"
assert result["solverId"] == "optimize-dispatch"
assert result["algorithmId"] == "optimize.edd"
assert result["vlCount"] == 0
assert result["woCount"] == 0
assert result["planning"]["summary"]["blockerCounts"]["SUPPLY_SHORTAGE"] > 0