from __future__ import annotations import pytest from server.aps_domain.closed_loop_runtime import run_closed_loop_candidate from server.aps_domain.mes import validate_dispatchable_version from tests.golden.test_closed_loop_runtime import BUSINESS_DATE, _ready_world from tests.golden.test_flex_publish_mes_gate import _scheduled_world def _scheduled_v2_world() -> tuple[dict, int]: 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" return world, int(result["versionId"]) def _scheduled_multi_resource_v2_world() -> tuple[dict, int]: world = _ready_world() world.update({ "factories": [{ "id": 101, "code": "F1", "name": "Factory", "status": "ACTIVE", "capacityMinutesPerDay": 1000, }], "workshops": [{ "id": 201, "code": "WS1", "name": "Workshop", "factoryId": 101, "status": "ACTIVE", "capacityMinutesPerDay": 900, }], "lines": [{ "id": 301, "code": "L1", "name": "Line", "workshopId": 201, "status": "ACTIVE", "capacityMinutesPerDay": 800, }], "workstations": [{ "id": 401, "code": "ST1", "name": "Station", "lineId": 301, "status": "ACTIVE", "capacityMinutesPerDay": 700, }], "flexTeams": [{ "id": 501, "code": "TEAM-CUT", "name": "Cut team", "status": "ACTIVE", "memberCount": 2, "supportOps": ["CUT"], "workstationId": 401, }], "flexMolds": [{ "id": 601, "code": "MOLD-CUT", "name": "Cut mold", "status": "AVAILABLE", "quantity": 1, "operationCode": "CUT", "capabilities": ["CUT"], "adaptableEquipment": ["EQ-CUT"], "lifeTotal": 100, "lifeUsed": 2, "workstationId": 401, }], }) world["flexEquipment"][0]["workstationId"] = 401 world["routingSteps"][0].update({ "requireMold": True, "requiredTeamMembers": 1, "moldLifePerUnit": 2, }) 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" return world, int(result["versionId"]) def _codes(validation: dict) -> set[str]: return {str(item["code"]) for item in validation["publishBlockingReasons"]} def test_mes_evidence_legacy_compatibility_is_limited_to_explicit_demo_world(): world, version_id = _scheduled_world() demo_gate = validate_dispatchable_version(world, "flex", version_id) assert demo_gate["publishReady"] is True assert demo_gate["evidenceSummary"]["mode"] == "LEGACY_DEMO_COMPAT" world["factories"][0]["name"] = "????" real_gate = validate_dispatchable_version(world, "flex", version_id) assert real_gate["publishReady"] is False assert real_gate["dispatchReady"] is False assert "V2_EVIDENCE_REQUIRED" in _codes(real_gate) assert real_gate["evidenceRef"] != demo_gate["evidenceRef"] def test_mes_evidence_v2_digest_is_stable_and_binds_planning_solution_validation_resources(): world, version_id = _scheduled_v2_world() first = validate_dispatchable_version(world, "flex", version_id) second = validate_dispatchable_version(world, "flex", version_id) assert first["publishReady"] is True assert first["dispatchReady"] is False # DRAFT 尚未完成 P2 发布 assert first["evidenceRef"] == second["evidenceRef"] assert first["evidenceSummary"] == second["evidenceSummary"] summary = first["evidenceSummary"] version = world["flexScheduleVersions"][-1] assert summary["mode"] == "V2_FAIL_CLOSED" assert summary["planningProblemId"] == version["planningProblemId"] assert summary["planningSourceHash"] == version["planningSourceHash"] assert summary["solveStatus"] == "FEASIBLE" for key in ( "problemDigest", "solutionDigest", "validationDigest", "workOrderIdentityDigest", "plannedResourceConstraintDigest", "currentResourceConstraintDigest", "currentValidationDigest", "currentOperationIdentityHash", ): assert len(summary[key]) == 64 assert summary["plannedResourceConstraintDigest"] == summary["currentResourceConstraintDigest"] @pytest.mark.parametrize( ("field", "replacement", "expected_code"), [ ("activityId", "ACT:TAMPERED:999", "WORK_ORDER_ACTIVITY_NOT_IN_SOLUTION"), ("activityIdentity", "0" * 64, "WORK_ORDER_ACTIVITY_IDENTITY_DRIFT"), ("closedLoopRequirementId", "REQ-TAMPERED", "WORK_ORDER_REQUIREMENT_ID_DRIFT"), ], ) def test_mes_evidence_v2_work_order_identity_tampering_fails_closed_and_changes_evidence( field: str, replacement: str, expected_code: str, ): world, version_id = _scheduled_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexWorkOrders"][0][field] = replacement drifted = validate_dispatchable_version(world, "flex", version_id) assert drifted["publishReady"] is False assert drifted["dispatchReady"] is False assert expected_code in _codes(drifted) assert drifted["evidenceRef"] != baseline["evidenceRef"] assert drifted["evidenceSummary"]["workOrderIdentityDigest"] != baseline["evidenceSummary"]["workOrderIdentityDigest"] @pytest.mark.parametrize("field", ["activityId", "activityIdentity", "closedLoopRequirementId"]) def test_mes_evidence_v2_missing_work_order_identity_field_fails_closed(field: str): world, version_id = _scheduled_v2_world() world["flexWorkOrders"][0][field] = None validation = validate_dispatchable_version(world, "flex", version_id) assert validation["publishReady"] is False assert validation["dispatchReady"] is False assert "INCOMPLETE_V2_WORK_ORDER_IDENTITY" in _codes(validation) def test_mes_evidence_v2_conflicting_maintenance_fails_closed_and_changes_evidence(): world, version_id = _scheduled_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) activity = world["flexScheduleVersions"][-1]["schedulingSolutionV2"]["activities"][0] equipment = world["flexEquipment"][0] world["maintenance"].append({ "id": 9001, "equipmentId": equipment["id"], "equipmentCode": equipment["code"], "startTime": activity["start"], "endTime": activity["end"], "status": "PLANNED", }) drifted = validate_dispatchable_version(world, "flex", version_id) assert drifted["publishReady"] is False assert drifted["dispatchReady"] is False assert "CURRENT_RESOURCE_CONSTRAINT_DRIFT" in _codes(drifted) assert "CURRENT_RESOURCE_MAINTENANCE_VIOLATION" in _codes(drifted) assert drifted["evidenceRef"] != baseline["evidenceRef"] assert drifted["evidenceSummary"]["currentResourceConstraintDigest"] != baseline["evidenceSummary"]["currentResourceConstraintDigest"] def test_mes_evidence_v2_calendar_drift_fails_closed_and_changes_evidence(): world, version_id = _scheduled_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexCalendar"][0]["startTime"] = "09:00" drifted = validate_dispatchable_version(world, "flex", version_id) assert drifted["publishReady"] is False assert drifted["dispatchReady"] is False assert "CURRENT_RESOURCE_CONSTRAINT_DRIFT" in _codes(drifted) assert "CURRENT_RESOURCE_CALENDAR_VIOLATION" in _codes(drifted) assert drifted["evidenceRef"] != baseline["evidenceRef"] assert drifted["evidenceSummary"]["currentResourceConstraintDigest"] != baseline["evidenceSummary"]["currentResourceConstraintDigest"] def test_mes_evidence_v2_equipment_capability_drift_fails_closed_and_changes_evidence(): world, version_id = _scheduled_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexEquipment"][0]["capabilities"] = [] drifted = validate_dispatchable_version(world, "flex", version_id) assert drifted["publishReady"] is False assert drifted["dispatchReady"] is False assert "CURRENT_RESOURCE_CONSTRAINT_DRIFT" in _codes(drifted) assert "CURRENT_RESOURCE_CAPABILITY_MISMATCH" in _codes(drifted) assert drifted["evidenceRef"] != baseline["evidenceRef"] assert drifted["evidenceSummary"]["currentResourceConstraintDigest"] != baseline["evidenceSummary"]["currentResourceConstraintDigest"] def test_mes_evidence_v2_normalizes_complete_multi_resource_solution_and_snapshot(): world, version_id = _scheduled_multi_resource_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) assert baseline["publishReady"] is True assert baseline["evidenceSummary"]["currentResourceKinds"] == [ "EQUIPMENT", "FACTORY", "LINE", "TEAM", "TOOLING", "WORKSHOP", "WORKSTATION", ] allocations = world["flexScheduleVersions"][-1]["schedulingSolutionV2"]["activities"][0]["resourceAllocations"] assert {item["kind"] for item in allocations} == {"EQUIPMENT", "TEAM", "TOOLING"} allocations.reverse() reordered = validate_dispatchable_version(world, "flex", version_id) assert reordered["publishReady"] is True assert reordered["evidenceSummary"]["solutionDigest"] == baseline["evidenceSummary"]["solutionDigest"] assert reordered["evidenceSummary"]["solutionActivityDigest"] == baseline["evidenceSummary"]["solutionActivityDigest"] assert reordered["evidenceRef"] == baseline["evidenceRef"] def test_mes_evidence_v2_observed_snapshot_automatically_covers_future_resource_fields(): world, version_id = _scheduled_multi_resource_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexTeams"][0]["futureDispatchConstraint"] = {"mode": "STRICT", "limit": 7} changed = validate_dispatchable_version(world, "flex", version_id) assert changed["publishReady"] is True # Unknown field is evidenced, not guessed as a constraint. assert changed["evidenceSummary"]["currentResourceSnapshotDigest"] != baseline["evidenceSummary"]["currentResourceSnapshotDigest"] assert changed["evidenceRef"] != baseline["evidenceRef"] @pytest.mark.parametrize( ("field", "replacement"), [("teamCode", "TEAM-OTHER"), ("moldCode", "MOLD-OTHER")], ) def test_mes_evidence_v2_work_order_binds_every_resource_allocation(field: str, replacement: str): world, version_id = _scheduled_multi_resource_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexWorkOrders"][0][field] = replacement changed = validate_dispatchable_version(world, "flex", version_id) assert changed["publishReady"] is False assert "WORK_ORDER_RESOURCE_ALLOCATION_DRIFT" in _codes(changed) assert changed["evidenceRef"] != baseline["evidenceRef"] def test_mes_evidence_v2_team_tooling_and_hierarchy_drift_change_normalized_snapshot(): world, version_id = _scheduled_multi_resource_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexTeams"][0]["memberCount"] = 1 team_drift = validate_dispatchable_version(world, "flex", version_id) assert team_drift["publishReady"] is False assert "CURRENT_RESOURCE_CONSTRAINT_DRIFT" in _codes(team_drift) assert team_drift["evidenceSummary"]["currentResourceConstraintDigest"] != baseline["evidenceSummary"]["currentResourceConstraintDigest"] world, version_id = _scheduled_multi_resource_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["flexMolds"][0]["lifeUsed"] += 1 tooling_drift = validate_dispatchable_version(world, "flex", version_id) assert tooling_drift["publishReady"] is False assert "CURRENT_RESOURCE_CONSTRAINT_DRIFT" in _codes(tooling_drift) assert tooling_drift["evidenceSummary"]["currentResourceConstraintDigest"] != baseline["evidenceSummary"]["currentResourceConstraintDigest"] world, version_id = _scheduled_multi_resource_v2_world() baseline = validate_dispatchable_version(world, "flex", version_id) world["lines"][0]["capacityMinutesPerDay"] = 5 hierarchy_drift = validate_dispatchable_version(world, "flex", version_id) assert hierarchy_drift["publishReady"] is False assert "CURRENT_RESOURCE_DAILY_CAPACITY_EXCEEDED" in _codes(hierarchy_drift) assert hierarchy_drift["evidenceSummary"]["currentResourceConstraintDigest"] != baseline["evidenceSummary"]["currentResourceConstraintDigest"]