from __future__ import annotations from datetime import date, datetime, timezone from types import MappingProxyType import pytest from server.aps_domain.closed_loop_problem import build_closed_loop_problem from server.aps_domain.scheduling_problem_v2 import ( ConstraintPolicy, ObjectivePolicy, OperationActivity, PeggingAllocation, Requirement, Resource, ResourceKind, RoutingStatus, ScheduledActivity, SchedulingProblemV2, SchedulingSolutionV2, SolutionProvenance, SolveStatus, SourceFingerprint, SupplyDecision, SupplyEvent, SupplySource, SourcingType, TimeInterval, operation_activity_identity, scheduling_problem_hash, ) from server.aps_domain.scheduling_validator import ( validate_scheduling_solution, validate_solution, ) BUSINESS_DATE = date(2026, 8, 2) SOURCE_ID = "factory-pack" SOURCE_REVISION = "ruiyang-2026-08-02-r1" SOURCE_HASH = "a" * 64 def _dt(hour: int, minute: int = 0) -> datetime: return datetime(2026, 8, 2, hour, minute, tzinfo=timezone.utc) def _source_fields() -> dict[str, str]: return { "sourceRef": SOURCE_ID, "sourceRevision": SOURCE_REVISION, "sourceHash": SOURCE_HASH, } def _operation( *, activity_id: str, requirement_id: str, operation_code: str, sequence: int, eligible_resource_id: str, predecessor_ids: tuple[str, ...] = (), input_requirement_ids: tuple[str, ...] = (), ) -> OperationActivity: raw = { "activityId": activity_id, "requirementId": requirement_id, "routingId": f"route-{requirement_id}", "routingVersion": "confirmed-r1", "operationId": f"op-{activity_id}", "operationCode": operation_code, "sequence": sequence, "durationMin": 60.0, "predecessorActivityIds": predecessor_ids, "inputRequirementIds": input_requirement_ids, "eligibleResourceIds": (eligible_resource_id,), "requiredCapabilities": (operation_code,), "requiredResourceUnits": 1.0, "continuous": False, **_source_fields(), } return OperationActivity( activityIdentity=operation_activity_identity(raw), **raw, ) def _scheduled( activity: OperationActivity, *, start: datetime, end: datetime, resource_id: str, ) -> ScheduledActivity: return ScheduledActivity( activityId=activity.activityId, activityIdentity=activity.activityIdentity, requirementId=activity.requirementId, operationId=activity.operationId, sequence=activity.sequence, resourceId=resource_id, start=start, end=end, resourceUnits=1.0, ) def _rebind(problem: SchedulingProblemV2, solution: SchedulingSolutionV2) -> SchedulingSolutionV2: provenance = solution.provenance.model_copy( update={ "problemHash": scheduling_problem_hash(problem), "sourceRevision": problem.sourceRevision, "sourceFingerprints": problem.sourceFingerprints, "businessDate": problem.businessDate, } ) return solution.model_copy(update={"problemId": problem.problemId, "provenance": provenance}) def _build_valid_fixture() -> tuple[SchedulingProblemV2, SchedulingSolutionV2, MappingProxyType]: calendar = (TimeInterval(start=_dt(8), end=_dt(18), label="day-shift"),) resources = ( Resource( resourceId="factory-1", code="F1", name="Factory 1", kind=ResourceKind.FACTORY, calendarIntervals=calendar, cumulativeCapacity=4, **_source_fields(), ), Resource( resourceId="workshop-1", code="WS1", name="Workshop 1", kind=ResourceKind.WORKSHOP, parentResourceId="factory-1", calendarIntervals=calendar, cumulativeCapacity=2, **_source_fields(), ), Resource( resourceId="res-cut", code="CUT-01", name="Cut machine", kind=ResourceKind.EQUIPMENT, parentResourceId="workshop-1", capabilities=("CUT",), calendarIntervals=calendar, cumulativeCapacity=1, **_source_fields(), ), Resource( resourceId="res-weld", code="WELD-01", name="Weld machine", kind=ResourceKind.EQUIPMENT, parentResourceId="workshop-1", capabilities=("WELD",), calendarIntervals=calendar, cumulativeCapacity=1, **_source_fields(), ), Resource( resourceId="res-asm", code="ASM-01", name="Assembly station", kind=ResourceKind.EQUIPMENT, parentResourceId="workshop-1", capabilities=("ASM",), calendarIntervals=calendar, cumulativeCapacity=1, **_source_fields(), ), ) child_cut = _operation( activity_id="act-child-cut", requirement_id="req-child-make", operation_code="CUT", sequence=10, eligible_resource_id="res-cut", ) child_weld = _operation( activity_id="act-child-weld", requirement_id="req-child-make", operation_code="WELD", sequence=20, eligible_resource_id="res-weld", predecessor_ids=(child_cut.activityId,), ) root_asm = _operation( activity_id="act-root-asm", requirement_id="req-root", operation_code="ASM", sequence=10, eligible_resource_id="res-asm", input_requirement_ids=("req-child-make", "req-buy", "req-sub"), ) requirements = ( Requirement( requirementId="req-root", orderId="MOM--00280", orderLineId="MOM--00280:1", materialId="FG-001", quantity=1, sourcingType=SourcingType.MAKE, requiredAt=_dt(17), routingStatus=RoutingStatus.CONFIRMED, routingId="route-req-root", routingVersion="confirmed-r1", activityIds=(root_asm.activityId,), **_source_fields(), ), Requirement( requirementId="req-child-make", orderId="MOM--00280", orderLineId="MOM--00280:1", materialId="SEMI-001", quantity=1, sourcingType=SourcingType.MAKE, requiredAt=_dt(10), parentRequirementId="req-root", requiredByActivityId=root_asm.activityId, routingStatus=RoutingStatus.CONFIRMED, routingId="route-req-child-make", routingVersion="confirmed-r1", activityIds=(child_cut.activityId, child_weld.activityId), **_source_fields(), ), Requirement( requirementId="req-buy", orderId="MOM--00280", orderLineId="MOM--00280:1", materialId="RAW-001", quantity=2, sourcingType=SourcingType.BUY, requiredAt=_dt(10), parentRequirementId="req-root", requiredByActivityId=root_asm.activityId, **_source_fields(), ), Requirement( requirementId="req-sub", orderId="MOM--00280", orderLineId="MOM--00280:1", materialId="SUB-001", quantity=1, sourcingType=SourcingType.SUBCONTRACT, requiredAt=_dt(10), parentRequirementId="req-root", requiredByActivityId=root_asm.activityId, **_source_fields(), ), ) supply_events = ( SupplyEvent( supplyEventId="supply-buy", requirementId="req-buy", materialId="RAW-001", source=SupplySource.PURCHASE, quantity=2, availableAt=_dt(9), **_source_fields(), ), SupplyEvent( supplyEventId="supply-sub", requirementId="req-sub", materialId="SUB-001", source=SupplySource.SUBCONTRACT, quantity=1, availableAt=_dt(9, 30), **_source_fields(), ), ) fingerprints = ( SourceFingerprint(sourceId=SOURCE_ID, revision=SOURCE_REVISION, sha256=SOURCE_HASH), ) problem = SchedulingProblemV2( problemId="problem-round65-w3", businessDate=BUSINESS_DATE, planningStart=_dt(8), planningEnd=_dt(18), requirements=requirements, supplyEvents=supply_events, activities=(child_cut, child_weld, root_asm), resources=resources, objectivePolicy=ObjectivePolicy( priorities=("weightedTardiness", "changeover", "loadBalance"), weights={"weightedTardiness": 1000.0, "changeover": 10.0, "loadBalance": 1.0}, ), constraintPolicy=ConstraintPolicy( enabled={"routing": True, "material": True, "resource": True}, hardConstraints=("routing", "material", "resource", "calendar", "maintenance"), ), sourceRevision=SOURCE_REVISION, sourceFingerprints=fingerprints, metadata={"factory": "閿愭壃", "businessDate": "2026-08-02"}, ) scheduled_activities = ( _scheduled(child_cut, start=_dt(8), end=_dt(9), resource_id="res-cut"), _scheduled(child_weld, start=_dt(9), end=_dt(10), resource_id="res-weld"), _scheduled(root_asm, start=_dt(10), end=_dt(11), resource_id="res-asm"), ) solution = SchedulingSolutionV2( problemId=problem.problemId, solveStatus=SolveStatus.FEASIBLE, objectiveValues={"weightedTardiness": 0.0, "changeover": 0.0, "loadBalance": 0.25}, activities=scheduled_activities, supplyDecisions=( SupplyDecision( decisionId="decision-buy", supplyEventId="supply-buy", requirementId="req-buy", materialId="RAW-001", source=SupplySource.PURCHASE, quantity=2, availableAt=_dt(9), consumedByActivityId=root_asm.activityId, ), SupplyDecision( decisionId="decision-sub", supplyEventId="supply-sub", requirementId="req-sub", materialId="SUB-001", source=SupplySource.SUBCONTRACT, quantity=1, availableAt=_dt(9, 30), consumedByActivityId=root_asm.activityId, ), ), pegging=( PeggingAllocation( peggingId="peg-make", childRequirementId="req-child-make", parentRequirementId="req-root", quantity=1, ), PeggingAllocation( peggingId="peg-buy", childRequirementId="req-buy", parentRequirementId="req-root", quantity=2, ), PeggingAllocation( peggingId="peg-sub", childRequirementId="req-sub", parentRequirementId="req-root", quantity=1, ), ), provenance=SolutionProvenance( runId="skill-run-valid", solverId="fixture-skill", solverVersion="2.0.0", generatedAt=_dt(12), businessDate=BUSINESS_DATE, problemHash=scheduling_problem_hash(problem), sourceRevision=SOURCE_REVISION, sourceFingerprints=fingerprints, ), ) world = MappingProxyType( { "sourceRevision": SOURCE_REVISION, "problemHash": scheduling_problem_hash(problem), "sourceFingerprints": MappingProxyType( {SOURCE_ID: (SOURCE_REVISION, SOURCE_HASH)} ), } ) return problem, solution, world @pytest.fixture def valid_skill_fixture(): return _build_valid_fixture() @pytest.fixture def malicious_skill_fixture(valid_skill_fixture): problem, solution, world = valid_skill_fixture orphan = ScheduledActivity( activityId="act-orphan", activityIdentity="f" * 64, requirementId="req-root", operationId="op-orphan", sequence=99, resourceId="res-asm", start=_dt(12), end=_dt(13), ) duplicate_orphan = solution.model_copy( update={"activities": (*solution.activities, solution.activities[0], orphan)} ) child_cut, child_weld, root_asm = solution.activities reversed_routing = solution.model_copy( update={ "activities": ( child_cut.model_copy(update={"start": _dt(9), "end": _dt(10)}), child_weld.model_copy(update={"start": _dt(8), "end": _dt(9)}), root_asm, ) } ) overlap_wrong_resource = solution.model_copy( update={ "activities": ( child_cut, child_weld, root_asm.model_copy( update={"resourceId": "res-weld", "start": _dt(9, 30), "end": _dt(10, 30)} ), ) } ) return { "problem": problem, "world": world, "duplicate_orphan": duplicate_orphan, "reversed_routing": reversed_routing, "overlap_wrong_resource": overlap_wrong_resource, } def _codes(report) -> set[str]: return {issue.code for issue in report.hardViolations} def test_valid_skill_fixture_passes_and_contract_round_trips(valid_skill_fixture): problem, solution, world = valid_skill_fixture world_before = { "sourceRevision": world["sourceRevision"], "problemHash": world["problemHash"], "sourceFingerprints": dict(world["sourceFingerprints"]), } report = validate_solution(problem, solution, world=world) assert report.valid is True assert report.hardViolations == () assert len(report.operationIdentityHash) == 64 assert report.recomputedKpi["scheduledActivityCount"] == 3 assert report.recomputedKpi["requirementCount"] == 4 assert problem.businessDate == BUSINESS_DATE assert world_before == { "sourceRevision": world["sourceRevision"], "problemHash": world["problemHash"], "sourceFingerprints": dict(world["sourceFingerprints"]), } dumped = problem.model_dump(mode="json") restored = SchedulingProblemV2.model_validate(dumped) assert restored == problem assert dumped["requirements"][1]["requirementId"] == "req-child-make" assert dumped["supplyEvents"][0]["supplyEventId"] == "supply-buy" alias_report = validate_scheduling_solution(problem, solution, world=world) assert alias_report == report def test_operation_identity_hash_is_order_independent(valid_skill_fixture): problem, solution, world = valid_skill_fixture first = validate_solution(problem, solution, world=world) reordered = solution.model_copy(update={"activities": tuple(reversed(solution.activities))}) second = validate_solution(problem, reordered, world=world) assert first.valid and second.valid assert first.operationIdentityHash == second.operationIdentityHash def test_duplicate_and_orphan_skill_activities_are_rejected(malicious_skill_fixture): report = validate_solution( malicious_skill_fixture["problem"], malicious_skill_fixture["duplicate_orphan"], world=malicious_skill_fixture["world"], ) assert report.valid is False assert {"DUPLICATE_ACTIVITY", "ORPHAN_ACTIVITY"} <= _codes(report) def test_wrong_routing_order_is_rejected(malicious_skill_fixture): report = validate_solution( malicious_skill_fixture["problem"], malicious_skill_fixture["reversed_routing"], world=malicious_skill_fixture["world"], ) assert report.valid is False assert {"ROUTING_ORDER_VIOLATION", "ROUTING_PRECEDENCE_VIOLATION"} <= _codes(report) def test_overlap_wrong_resource_and_capability_are_rejected(malicious_skill_fixture): report = validate_solution( malicious_skill_fixture["problem"], malicious_skill_fixture["overlap_wrong_resource"], world=malicious_skill_fixture["world"], ) assert report.valid is False assert { "RESOURCE_CAPACITY_OVERLAP", "INELIGIBLE_RESOURCE", "RESOURCE_CAPABILITY_MISMATCH", "PARENT_STARTS_BEFORE_CHILD_COMPLETE", } <= _codes(report) def test_calendar_and_maintenance_violations_are_rejected(valid_skill_fixture): problem, solution, world = valid_skill_fixture child_cut, child_weld, root_asm = solution.activities outside_calendar = solution.model_copy( update={ "activities": ( child_cut, child_weld, root_asm.model_copy(update={"start": _dt(7), "end": _dt(8)}), ) } ) calendar_report = validate_solution(problem, outside_calendar, world=world) assert {"RESOURCE_CALENDAR_VIOLATION", "ACTIVITY_OUTSIDE_HORIZON"} <= _codes(calendar_report) maintenance = TimeInterval(start=_dt(10, 15), end=_dt(10, 45), label="planned-maintenance") changed_resources = tuple( resource.model_copy(update={"maintenanceIntervals": (maintenance,)}) if resource.resourceId == "res-asm" else resource for resource in problem.resources ) maintenance_problem = problem.model_copy(update={"resources": changed_resources}) maintenance_solution = _rebind(maintenance_problem, solution) maintenance_world = MappingProxyType( { "sourceRevision": SOURCE_REVISION, "problemHash": scheduling_problem_hash(maintenance_problem), "sourceFingerprints": world["sourceFingerprints"], } ) maintenance_report = validate_solution( maintenance_problem, maintenance_solution, world=maintenance_world, ) assert "RESOURCE_MAINTENANCE_VIOLATION" in _codes(maintenance_report) def test_material_readiness_and_parent_start_gate_are_rejected(valid_skill_fixture): problem, solution, world = valid_skill_fixture late_events = tuple( event.model_copy(update={"availableAt": _dt(10, 30)}) if event.supplyEventId == "supply-buy" else event.model_copy(update={"availableAt": _dt(10, 45)}) for event in problem.supplyEvents ) late_problem = problem.model_copy(update={"supplyEvents": late_events}) late_decisions = tuple( decision.model_copy( update={ "availableAt": _dt(10, 30) if decision.supplyEventId == "supply-buy" else _dt(10, 45) } ) for decision in solution.supplyDecisions ) late_solution = _rebind( late_problem, solution.model_copy(update={"supplyDecisions": late_decisions}), ) late_world = MappingProxyType( { "sourceRevision": SOURCE_REVISION, "problemHash": scheduling_problem_hash(late_problem), "sourceFingerprints": world["sourceFingerprints"], } ) report = validate_solution(late_problem, late_solution, world=late_world) assert report.valid is False assert {"MATERIAL_NOT_READY", "PARENT_STARTS_BEFORE_SUPPLY_READY"} <= _codes(report) material_refs = { ref for issue in report.hardViolations if issue.code == "MATERIAL_NOT_READY" for ref in issue.entityRefs } assert {"decision-buy", "decision-sub"} <= material_refs def test_supply_and_pegging_quantity_conservation_is_enforced(valid_skill_fixture): problem, solution, world = valid_skill_fixture short_buy = solution.supplyDecisions[0].model_copy(update={"quantity": 1.0}) short_peg = solution.pegging[1].model_copy(update={"quantity": 1.0}) malicious = solution.model_copy( update={ "supplyDecisions": (short_buy, solution.supplyDecisions[1]), "pegging": (solution.pegging[0], short_peg, solution.pegging[2]), } ) report = validate_solution(problem, malicious, world=world) assert report.valid is False assert {"SUPPLY_QUANTITY_NOT_CONSERVED", "PEGGING_QUANTITY_NOT_CONSERVED"} <= _codes(report) def test_identity_provenance_and_world_hash_drift_are_rejected(valid_skill_fixture): problem, solution, world = valid_skill_fixture tampered_activity = solution.activities[0].model_copy(update={"activityIdentity": "b" * 64}) drifted_fingerprint = SourceFingerprint( sourceId=SOURCE_ID, revision="stale-r0", sha256="c" * 64, ) drifted_provenance = solution.provenance.model_copy( update={ "problemHash": "d" * 64, "sourceRevision": "stale-r0", "sourceFingerprints": (drifted_fingerprint,), } ) malicious = solution.model_copy( update={ "activities": (tampered_activity, *solution.activities[1:]), "provenance": drifted_provenance, } ) stale_world = MappingProxyType( { "sourceRevision": "stale-world", "problemHash": "e" * 64, "sourceFingerprints": MappingProxyType( {SOURCE_ID: ("stale-world", "f" * 64)} ), } ) before = { "sourceRevision": stale_world["sourceRevision"], "problemHash": stale_world["problemHash"], "sourceFingerprints": dict(stale_world["sourceFingerprints"]), } report = validate_solution(problem, malicious, world=stale_world) assert report.valid is False assert { "ACTIVITY_IDENTITY_DRIFT", "PROBLEM_HASH_DRIFT", "SOURCE_REVISION_DRIFT", "SOURCE_FINGERPRINT_DRIFT", "WORLD_SOURCE_REVISION_DRIFT", "WORLD_PROBLEM_HASH_DRIFT", "WORLD_SOURCE_FINGERPRINT_DRIFT", } <= _codes(report) assert before == { "sourceRevision": stale_world["sourceRevision"], "problemHash": stale_world["problemHash"], "sourceFingerprints": dict(stale_world["sourceFingerprints"]), } def test_template_make_routing_is_fail_closed(valid_skill_fixture): problem, solution, world = valid_skill_fixture changed_requirements = tuple( requirement.model_copy(update={"routingStatus": RoutingStatus.TEMPLATE}) if requirement.requirementId == "req-child-make" else requirement for requirement in problem.requirements ) template_problem = problem.model_copy(update={"requirements": changed_requirements}) template_solution = _rebind(template_problem, solution) template_world = MappingProxyType( { "sourceRevision": SOURCE_REVISION, "problemHash": scheduling_problem_hash(template_problem), "sourceFingerprints": world["sourceFingerprints"], } ) report = validate_solution(template_problem, template_solution, world=template_world) assert report.valid is False assert "ROUTING_NOT_CONFIRMED" in _codes(report) def _w1_multi_parent_world() -> dict: return { "materials": [ {"id": 1, "code": "FG", "name": "Finished", "type": "FINISHED_PRODUCT", "unit": "PCS"}, {"id": 2, "code": "SFA", "name": "Semi A", "type": "SEMI_FINISHED", "unit": "PCS"}, {"id": 3, "code": "SFB", "name": "Semi B", "type": "SEMI_FINISHED", "unit": "PCS"}, {"id": 4, "code": "RM", "name": "Shared raw", "type": "RAW_MATERIAL", "unit": "KG", "stock": 5}, ], "flexMaterials": [], "boms": [ {"id": 1, "productId": 1, "isDefault": True, "status": "ACTIVE"}, {"id": 2, "productId": 2, "isDefault": True, "status": "ACTIVE"}, {"id": 3, "productId": 3, "isDefault": True, "status": "ACTIVE"}, ], "bomItems": [ {"id": 1, "bomId": 1, "materialId": 2, "quantity": 1}, {"id": 2, "bomId": 1, "materialId": 3, "quantity": 1}, {"id": 3, "bomId": 2, "materialId": 4, "quantity": 2}, {"id": 4, "bomId": 3, "materialId": 4, "quantity": 3}, ], "flexBom": [], "routings": [ {"id": 1, "productId": 1, "isDefault": True, "status": "ACTIVE"}, {"id": 2, "productId": 2, "isDefault": True, "status": "ACTIVE"}, {"id": 3, "productId": 3, "isDefault": True, "status": "ACTIVE"}, ], "routingSteps": [ {"id": 1, "routingId": 1, "operationId": 1, "sequenceNo": 1, "isExternal": False}, {"id": 2, "routingId": 2, "operationId": 2, "sequenceNo": 1, "isExternal": False}, {"id": 3, "routingId": 3, "operationId": 3, "sequenceNo": 1, "isExternal": False}, ], "routingOperations": [], "operations": [ {"id": 1, "code": "ASM", "name": "Assembly"}, {"id": 2, "code": "WELD", "name": "Weld A"}, {"id": 3, "code": "CUT", "name": "Cut B"}, ], "flexRoutings": [], "workstations": [], "workstationOperations": [], "equipment": [], "flexEquipment": [ {"id": 1, "code": "EQ-ASM", "status": "RUNNING", "capabilities": ["ASM"]}, {"id": 2, "code": "EQ-WELD", "status": "RUNNING", "capabilities": ["WELD"]}, {"id": 3, "code": "EQ-CUT", "status": "RUNNING", "capabilities": ["CUT"]}, ], "salesOrders": [ { "id": 1, "orderNo": "SO-W1-MULTI", "status": "APPROVED", "deliveryDate": "2026-08-05", "items": [ {"id": 1, "productId": 1, "productCode": "FG", "quantity": 1, "unit": "PCS"} ], } ], "purchaseOrders": [], "outsourceOrders": [], } def _w1_payload_to_v2(payload: dict) -> tuple[SchedulingProblemV2, SchedulingSolutionV2, MappingProxyType]: source_hash = payload["sourceHash"] source_id = "w1-closed-loop" source = { "sourceRef": source_id, "sourceRevision": source_hash, "sourceHash": source_hash, } fingerprint = SourceFingerprint(sourceId=source_id, revision=source_hash, sha256=source_hash) rows = {row["requirement_id"]: row for row in payload["requirements"]} activity_id_by_requirement = { requirement_id: f"w1-act:{requirement_id}" for requirement_id, row in rows.items() if row["sourcing_type"] == "MAKE" } consumer_ids_by_child = { requirement_id: tuple( activity_id_by_requirement[parent_id] for parent_id in row["parent_requirement_ids"] if parent_id in activity_id_by_requirement ) for requirement_id, row in rows.items() } requirement_models = [] activity_models = [] for requirement_id, row in rows.items(): parent_ids = tuple(row["parent_requirement_ids"]) consumer_ids = consumer_ids_by_child[requirement_id] activity_ids = ( (activity_id_by_requirement[requirement_id],) if requirement_id in activity_id_by_requirement else () ) routing_status = { "READY": RoutingStatus.CONFIRMED, "TEMPLATE": RoutingStatus.TEMPLATE, "MISSING": RoutingStatus.MISSING, "NOT_APPLICABLE": RoutingStatus.CONFIRMED, }[row["routing_status"]] requirement_models.append( Requirement( requirementId=requirement_id, orderId=str(row["sales_order_id"]), orderLineId=str(row["sales_order_line_id"]), materialId=str(row["material_key"]), quantity=row["quantity"], sourcingType=SourcingType(row["sourcing_type"]), requiredAt=datetime.fromisoformat(row["required_at"]).replace(tzinfo=timezone.utc), parentRequirementId=parent_ids[0] if len(parent_ids) == 1 else None, parentRequirementIds=parent_ids, requiredByActivityId=consumer_ids[0] if len(consumer_ids) == 1 else None, requiredByActivityIds=consumer_ids, routingStatus=routing_status, routingId=f"w1-route:{requirement_id}" if activity_ids else None, routingVersion=source_hash if activity_ids else None, activityIds=activity_ids, **source, ) ) make_rows = sorted( (row for row in rows.values() if row["sourcing_type"] == "MAKE"), key=lambda row: (-row["bom_depth"], row["requirement_id"]), ) scheduled_models = [] for index, row in enumerate(make_rows): requirement_id = row["requirement_id"] start = _dt(8 + index) end = _dt(9 + index) raw = { "activityId": activity_id_by_requirement[requirement_id], "requirementId": requirement_id, "routingId": f"w1-route:{requirement_id}", "routingVersion": source_hash, "operationId": f"w1-op:{requirement_id}", "operationCode": "GENERAL", "sequence": 10, "durationMin": 60.0, "predecessorActivityIds": (), "inputRequirementIds": tuple(row["child_requirement_ids"]), "eligibleResourceIds": ("w1-equipment",), "requiredCapabilities": ("GENERAL",), "requiredResourceUnits": 1.0, "continuous": False, **source, } definition = OperationActivity( activityIdentity=operation_activity_identity(raw), **raw, ) activity_models.append(definition) scheduled_models.append( _scheduled(definition, start=start, end=end, resource_id="w1-equipment") ) calendar = (TimeInterval(start=_dt(8), end=_dt(18), label="W1 mapped shift"),) resources = ( Resource( resourceId="w1-factory", code="W1-F", kind=ResourceKind.FACTORY, calendarIntervals=calendar, cumulativeCapacity=1, **source, ), Resource( resourceId="w1-equipment", code="W1-EQ", kind=ResourceKind.EQUIPMENT, parentResourceId="w1-factory", capabilities=("GENERAL",), calendarIntervals=calendar, cumulativeCapacity=1, **source, ), ) supply_models = [] supply_decisions = [] supply_event_id_by_requirement = {} for event in payload["supplyEvents"]: if event["source"] == "MAKE": continue if not event["available_at"]: continue available_at = datetime.fromisoformat(event["available_at"]).replace(tzinfo=timezone.utc) for allocation in event["allocations"]: requirement_id = allocation["requirement_id"] row = rows[requirement_id] mapped_event_id = f"w1:{event['event_id']}:{requirement_id}" supply_event_id_by_requirement[requirement_id] = mapped_event_id supply_models.append( SupplyEvent( supplyEventId=mapped_event_id, requirementId=requirement_id, materialId=str(row["material_key"]), source=SupplySource(event["source"]), quantity=allocation["quantity"], availableAt=available_at, **source, ) ) for peg in row["pegging_refs"]: parent_id = peg["parent_requirement_id"] supply_decisions.append( SupplyDecision( decisionId=f"w1-decision:{requirement_id}:{parent_id}", supplyEventId=mapped_event_id, requirementId=requirement_id, materialId=str(row["material_key"]), source=SupplySource(event["source"]), quantity=peg["quantity"], availableAt=available_at, consumedByActivityId=activity_id_by_requirement[parent_id], ) ) pegging = tuple( PeggingAllocation( peggingId=f"w1-peg:{row['requirement_id']}:{peg['parent_requirement_id']}", childRequirementId=row["requirement_id"], parentRequirementId=peg["parent_requirement_id"], quantity=peg["quantity"], ) for row in rows.values() for peg in row["pegging_refs"] ) problem = SchedulingProblemV2( problemId=payload["problemId"], businessDate=date.fromisoformat(payload["businessDate"]), planningStart=_dt(8), planningEnd=_dt(18), requirements=tuple(requirement_models), supplyEvents=tuple(supply_models), activities=tuple(activity_models), resources=resources, sourceRevision=source_hash, sourceFingerprints=(fingerprint,), metadata={"adapter": "explicit-w1-to-v2-test", "w1SchemaVersion": payload["schemaVersion"]}, ) solution = SchedulingSolutionV2( problemId=problem.problemId, solveStatus=SolveStatus.FEASIBLE, activities=tuple(scheduled_models), supplyDecisions=tuple(supply_decisions), pegging=pegging, provenance=SolutionProvenance( runId="w1-map-valid", solverId="w1-map-fixture", solverVersion="2.0", generatedAt=_dt(14), businessDate=BUSINESS_DATE, problemHash=scheduling_problem_hash(problem), sourceRevision=source_hash, sourceFingerprints=(fingerprint,), ), ) world = MappingProxyType( { "sourceRevision": source_hash, "problemHash": scheduling_problem_hash(problem), "sourceFingerprints": MappingProxyType( {source_id: (source_hash, source_hash)} ), } ) return problem, solution, world def test_w1_snake_case_payload_maps_to_v2_and_preserves_all_parents(): w1_problem = build_closed_loop_problem( _w1_multi_parent_world(), business_date="2026-08-02", ) payload = w1_problem.to_dict() shared = next( row for row in payload["requirements"] if row["material_code"] == "RM" ) assert len(shared["parent_requirement_ids"]) == 2 assert sum(peg["quantity"] for peg in shared["pegging_refs"]) == shared["quantity"] problem, solution, world = _w1_payload_to_v2(payload) mapped = next(row for row in problem.requirements if row.requirementId == shared["requirement_id"]) report = validate_solution(problem, solution, world=world) assert mapped.parentRequirementId is None assert set(mapped.parentRequirementIds) == set(shared["parent_requirement_ids"]) assert len(mapped.requiredByActivityIds) == 2 assert report.valid is True, report.hardViolations def test_multi_parent_pegging_checks_every_parent_not_only_compatibility_parent(): payload = build_closed_loop_problem( _w1_multi_parent_world(), business_date="2026-08-02", ).to_dict() problem, solution, world = _w1_payload_to_v2(payload) shared = next(row for row in problem.requirements if len(row.parentRequirementIds) == 2) shared_pegs = [peg for peg in solution.pegging if peg.childRequirementId == shared.requirementId] assert len(shared_pegs) == 2 first = shared_pegs[0].model_copy(update={"quantity": shared.quantity}) malicious = solution.model_copy( update={ "pegging": tuple( peg for peg in solution.pegging if peg.childRequirementId != shared.requirementId ) + (first,) } ) report = validate_solution(problem, malicious, world=world) assert report.valid is False assert "PEGGING_PARENT_MISSING" in _codes(report) assert "PEGGING_QUANTITY_NOT_CONSERVED" not in _codes(report)