from __future__ import annotations from copy import deepcopy import pytest from server.aps_domain.closed_loop_problem import build_closed_loop_problem from server.aps_domain.closed_loop_runtime import ( closed_loop_to_problem_v2, flex_version_to_solution_v2, ) from server.aps_domain.scheduling_problem_v2 import ( ResourceKind, SchedulingProblemV2, SchedulingSolutionV2, scheduling_problem_hash, ) from server.aps_domain.scheduling_validator import validate_scheduling_solution_v2 from tests.golden.test_closed_loop_runtime import _ready_world BUSINESS_DATE = "2026-08-02" def _production_resource_world() -> dict: world = _ready_world() world.update({ "factories": [{"id": 1, "code": "F1", "name": "Factory", "status": "ACTIVE"}], "workshops": [{"id": 1, "factoryId": 1, "code": "W1", "name": "Workshop", "status": "ACTIVE"}], "lines": [{ "id": 1, "workshopId": 1, "code": "L1", "name": "Line", "status": "ACTIVE", "parallelCapacity": 2, "capacityPerDay": 1_000, }], "workstations": [{ "id": 1, "lineId": 1, "code": "S1", "name": "Workstation", "status": "ACTIVE", "parallelCapacity": 2, }], "flexTeams": [{ "code": "T-CUT", "name": "Cutting team", "memberCount": 2, "supportOps": ["CUT"], "workstationId": 1, }], "flexMolds": [{ "id": 1, "code": "M-CUT", "name": "Cutting mold", "operationCode": "CUT", "adaptableEquipment": ["EQ-CUT"], "lifeTotal": 10, "lifeUsed": 2, "status": "AVAILABLE", "workstationId": 1, }], }) world["flexEquipment"][0].update({ "workstationId": 1, "adaptableMolds": ["M-CUT"], }) world["routingSteps"][0].update({ "requireMold": True, "requiredTeamMembers": 1, "moldLifePerUnit": 1, }) return world def _problem_and_solution(*, include_team: bool = True, include_tooling: bool = True): world = _production_resource_world() closed_loop = build_closed_loop_problem(world, business_date=BUSINESS_DATE) problem = closed_loop_to_problem_v2(world, closed_loop) requirement_id = closed_loop.manufacturing_demands[0].requirement_id world["flexOrders"] = [{ "id": 1, "orderNo": "SO-READY", "closedLoopRequirementId": requirement_id, }] world["flexScheduleVersions"] = [{ "id": 1, "createdAt": f"{BUSINESS_DATE} 00:00", "totalTardiness": 0, "avgUtilization": 0.5, }] world["flexWorkOrders"] = [{ "id": 1, "versionId": 1, "flexOrderNo": "SO-READY", "closedLoopRequirementId": requirement_id, "seq": 1, "operationCode": "CUT", "equipmentId": 1, "equipmentCode": "EQ-CUT", "teamCode": "T-CUT" if include_team else None, "moldCode": "M-CUT" if include_tooling else None, "plannedStartTime": "2026-08-03 08:00", "plannedEndTime": "2026-08-03 08:10", }] world["flexConflicts"] = [] solution = flex_version_to_solution_v2(world, closed_loop, problem, 1) return world, problem, solution def _rebind(problem: SchedulingProblemV2, solution: SchedulingSolutionV2) -> SchedulingSolutionV2: provenance = solution.provenance.model_copy(update={"problemHash": scheduling_problem_hash(problem)}) return solution.model_copy(update={"provenance": provenance}) def _replace_resource(problem: SchedulingProblemV2, resource_id: str, **updates) -> SchedulingProblemV2: resources = tuple( resource.model_copy(update=updates) if resource.resourceId == resource_id else resource for resource in problem.resources ) return problem.model_copy(update={"resources": resources}) def test_production_adapter_reads_organization_team_and_tooling_resources(): world = _production_resource_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 resources = {resource.resourceId: resource for resource in problem.resources} assert resources["workshop:1"].parentResourceId == "factory:1" assert resources["line:1"].parentResourceId == "workshop:1" assert resources["workstation:1"].parentResourceId == "line:1" assert resources["equipment:1"].parentResourceId == "workstation:1" assert resources["team:T-CUT"].kind is ResourceKind.TEAM assert resources["team:T-CUT"].cumulativeCapacity == 2 assert resources["tooling:M-CUT"].kind is ResourceKind.TOOLING assert resources["tooling:M-CUT"].compatibleResourceIds == ("equipment:1",) assert resources["tooling:M-CUT"].lifeTotal == 10 requirements = {item.kind: item for item in problem.activities[0].resourceRequirements} assert set(requirements) == {ResourceKind.EQUIPMENT, ResourceKind.TEAM, ResourceKind.TOOLING} assert requirements[ResourceKind.TEAM].eligibleResourceIds == ("team:T-CUT",) assert requirements[ResourceKind.TOOLING].eligibleResourceIds == ("tooling:M-CUT",) assert requirements[ResourceKind.TOOLING].lifeUnits == 1 def test_complete_multi_resource_candidate_passes_v2_validator(): world, problem, solution = _problem_and_solution() report = validate_scheduling_solution_v2(problem, solution, world=world) assert report.valid is True assert report.hardViolations == () assert {allocation.kind for allocation in solution.activities[0].resourceAllocations} == { ResourceKind.EQUIPMENT, ResourceKind.TEAM, ResourceKind.TOOLING, } def test_external_style_candidate_missing_team_and_tooling_fails_closed(): world, problem, solution = _problem_and_solution(include_team=False, include_tooling=False) report = validate_scheduling_solution_v2(problem, solution, world=world) codes = {issue.code for issue in report.hardViolations} assert report.valid is False assert "REQUIRED_RESOURCE_ASSIGNMENT_MISSING" in codes missing_kinds = { issue.entityRefs[-1] for issue in report.hardViolations if issue.code == "REQUIRED_RESOURCE_ASSIGNMENT_MISSING" } assert missing_kinds == {"TEAM", "TOOLING"} @pytest.mark.parametrize( ("resource_id", "expected_ref"), [("team:T-CUT", "team:T-CUT"), ("line:1", "line:1")], ) def test_team_and_hierarchy_cumulative_capacity_fail_closed(resource_id: str, expected_ref: str): world, problem, solution = _problem_and_solution() constrained = _replace_resource(problem, resource_id, cumulativeCapacity=0.5) report = validate_scheduling_solution_v2(constrained, _rebind(constrained, solution), world=world) assert report.valid is False assert any( issue.code == "RESOURCE_CAPACITY_OVERLAP" and expected_ref in issue.entityRefs for issue in report.hardViolations ) def test_tooling_life_exhaustion_fails_closed(): world, problem, solution = _problem_and_solution() exhausted = _replace_resource(problem, "tooling:M-CUT", lifeTotal=2, lifeUsed=2) report = validate_scheduling_solution_v2(exhausted, _rebind(exhausted, solution), world=world) assert report.valid is False assert any( issue.code == "TOOLING_LIFE_EXCEEDED" and "tooling:M-CUT" in issue.entityRefs for issue in report.hardViolations )