Skip to main content
Glama

mcp-optimizer

test_scheduling.pyโ€ข23.2 kB
"""Tests for scheduling optimization tools.""" from unittest.mock import Mock import pytest from mcp_optimizer.schemas.base import OptimizationStatus from mcp_optimizer.tools.scheduling import ( EmployeeConstraints, Job, JobSchedulingInput, Shift, ShiftSchedulingInput, Task, register_scheduling_tools, solve_job_scheduling, solve_shift_scheduling, ) class TestTask: """Test Task model.""" def test_valid_task(self): """Test creating a valid task.""" task = Task(machine=0, duration=5, setup_time=2) assert task.machine == 0 assert task.duration == 5 assert task.setup_time == 2 def test_task_defaults(self): """Test task with default values.""" task = Task(machine=1, duration=3) assert task.setup_time == 0 def test_negative_values(self): """Test task with negative values.""" with pytest.raises(ValueError): Task(machine=-1, duration=5) with pytest.raises(ValueError): Task(machine=0, duration=-1) with pytest.raises(ValueError): Task(machine=0, duration=5, setup_time=-1) class TestJob: """Test Job model.""" def test_valid_job(self): """Test creating a valid job.""" tasks = [ Task(machine=0, duration=5), Task(machine=1, duration=3), ] job = Job( id="job1", tasks=tasks, priority=2, deadline=20, release_time=5, ) assert job.id == "job1" assert len(job.tasks) == 2 assert job.priority == 2 assert job.deadline == 20 assert job.release_time == 5 def test_job_defaults(self): """Test job with default values.""" tasks = [Task(machine=0, duration=5)] job = Job(id="job1", tasks=tasks) assert job.priority == 1 assert job.deadline is None assert job.release_time == 0 def test_empty_tasks(self): """Test job with empty tasks.""" with pytest.raises(ValueError, match="Job must have at least one task"): Job(id="job1", tasks=[]) def test_negative_values(self): """Test job with negative values.""" tasks = [Task(machine=0, duration=5)] with pytest.raises(ValueError): Job(id="job1", tasks=tasks, priority=0) with pytest.raises(ValueError): Job(id="job1", tasks=tasks, deadline=-1) with pytest.raises(ValueError): Job(id="job1", tasks=tasks, release_time=-1) class TestJobSchedulingInput: """Test JobSchedulingInput model.""" def test_valid_job_scheduling_input(self): """Test creating valid job scheduling input.""" jobs = [ Job(id="job1", tasks=[Task(machine=0, duration=5)]), Job(id="job2", tasks=[Task(machine=1, duration=3)]), ] machines = ["machine1", "machine2"] input_data = JobSchedulingInput( jobs=jobs, machines=machines, horizon=100, objective="makespan", time_limit_seconds=60.0, ) assert len(input_data.jobs) == 2 assert len(input_data.machines) == 2 assert input_data.horizon == 100 assert input_data.objective == "makespan" assert input_data.time_limit_seconds == 60.0 def test_job_scheduling_input_defaults(self): """Test job scheduling input with default values.""" jobs = [Job(id="job1", tasks=[Task(machine=0, duration=5)])] machines = ["machine1"] input_data = JobSchedulingInput(jobs=jobs, machines=machines, horizon=50) assert input_data.objective == "makespan" assert input_data.time_limit_seconds == 30.0 def test_empty_jobs(self): """Test job scheduling input with empty jobs.""" with pytest.raises(ValueError, match="Must have at least one job"): JobSchedulingInput(jobs=[], machines=["machine1"], horizon=50) def test_empty_machines(self): """Test job scheduling input with empty machines.""" jobs = [Job(id="job1", tasks=[Task(machine=0, duration=5)])] with pytest.raises(ValueError, match="Must have at least one machine"): JobSchedulingInput(jobs=jobs, machines=[], horizon=50) def test_invalid_objective(self): """Test job scheduling input with invalid objective.""" jobs = [Job(id="job1", tasks=[Task(machine=0, duration=5)])] machines = ["machine1"] with pytest.raises(ValueError): JobSchedulingInput( jobs=jobs, machines=machines, horizon=50, objective="invalid_objective" ) def test_negative_horizon(self): """Test job scheduling input with negative horizon.""" jobs = [Job(id="job1", tasks=[Task(machine=0, duration=5)])] machines = ["machine1"] with pytest.raises(ValueError): JobSchedulingInput(jobs=jobs, machines=machines, horizon=0) class TestShift: """Test Shift model.""" def test_valid_shift(self): """Test creating a valid shift.""" shift = Shift( name="morning", start=8, end=16, required_staff=3, skills_required=["customer_service", "cash_handling"], ) assert shift.name == "morning" assert shift.start == 8 assert shift.end == 16 assert shift.required_staff == 3 assert shift.skills_required == ["customer_service", "cash_handling"] def test_shift_defaults(self): """Test shift with default values.""" shift = Shift(name="evening", start=16, end=24, required_staff=2) assert shift.skills_required == [] def test_invalid_end_time(self): """Test shift with invalid end time.""" with pytest.raises(ValueError, match="End time must be after start time"): Shift(name="invalid", start=16, end=8, required_staff=2) with pytest.raises(ValueError, match="End time must be after start time"): Shift(name="invalid", start=16, end=16, required_staff=2) def test_negative_values(self): """Test shift with negative values.""" with pytest.raises(ValueError): Shift(name="test", start=-1, end=8, required_staff=2) with pytest.raises(ValueError): Shift(name="test", start=8, end=-1, required_staff=2) with pytest.raises(ValueError): Shift(name="test", start=8, end=16, required_staff=0) class TestEmployeeConstraints: """Test EmployeeConstraints model.""" def test_valid_employee_constraints(self): """Test creating valid employee constraints.""" constraints = EmployeeConstraints( max_shifts_per_week=5, min_shifts_per_week=2, unavailable_shifts=["night"], preferred_shifts=["morning", "afternoon"], skills=["customer_service"], max_consecutive_shifts=3, ) assert constraints.max_shifts_per_week == 5 assert constraints.min_shifts_per_week == 2 assert constraints.unavailable_shifts == ["night"] assert constraints.preferred_shifts == ["morning", "afternoon"] assert constraints.skills == ["customer_service"] assert constraints.max_consecutive_shifts == 3 def test_employee_constraints_defaults(self): """Test employee constraints with default values.""" constraints = EmployeeConstraints() assert constraints.max_shifts_per_week is None assert constraints.min_shifts_per_week is None assert constraints.unavailable_shifts == [] assert constraints.preferred_shifts == [] assert constraints.skills == [] assert constraints.max_consecutive_shifts is None def test_negative_values(self): """Test employee constraints with negative values.""" with pytest.raises(ValueError): EmployeeConstraints(max_shifts_per_week=-1) with pytest.raises(ValueError): EmployeeConstraints(min_shifts_per_week=-1) with pytest.raises(ValueError): EmployeeConstraints(max_consecutive_shifts=0) class TestShiftSchedulingInput: """Test ShiftSchedulingInput model.""" def test_valid_shift_scheduling_input(self): """Test creating valid shift scheduling input.""" employees = ["Alice", "Bob", "Charlie"] shifts = [ Shift(name="morning", start=8, end=16, required_staff=2), Shift(name="evening", start=16, end=24, required_staff=1), ] employee_constraints = { "Alice": EmployeeConstraints(max_shifts_per_week=4), "Bob": EmployeeConstraints(unavailable_shifts=["night"]), } input_data = ShiftSchedulingInput( employees=employees, shifts=shifts, days=7, employee_constraints=employee_constraints, time_limit_seconds=60.0, ) assert len(input_data.employees) == 3 assert len(input_data.shifts) == 2 assert input_data.days == 7 assert len(input_data.employee_constraints) == 2 assert input_data.time_limit_seconds == 60.0 def test_shift_scheduling_input_defaults(self): """Test shift scheduling input with default values.""" employees = ["Alice"] shifts = [Shift(name="morning", start=8, end=16, required_staff=1)] input_data = ShiftSchedulingInput(employees=employees, shifts=shifts, days=5) assert input_data.employee_constraints == {} assert input_data.time_limit_seconds == 30.0 def test_empty_employees(self): """Test shift scheduling input with empty employees.""" shifts = [Shift(name="morning", start=8, end=16, required_staff=1)] with pytest.raises(ValueError, match="Must have at least one employee"): ShiftSchedulingInput(employees=[], shifts=shifts, days=7) def test_empty_shifts(self): """Test shift scheduling input with empty shifts.""" employees = ["Alice"] with pytest.raises(ValueError, match="Must have at least one shift"): ShiftSchedulingInput(employees=employees, shifts=[], days=7) def test_negative_days(self): """Test shift scheduling input with negative days.""" employees = ["Alice"] shifts = [Shift(name="morning", start=8, end=16, required_staff=1)] with pytest.raises(ValueError): ShiftSchedulingInput(employees=employees, shifts=shifts, days=0) class TestJobScheduling: """Test Job Scheduling functions.""" def test_simple_job_scheduling(self): """Test simple job scheduling problem.""" input_data = { "jobs": [ { "id": "job1", "tasks": [ {"machine": 0, "duration": 3}, {"machine": 1, "duration": 2}, ], }, { "id": "job2", "tasks": [ {"machine": 1, "duration": 2}, {"machine": 0, "duration": 4}, ], }, ], "machines": ["machine1", "machine2"], "horizon": 20, "objective": "makespan", } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL assert "schedule" in result.variables assert result.objective_value is not None def test_job_scheduling_with_setup_time(self): """Test job scheduling with setup times.""" input_data = { "jobs": [ { "id": "job1", "tasks": [ {"machine": 0, "duration": 3, "setup_time": 1}, {"machine": 1, "duration": 2, "setup_time": 1}, ], }, ], "machines": ["machine1", "machine2"], "horizon": 20, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_job_scheduling_with_release_time(self): """Test job scheduling with release times.""" input_data = { "jobs": [ { "id": "job1", "tasks": [{"machine": 0, "duration": 3}], "release_time": 5, }, ], "machines": ["machine1"], "horizon": 20, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_job_scheduling_with_deadline(self): """Test job scheduling with deadlines.""" input_data = { "jobs": [ { "id": "job1", "tasks": [{"machine": 0, "duration": 3}], "deadline": 10, }, ], "machines": ["machine1"], "horizon": 20, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_job_scheduling_total_completion_time(self): """Test job scheduling with total completion time objective.""" input_data = { "jobs": [ { "id": "job1", "tasks": [{"machine": 0, "duration": 3}], }, { "id": "job2", "tasks": [{"machine": 0, "duration": 2}], }, ], "machines": ["machine1"], "horizon": 20, "objective": "total_completion_time", } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_job_scheduling_infeasible(self): """Test infeasible job scheduling problem.""" input_data = { "jobs": [ { "id": "job1", "tasks": [{"machine": 0, "duration": 15}], "deadline": 5, # Impossible deadline }, ], "machines": ["machine1"], "horizon": 10, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.INFEASIBLE def test_job_scheduling_invalid_input(self): """Test job scheduling with invalid input.""" result = solve_job_scheduling( { "jobs": [], "machines": ["machine1"], "horizon": 20, } ) assert result.status == OptimizationStatus.ERROR assert "at least one job" in result.error_message class TestShiftScheduling: """Test Shift Scheduling functions.""" def test_simple_shift_scheduling(self): """Test simple shift scheduling problem.""" input_data = { "employees": ["Alice", "Bob", "Charlie"], "shifts": [ {"name": "morning", "start": 8, "end": 16, "required_staff": 2}, {"name": "evening", "start": 16, "end": 24, "required_staff": 1}, ], "days": 3, } result = solve_shift_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL assert "schedule" in result.variables def test_shift_scheduling_with_constraints(self): """Test shift scheduling with employee constraints.""" input_data = { "employees": ["Alice", "Bob"], "shifts": [ {"name": "morning", "start": 8, "end": 16, "required_staff": 1}, {"name": "evening", "start": 16, "end": 24, "required_staff": 1}, ], "days": 5, "employee_constraints": { "Alice": { "max_shifts_per_week": 3, "unavailable_shifts": ["evening"], }, "Bob": { "min_shifts_per_week": 2, "preferred_shifts": ["evening"], }, }, } result = solve_shift_scheduling(input_data) assert result.status in [OptimizationStatus.OPTIMAL, OptimizationStatus.INFEASIBLE] def test_shift_scheduling_with_skills(self): """Test shift scheduling with skill requirements.""" input_data = { "employees": ["Alice", "Bob"], "shifts": [ { "name": "management", "start": 8, "end": 16, "required_staff": 1, "skills_required": ["management"], }, ], "days": 3, "employee_constraints": { "Alice": {"skills": ["management"]}, "Bob": {"skills": ["customer_service"]}, }, } result = solve_shift_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_shift_scheduling_consecutive_shifts(self): """Test shift scheduling with consecutive shift limits.""" input_data = { "employees": ["Alice"], "shifts": [ {"name": "shift1", "start": 8, "end": 16, "required_staff": 1}, ], "days": 5, "employee_constraints": { "Alice": {"max_consecutive_shifts": 2}, }, } result = solve_shift_scheduling(input_data) assert result.status in [OptimizationStatus.OPTIMAL, OptimizationStatus.INFEASIBLE] def test_shift_scheduling_infeasible(self): """Test infeasible shift scheduling problem.""" input_data = { "employees": ["Alice"], "shifts": [ {"name": "morning", "start": 8, "end": 16, "required_staff": 2}, # Need 2 staff ], "days": 1, } result = solve_shift_scheduling(input_data) assert result.status == OptimizationStatus.INFEASIBLE def test_shift_scheduling_invalid_input(self): """Test shift scheduling with invalid input.""" result = solve_shift_scheduling( { "employees": [], "shifts": [{"name": "morning", "start": 8, "end": 16, "required_staff": 1}], "days": 7, } ) assert result.status == OptimizationStatus.ERROR assert "at least one employee" in result.error_message class TestRegisterSchedulingTools: """Test MCP tool registration.""" def test_register_scheduling_tools(self): """Test registering scheduling tools with MCP.""" mcp_mock = Mock() mcp_mock.tool.return_value = lambda func: func # Mock decorator register_scheduling_tools(mcp_mock) # Verify that mcp.tool() was called mcp_mock.tool.assert_called() def test_job_shop_scheduling_tool(self): """Test the job shop scheduling MCP tool wrapper with real solving.""" mcp_mock = Mock() tool_functions = [] def mock_tool_decorator(): def decorator(func): tool_functions.append(func) return func return decorator mcp_mock.tool = mock_tool_decorator register_scheduling_tools(mcp_mock) # Get the first registered tool function (job scheduling) assert len(tool_functions) >= 1 tool_func = tool_functions[0] # Test the tool function with real problem jobs = [{"id": "job1", "tasks": [{"machine": 0, "duration": 5}]}] machines = ["machine1"] result = tool_func(jobs=jobs, machines=machines, horizon=20) # Verify real solving occurred assert result["status"] == "optimal" assert "variables" in result assert "execution_time" in result def test_shift_scheduling_tool(self): """Test the shift scheduling MCP tool wrapper with real solving.""" mcp_mock = Mock() tool_functions = [] def mock_tool_decorator(): def decorator(func): tool_functions.append(func) return func return decorator mcp_mock.tool = mock_tool_decorator register_scheduling_tools(mcp_mock) # Get the second registered tool function (shift scheduling) assert len(tool_functions) >= 2 tool_func = tool_functions[1] # Test the tool function with real problem employees = ["Alice", "Bob"] shifts = [{"name": "morning", "start": 8, "end": 16, "required_staff": 1}] result = tool_func(employees=employees, shifts=shifts, days=5) # Verify real solving occurred assert result["status"] == "optimal" assert "variables" in result assert "execution_time" in result class TestEdgeCases: """Test edge cases and error conditions.""" def test_single_job_single_machine(self): """Test scheduling with single job and machine.""" input_data = { "jobs": [ {"id": "job1", "tasks": [{"machine": 0, "duration": 5}]}, ], "machines": ["machine1"], "horizon": 20, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_single_employee_single_shift(self): """Test shift scheduling with single employee and shift.""" input_data = { "employees": ["Alice"], "shifts": [{"name": "morning", "start": 8, "end": 16, "required_staff": 1}], "days": 1, } result = solve_shift_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_zero_duration_task(self): """Test job scheduling with zero duration task.""" input_data = { "jobs": [ {"id": "job1", "tasks": [{"machine": 0, "duration": 0}]}, ], "machines": ["machine1"], "horizon": 20, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL def test_high_priority_job(self): """Test job scheduling with high priority job.""" input_data = { "jobs": [ { "id": "job1", "tasks": [{"machine": 0, "duration": 5}], "priority": 10, }, { "id": "job2", "tasks": [{"machine": 0, "duration": 3}], "priority": 1, }, ], "machines": ["machine1"], "horizon": 20, } result = solve_job_scheduling(input_data) assert result.status == OptimizationStatus.OPTIMAL

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dmitryanchikov/mcp-optimizer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server