from __future__ import annotations from datetime import date, timedelta from itertools import combinations import pytest import server.engines.cp_engine as cp_engine from server.engines.base import EngineParams from server.engines.cp_engine import CpSatEngine, optimize_line_assignment from server.engines.queries import find_routing_steps, find_workstation_for_operation from server.engines.solver_process import SolverProcessError from server.aps_domain.constraints import hard_blocking_conflicts from server.state.seed import build_demo_world from server.timeutil import add_minutes, fmt_dt, parse_dt _C3_SCHEMA = "cp-calendar-segmented.v1" _CALENDAR_MODE = "calendar-boundary-only" _CONTINUOUS_MODE = "continuous" _SLOT_CONTRACT = { "segments", "processingMinutes", "elapsedSpanMinutes", "pauseMinutes", "segmentCount", "calendarCompliant", "calendarMode", } def _calendar_date(world: dict, line_id: int, *, working: bool) -> str: return min( row["date"] for row in world["shiftCalendar"] if row["lineId"] == line_id and bool(row["isWorking"]) is working ) def _single_line_world( *, quantity: int, orders: int = 1, start_date: str | None = None, day_shift_only: bool = False, ) -> tuple[dict, str]: world = build_demo_world() resolved_start = start_date or _calendar_date(world, 1, working=True) selected = [ row for row in world["salesOrders"] if row["items"][0]["productId"] == 1 ][:orders] assert len(selected) == orders for order in selected: order["deliveryDate"] = resolved_start order["items"][0]["quantity"] = quantity world["salesOrders"] = selected world["lineProducts"] = [ row for row in world["lineProducts"] if row["productId"] == 1 and row["lineId"] == 1 ] routing_id = next( row["id"] for row in world["routings"] if row["productId"] == 1 and row["isDefault"] ) world["routingSteps"] = [ row for row in world["routingSteps"] if row["routingId"] == routing_id and row["sequenceNo"] == 1 ] if day_shift_only: for row in world["shiftCalendar"]: if row["lineId"] == 1 and row["shiftId"] != 1: row["isWorking"] = False world["scheduleParams"]["freezeWindowHours"] = 0 world["scheduleParams"]["deliveryBufferRatio"] = 1.0 return world, resolved_start def _params(start_date: str, *, cumulative: bool = False) -> EngineParams: return EngineParams( orderIds=[], engineType="CP", strategyTemplate="COMPREHENSIVE", planningHorizonDays=3, startDate=start_date, timeLimitSeconds=3, freezeWindowHours=0, deliveryBufferRatio=1.0, constraints={ "capacity": False, "changeover": False, "personnel": cumulative, "tooling": cumulative, }, ) def _diagnose( world: dict, start_date: str, *, relaxed_constraint_id: str | None = None, cumulative: bool = False, ) -> dict: params = _params(start_date, cumulative=cumulative) entries, _, _ = CpSatEngine().collect_and_order(world, params) _, meta = optimize_line_assignment( world, entries, params, pipeline_label=( "C3-CALENDAR-BASELINE" if relaxed_constraint_id is None else "C3-CALENDAR-REMOVAL" ), relaxed_constraint_ids=( [] if relaxed_constraint_id is None else [relaxed_constraint_id] ), diagnostic_mode=True, ) assert meta["status"] in {"OPTIMAL", "FEASIBLE"} return meta def _active_slots(meta: dict) -> list[dict]: return [ slot for slot in meta["operationSlots"] if not slot.get("isFrozen") and slot.get("lineId") is not None ] def _assert_slot_contract(slot: dict) -> list[dict]: assert _SLOT_CONTRACT <= set(slot) segments = slot["segments"] assert isinstance(segments, list) and segments assert slot["segmentCount"] == len(segments) assert all( isinstance(segment.get("startMin"), int) and isinstance(segment.get("endMin"), int) and segment["startMin"] < segment["endMin"] for segment in segments ) assert segments == sorted(segments, key=lambda segment: segment["startMin"]) processing = sum(segment["endMin"] - segment["startMin"] for segment in segments) elapsed = segments[-1]["endMin"] - segments[0]["startMin"] assert slot["processingMinutes"] == processing assert slot["elapsedSpanMinutes"] == elapsed assert slot["pauseMinutes"] == elapsed - processing assert slot["startMin"] == segments[0]["startMin"] assert slot["endMin"] == segments[-1]["endMin"] return segments def _assert_no_overlap(intervals: list[tuple[int, int]]) -> None: for left, right in combinations(intervals, 2): assert left[1] <= right[0] or right[1] <= left[0], (left, right) def _counter(): values: dict[str, int] = {} def next_id(kind: str) -> int: values[kind] = values.get(kind, 0) + 1 return values[kind] return next_id def test_short_operation_uses_one_compliant_calendar_segment() -> None: world, start_date = _single_line_world(quantity=60) meta = _diagnose(world, start_date) c3 = meta["c3Calendar"] assert c3["schemaVersion"] == _C3_SCHEMA assert meta["constraintInstanceCounts"]["C3_calendar"] > 0 assert "C3_calendar" in meta["activeAssumptionConstraints"] slot = _active_slots(meta)[0] _assert_slot_contract(slot) assert slot["processingMinutes"] == 60 assert slot["elapsedSpanMinutes"] == 60 assert slot["pauseMinutes"] == 0 assert slot["segmentCount"] == 1 assert slot["calendarCompliant"] is True assert slot["calendarMode"] == _CALENDAR_MODE def test_operation_pauses_only_at_calendar_break_boundary() -> None: world, start_date = _single_line_world( quantity=500, day_shift_only=True, ) meta = _diagnose(world, start_date) slot = _active_slots(meta)[0] segments = _assert_slot_contract(slot) assert slot["processingMinutes"] == 280 assert slot["segmentCount"] == 2 assert slot["pauseMinutes"] == 60 assert segments[0]["endMin"] <= 240 assert segments[1]["startMin"] >= 300 assert slot["calendarCompliant"] is True assert slot["calendarMode"] == _CALENDAR_MODE def test_nonworking_start_day_defers_all_processing_to_next_working_window() -> None: probe = build_demo_world() start_date = _calendar_date(probe, 1, working=False) next_working_date = min( row["date"] for row in probe["shiftCalendar"] if row["lineId"] == 1 and row["isWorking"] and row["date"] > start_date ) world, _ = _single_line_world(quantity=60, start_date=start_date) meta = _diagnose(world, start_date) slot = _active_slots(meta)[0] segments = _assert_slot_contract(slot) next_working_offset = ( date.fromisoformat(next_working_date) - date.fromisoformat(start_date) ).days * 24 * 60 assert all(segment["startMin"] >= next_working_offset for segment in segments) assert slot["calendarCompliant"] is True assert slot["calendarMode"] == _CALENDAR_MODE def test_c3_removal_restores_one_continuous_interval() -> None: world, start_date = _single_line_world( quantity=500, day_shift_only=True, ) baseline = _diagnose(world, start_date) relaxed = _diagnose( world, start_date, relaxed_constraint_id="C3_calendar", ) baseline_slot = _active_slots(baseline)[0] relaxed_slot = _active_slots(relaxed)[0] _assert_slot_contract(baseline_slot) _assert_slot_contract(relaxed_slot) assert baseline_slot["segmentCount"] == 2 assert baseline_slot["pauseMinutes"] == 60 assert relaxed["relaxedConstraintIds"] == ["C3_calendar"] assert baseline["c3Calendar"]["selectedMode"] == _CALENDAR_MODE assert relaxed["c3Calendar"]["selectedMode"] == _CONTINUOUS_MODE assert relaxed_slot["calendarMode"] == _CONTINUOUS_MODE assert relaxed_slot["segmentCount"] == 1 assert relaxed_slot["pauseMinutes"] == 0 assert relaxed_slot["elapsedSpanMinutes"] == relaxed_slot["processingMinutes"] == 280 assert relaxed_slot["calendarCompliant"] is False def _shared_resource_world() -> tuple[dict, str]: world = build_demo_world() start_date = _calendar_date(world, 1, working=True) selected = [ row for row in world["salesOrders"] if row["items"][0]["productId"] == 1 ][:2] selected += [ row for row in world["salesOrders"] if row["items"][0]["productId"] == 3 ][:1] assert len(selected) == 3 for order in selected: order["deliveryDate"] = start_date order["items"][0]["quantity"] = 500 world["salesOrders"] = selected world["lineProducts"] = [ row for row in world["lineProducts"] if (row["productId"], row["lineId"]) in {(1, 1), (3, 3)} ] routing_ids = { row["id"] for row in world["routings"] if row["productId"] in {1, 3} and row["isDefault"] } world["routingSteps"] = [ row for row in world["routingSteps"] if row["routingId"] in routing_ids and row["sequenceNo"] == 1 ] for row in world["shiftCalendar"]: if row["lineId"] in {1, 3} and row["shiftId"] != 1: row["isWorking"] = False for workstation in world["workstations"]: if workstation["code"] in {"WS001", "WS010"}: workstation["teamId"] = 1 workstation["toolingId"] = 1 team = next(row for row in world["teams"] if row["id"] == 1) team["memberCount"] = 1 team["availableCount"] = 1 world["toolings"] = [{ "id": 1, "code": "TOOL-1", "name": "Shared tool", "availableCount": 1, }] world["scheduleParams"]["freezeWindowHours"] = 0 world["scheduleParams"]["deliveryBufferRatio"] = 1.0 return world, start_date def test_c2_and_c12_apply_to_processing_segments_not_elapsed_spans() -> None: world, start_date = _shared_resource_world() meta = _diagnose(world, start_date, cumulative=True) slots = _active_slots(meta) assert len(slots) == 3 by_workstation: dict[int, list[tuple[int, int]]] = {} by_team: dict[int, list[tuple[int, int]]] = {} by_tooling: dict[int, list[tuple[int, int]]] = {} workstation_by_id = {row["id"]: row for row in world["workstations"]} for slot in slots: segments = _assert_slot_contract(slot) intervals = [(segment["startMin"], segment["endMin"]) for segment in segments] workstation_id = int(slot["workstationId"]) by_workstation.setdefault(workstation_id, []).extend(intervals) workstation = workstation_by_id[workstation_id] if workstation.get("teamId") is not None: by_team.setdefault(int(workstation["teamId"]), []).extend(intervals) if workstation.get("toolingId") is not None: by_tooling.setdefault(int(workstation["toolingId"]), []).extend(intervals) for intervals in by_workstation.values(): _assert_no_overlap(intervals) _assert_no_overlap(by_team[1]) _assert_no_overlap(by_tooling[1]) cumulative = meta["cumulative"] team_resource = next( row for row in cumulative["resources"] if row["kind"] == "team" and row["id"] == 1 ) tooling_resource = next( row for row in cumulative["resources"] if row["kind"] == "tooling" and row["id"] == 1 ) assert team_resource["peakConcurrent"] == 1 assert tooling_resource["peakConcurrent"] == 1 def test_c3_metadata_reports_schema_and_model_limit() -> None: world, start_date = _single_line_world(quantity=60) meta = _diagnose(world, start_date) c3 = meta["c3Calendar"] assert c3["schemaVersion"] == _C3_SCHEMA assert c3["calendarMode"] == _CALENDAR_MODE assert c3["modelSegmentLimit"] == 50_000 assert 0 < c3["modelSegmentCount"] <= c3["modelSegmentLimit"] assert c3["calendarCompliant"] is True def test_single_logical_operation_does_not_activate_c2() -> None: world, start_date = _single_line_world(quantity=60) meta = _diagnose(world, start_date) assert meta["constraintInstanceCounts"]["C2_no_overlap"] == 0 assert "C2_no_overlap" not in meta["activeAssumptionConstraints"] def test_zero_objective_hint_does_not_fix_across_frozen_obstacle() -> None: world, start_date = _single_line_world(quantity=60) world["salesOrders"][0]["deliveryDate"] = ( date.fromisoformat(start_date) + timedelta(days=10) ).isoformat() step = find_routing_steps(world, 1)[0] workstation = find_workstation_for_operation(world, 1, step["operationId"]) assert workstation is not None anchor = parse_dt(start_date + " 08:00") world["workOrders"].append({ "id": 99001, "orderNo": "WO-FROZEN-CROSS-FREEZE", "lineId": 1, "workstationId": workstation["id"], "plannedStartTime": fmt_dt(add_minutes(anchor, 50 * 60)), "plannedEndTime": fmt_dt(add_minutes(anchor, 70 * 60)), "isFrozen": True, }) params = _params(start_date).model_copy(update={"freezeWindowHours": 60}) entries, _, _ = CpSatEngine().collect_and_order(world, params) _, meta = optimize_line_assignment( world, entries, params, pipeline_label="C3-FROZEN-HINT", diagnostic_mode=True, ) assert meta["status"] in {"OPTIMAL", "FEASIBLE"} assert meta["zeroObjectiveHintCandidate"] is True assert meta["zeroObjectiveHintFixed"] is False slot = _active_slots(meta)[0] assert slot["startMin"] >= 70 * 60 assert meta["constraintInstanceCounts"]["C2_no_overlap"] > 0 def test_c3_model_rejects_more_than_50k_segment_decisions(monkeypatch) -> None: world, start_date = _single_line_world(quantity=60) params = _params(start_date) entries, _, _ = CpSatEngine().collect_and_order(world, params) original = cp_engine._line_day_buckets def oversized_buckets(world_arg, line_ids, anchor, horizon): buckets = original(world_arg, line_ids, anchor, horizon) target = next(row for row in buckets if row["lineId"] == 1) target["effectiveWindows"] = [ {"startMin": index, "endMin": index + 1, "shiftId": 1} for index in range(50_001) ] return buckets monkeypatch.setattr(cp_engine, "_line_day_buckets", oversized_buckets) with pytest.raises((ValueError, RuntimeError), match=r"C3.*(?:50,?000|50000)"): optimize_line_assignment( world, entries, params, pipeline_label="C3-MODEL-LIMIT", diagnostic_mode=True, ) def test_short_rule_materialization_remains_calendar_compliant() -> None: world, start_date = _single_line_world(quantity=60) result = CpSatEngine().solve(world, _params(start_date), _counter()) version = next(row for row in world["scheduleVersions"] if row["id"] == result.versionId) meta = version["solverMeta"] validation = meta["materializedC3Validation"] slot = _active_slots(meta)[0] work_order = world["workOrders"][0] assert result.solveStatus in {"OPTIMAL", "FEASIBLE"} assert meta["placement"] == "cp-calendar-segmented-direct" assert meta["directlyConsumedByMaterializer"] is True assert meta["operationTimingValidation"]["passed"] is True assert validation["checked"] is True assert validation["passed"] is True assert validation["cpTimingApplied"] is True assert validation["exactAlignmentCount"] == 1 assert validation["violationCount"] == 0 assert work_order["cpTimingSource"] == "operationSlots" assert work_order["logicalOperationKey"] == slot["logicalOperationKey"] assert work_order["processingMinutes"] == slot["processingMinutes"] assert work_order["elapsedSpanMinutes"] == slot["elapsedSpanMinutes"] assert work_order["pauseMinutes"] == slot["pauseMinutes"] assert work_order["segmentCount"] == len(work_order["plannedSegments"]) == 1 assert not [ row for row in world["conflicts"] if row.get("versionId") == result.versionId and row.get("conflictType") == "CALENDAR" ] def test_rule_materialization_crossing_break_creates_c3_hard_blocker() -> None: world, start_date = _single_line_world(quantity=500, day_shift_only=True) result = CpSatEngine().solve(world, _params(start_date), _counter()) version = next(row for row in world["scheduleVersions"] if row["id"] == result.versionId) validation = version["solverMeta"]["materializedC3Validation"] blockers = hard_blocking_conflicts(world, result.versionId) calendar_blockers = [ row for row in blockers if row.get("constraintId") == "C3_calendar" ] assert result.solveStatus in {"OPTIMAL", "FEASIBLE"} assert validation["checked"] is True assert validation["passed"] is True assert validation["cpTimingApplied"] is True assert validation["exactAlignmentCount"] == 1 assert validation["violationCount"] == 0 assert calendar_blockers == [] work_order = world["workOrders"][0] segments = work_order["plannedSegments"] assert work_order["segmentCount"] == len(segments) == 2 assert work_order["processingMinutes"] == sum(row["durationMin"] for row in segments) == 280 assert work_order["elapsedSpanMinutes"] == 340 assert work_order["pauseMinutes"] == 60 assert segments[0]["endTime"] < segments[1]["startTime"] def test_direct_materialization_rejects_segment_calendar_window_drift_before_write() -> None: world, start_date = _single_line_world(quantity=500, day_shift_only=True) params = _params(start_date) entries, _, _ = CpSatEngine().collect_and_order(world, params) ordered, meta = optimize_line_assignment( world, entries, params, pipeline_label="C3-PARENT-PREFLIGHT", ) meta["operationSlots"][0]["segments"][0]["calendarWindowId"] = "forged-window" before_counts = { key: len(world[key]) for key in ("scheduleVersions", "productionOrders", "workOrders", "conflicts") } with pytest.raises(SolverProcessError) as caught: cp_engine._prepare_cp_operation_timing(world, entries, ordered, meta) assert caught.value.code == "SOLVER_RESPONSE_INVALID" assert before_counts == { key: len(world[key]) for key in ("scheduleVersions", "productionOrders", "workOrders", "conflicts") }