from __future__ import annotations from copy import deepcopy from server.aps_domain.closed_loop_problem import build_closed_loop_problem from server.aps_domain.mes import validate_dispatchable_version from server.aps_domain.closed_loop_runtime import ( closed_loop_to_problem_v2, persist_closed_loop_projection, project_admitted_demands_to_flex_orders, record_blocked_flex_version, run_closed_loop_candidate, ) BUSINESS_DATE = "2026-08-02" def _ready_world() -> dict: return { "salesOrders": [{ "id": 1, "orderNo": "SO-READY", "status": "APPROVED", "deliveryDate": "2026-08-04", "priority": 1, "items": [{"id": 11, "productId": 1, "productCode": "FG", "quantity": 1, "unit": "PCS"}], }], "materials": [ {"id": 1, "code": "FG", "name": "Finished", "type": "FINISHED_PRODUCT", "unit": "PCS"}, {"id": 2, "code": "RM", "name": "Raw", "type": "RAW_MATERIAL", "unit": "PCS", "stock": 1}, ], "flexMaterials": [], "boms": [{"id": 1, "productId": 1, "isDefault": True, "status": "ACTIVE"}], "bomItems": [{"id": 1, "bomId": 1, "materialId": 2, "quantity": 1}], "flexBom": [], "routings": [{"id": 1, "productId": 1, "isDefault": True, "status": "ACTIVE", "version": "V1"}], "routingSteps": [{"id": 1, "routingId": 1, "operationId": 1, "sequenceNo": 1, "runTimePerUnit": 10, "isExternal": False}], "operations": [{"id": 1, "code": "CUT", "name": "Cut"}], "flexRoutings": [], "flexEquipment": [{"id": 1, "code": "EQ-CUT", "name": "Cutter", "status": "RUNNING", "capabilities": ["CUT"]}], "flexCalendar": [{"startTime": "08:00", "endTime": "17:00", "breaks": [{"start": "12:00", "end": "13:00"}], "workdays": [1, 2, 3, 4, 5]}], "purchaseOrders": [], "outsourceOrders": [], "maintenance": [], "flexParams": {"afterDays": 7, "weights": {"weightedTardiness": 1.0}}, } def test_ready_graph_maps_to_v2_without_mutating_world(): world = _ready_world() before = deepcopy(world) closed_loop = build_closed_loop_problem(world, business_date=BUSINESS_DATE) problem = closed_loop_to_problem_v2(world, closed_loop) assert world == before assert len(problem.requirements) == 2 assert len(problem.activities) == 1 assert problem.activities[0].operationCode == "CUT" assert problem.activities[0].eligibleResourceIds == ("equipment:1",) assert len(problem.resources) == 1 assert problem.resources[0].calendarIntervals assert problem.sourceRevision == closed_loop.source_revision def test_projection_persists_only_planning_artifacts(): world = _ready_world() closed_loop = build_closed_loop_problem(world, business_date=BUSINESS_DATE) problem = closed_loop_to_problem_v2(world, closed_loop) summary = persist_closed_loop_projection(world, closed_loop, problem) assert summary["readyDemandCount"] == 1 assert len(world["manufacturingDemands"]) == 1 assert world["closedLoopProblems"][0]["schedulingProblemV2"]["problemId"] == closed_loop.problem_id assert world.get("productionOrders") in (None, []) assert world.get("workOrders") in (None, []) assert world.get("flexWorkOrders") in (None, []) def test_ready_demands_project_to_idempotent_flex_inputs(): world = _ready_world() closed_loop = build_closed_loop_problem(world, business_date=BUSINESS_DATE) first = project_admitted_demands_to_flex_orders(world, closed_loop) second = project_admitted_demands_to_flex_orders(world, closed_loop) assert first["count"] == 1 assert first["generated"] == 1 assert second["count"] == 1 assert second["generated"] == 0 assert second["updated"] == 1 assert len(world["flexOrders"]) == 1 row = world["flexOrders"][0] assert row["source"] == "CLOSED_LOOP" assert row["closedLoopProblemId"] == closed_loop.problem_id assert row["closedLoopRequirementId"] == closed_loop.manufacturing_demands[0].requirement_id def test_blocked_version_records_diagnostics_without_orphan_work_orders(): world = _ready_world() world["materials"][1]["stock"] = 0 world["routings"] = [] closed_loop = build_closed_loop_problem(world, business_date=BUSINESS_DATE) counters: dict[str, int] = {} def next_id(kind: str) -> int: counters[kind] = counters.get(kind, 0) + 1 return counters[kind] result = record_blocked_flex_version(world, next_id, closed_loop) assert result["solveStatus"] == "BLOCKED" assert result["woCount"] == 0 assert result["vlCount"] == 0 assert world["flexWorkOrders"] == [] assert world["flexVirtualLines"] == [] assert world["flexScheduleVersions"][-1]["status"] == "DRAFT" assert world["flexConflicts"] def test_closed_loop_candidate_commits_only_validated_schedule(): world = _ready_world() counters: dict[str, int] = {} def next_id(kind: str) -> int: counters[kind] = counters.get(kind, 0) + 1 return counters[kind] result = run_closed_loop_candidate(world, next_id, business_date=BUSINESS_DATE) assert result["solveStatus"] == "FEASIBLE" assert result["validation"]["valid"] is True assert result["vlCount"] == 1 assert result["woCount"] == 1 version = world["flexScheduleVersions"][-1] assert version["planningProblemId"] == result["planningProblemId"] assert version["validationReport"]["hardViolations"] == [] work_order = world["flexWorkOrders"][0] assert work_order["activityId"].startswith("ACT:") assert len(work_order["activityIdentity"]) == 64 gate = validate_dispatchable_version(world, "flex", result["versionId"]) assert gate["structuralValid"] is True assert gate["publishReady"] is True assert gate["dispatchReady"] is False assert [reason["code"] for reason in gate["blockingReasons"]] == ["VERSION_NOT_PUBLISHED"] def test_closed_loop_candidate_fails_closed_before_pool_artifacts(): world = _ready_world() world["materials"][1]["stock"] = 0 world["routings"] = [] counters: dict[str, int] = {} def next_id(kind: str) -> int: counters[kind] = counters.get(kind, 0) + 1 return counters[kind] result = run_closed_loop_candidate(world, next_id, business_date=BUSINESS_DATE) assert result["solveStatus"] == "BLOCKED" assert result["woCount"] == 0 assert result["vlCount"] == 0 assert world["flexWorkOrders"] == [] assert world["flexVirtualLines"] == [] assert world["closedLoopProblems"][-1]["problemId"] == result["planningProblemId"] assert world["flexConflicts"]