From 818885951c8b7cf557f856beed75967dbaca1a83 Mon Sep 17 00:00:00 2001 From: ssk Date: Thu, 3 Sep 2026 13:27:27 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=9C=9F=E5=AE=9E=20MOM=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/aps_domain/closed_loop_runtime.py | 28 +++++++++++++-- tests/golden/test_optimize_engine.py | 44 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/server/aps_domain/closed_loop_runtime.py b/server/aps_domain/closed_loop_runtime.py index daf95dd..b247c50 100644 --- a/server/aps_domain/closed_loop_runtime.py +++ b/server/aps_domain/closed_loop_runtime.py @@ -1043,6 +1043,8 @@ def _record_rejected_candidate( closed_loop: ClosedLoopProblem, report: Any, candidate_result: Mapping[str, Any], + *, + engine_type: str = "CLOSED_LOOP", ) -> dict[str, Any]: """Persist validation diagnostics without leaking candidate WO/VL artifacts.""" @@ -1053,12 +1055,21 @@ def _record_rejected_candidate( version_id = next_id("flexScheduleVersion") version_no = f"FV{closed_loop.business_date.replace('-', '')}-{len(world['flexScheduleVersions']) + 1:03d}" violations = list(report.hardViolations) + normalized_engine = ( + "OPTIMIZE" + if str(engine_type or "CLOSED_LOOP").strip().upper() == "OPTIMIZE" + else "CLOSED_LOOP" + ) version = { "id": version_id, "versionNo": version_no, "versionName": f"闭环排产校验失败 {closed_loop.business_date}", "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", "solveStatus": "REJECTED", "planningProblemId": closed_loop.problem_id, @@ -1094,7 +1105,11 @@ def _record_rejected_candidate( return { "versionId": version_id, "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", "solveStatus": "REJECTED", "orderCount": version["orderCount"], @@ -1226,7 +1241,14 @@ def run_closed_loop_candidate( solved["projectedFlexOrders"] = projected 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} for key in ( diff --git a/tests/golden/test_optimize_engine.py b/tests/golden/test_optimize_engine.py index 1be2d73..0abab39 100644 --- a/tests/golden/test_optimize_engine.py +++ b/tests/golden/test_optimize_engine.py @@ -1,9 +1,17 @@ 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.kangni_intake import apply_site_payload_to_world, build_site_payload_from_data_dir from server.engines import get_engine from server.engines.optimize_engine import normalize_dispatch_rule 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 @@ -73,3 +81,39 @@ def test_optimize_blocker_keeps_engine_identity_and_zero_artifacts(): assert result["woCount"] == 0 assert world["flexScheduleVersions"][-1]["engineType"] == "OPTIMIZE" 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