from env.models import SchedulingAction from env.simulator import GPUSchedulingSimulator def test_valid_assignment_completes_job(): simulator = GPUSchedulingSimulator.from_hand_authored("deadline_crunch") result = simulator.step( SchedulingAction( action="place", job_id="req-4", gpu_id="small-1", rationale="Short urgent job fits and should clear quickly.", ) ) assert result.score > 0 assert simulator.jobs["req-4"].status == "completed" assert simulator.metrics.completed_jobs == 1 def test_invalid_assignment_is_penalized(): simulator = GPUSchedulingSimulator.from_hand_authored("deadline_crunch") result = simulator.step( SchedulingAction( action="place", job_id="req-5", gpu_id="small-1", rationale="This should fail due to VRAM pressure.", ) ) assert result.score < 0 assert simulator.metrics.invalid_actions == 1 assert simulator.jobs["req-5"].status == "pending" def test_defer_advances_time(): simulator = GPUSchedulingSimulator.from_hand_authored("deadline_crunch") initial_tick = simulator.current_tick result = simulator.step(SchedulingAction(action="defer", rationale="Holding for a better placement.")) assert result.observation.current_tick == initial_tick + 1 assert simulator.metrics.total_reward == result.score def test_wait_can_outperform_greedy_large_gpu_assignment(): waiting_simulator = GPUSchedulingSimulator.from_hand_authored("wait_for_capacity") greedy_simulator = GPUSchedulingSimulator.from_hand_authored("wait_for_capacity") defer_result = waiting_simulator.step( SchedulingAction( action="defer", rationale="Cheaper capacity frees next step, so paying for the large GPU now is wasteful.", ) ) greedy_result = greedy_simulator.step( SchedulingAction( action="place", job_id="queued-b", gpu_id="large-1", rationale="Use the only idle GPU immediately.", ) ) assert defer_result.score > greedy_result.score assert waiting_simulator.jobs["queued-b"].status == "pending" assert greedy_simulator.jobs["queued-b"].status == "running" def test_wait_can_preserve_large_gpu_for_visible_heavy_workload(): waiting_simulator = GPUSchedulingSimulator.from_hand_authored("reserve_large_gpu") greedy_simulator = GPUSchedulingSimulator.from_hand_authored("reserve_large_gpu") defer_result = waiting_simulator.step( SchedulingAction( action="defer", rationale="Hold the expensive large GPU until the cheaper GPUs finish warmup.", ) ) greedy_result = greedy_simulator.step( SchedulingAction( action="place", job_id="reserve-small", gpu_id="large-1", rationale="Use the only idle GPU immediately.", ) ) assert defer_result.score > greedy_result.score assert waiting_simulator.jobs["reserve-heavy"].status == "pending" assert greedy_simulator.jobs["reserve-small"].status == "running" def test_seeded_scenario_is_deterministic(): sim_a = GPUSchedulingSimulator.from_seed(42) sim_b = GPUSchedulingSimulator.from_seed(42) assert sim_a.observe().model_dump() == sim_b.observe().model_dump() def test_scenario_terminates_after_jobs_resolve(): simulator = GPUSchedulingSimulator.from_hand_authored("deadline_crunch") done = False while not done: result = simulator.step(SchedulingAction(action="defer", rationale="Let the pending queue age.")) done = result.done assert simulator.current_tick <= simulator.tick_limit assert all(job.status in {"completed", "missed"} for job in simulator.jobs.values())