Add the pure scheduling seam (campaign_scheduler.decide_schedule) that, given a static plan and window-clock offset, decides which plan entries are due and whether the window ended — mirroring judgement.combine_case_outcome, with time_scale confined to the clock mapping so it never touches judgement/report. The campaign_runner shell maps injected elapsed time to a window offset, spawns due child Runs through the existing EvalEngine.run(existing_run=...) path with campaign_id + RunTrigger.CAMPAIGN, and persists spawned-entry indices per entry for idempotent, restart-recoverable progress. No auto loop yet (ticket 03).
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""Unit tests for the pure campaign scheduling decision (no I/O)."""
|
|
|
|
from agenteval.evaluation.campaign_scheduler import (
|
|
clock_offset,
|
|
decide_schedule,
|
|
)
|
|
from agenteval.models import CampaignPlanEntry
|
|
|
|
|
|
def _plan() -> list[CampaignPlanEntry]:
|
|
return [
|
|
CampaignPlanEntry(scenario_id="s-1", offset_seconds=0, count=2),
|
|
CampaignPlanEntry(scenario_id="s-2", offset_seconds=3600, count=1),
|
|
CampaignPlanEntry(scenario_id="s-3", offset_seconds=3600, count=3),
|
|
]
|
|
|
|
|
|
# ── clock mapping ──────────────────────────────────────────────────────────
|
|
|
|
def test_clock_offset_real_wall_clock():
|
|
assert clock_offset(elapsed_seconds=120.0, time_scale=1.0) == 120.0
|
|
|
|
|
|
def test_clock_offset_compressed():
|
|
# 1 real second maps to 3600 window seconds at 3600x.
|
|
assert clock_offset(elapsed_seconds=1.0, time_scale=3600.0) == 3600.0
|
|
|
|
|
|
# ── due / not-due matrix ─────────────────────────────────────────────────────
|
|
|
|
def test_nothing_due_before_first_offset():
|
|
# Only offset-0 entries exist at t<0? Use a plan whose earliest offset is 10.
|
|
plan = [CampaignPlanEntry(scenario_id="s-1", offset_seconds=10, count=1)]
|
|
decision = decide_schedule(plan=plan, window_seconds=100, clock_offset_seconds=5)
|
|
assert decision.due == ()
|
|
assert decision.finished is False
|
|
|
|
|
|
def test_offset_zero_entry_due_at_start():
|
|
decision = decide_schedule(plan=_plan(), window_seconds=7200, clock_offset_seconds=0)
|
|
assert [d.index for d in decision.due] == [0]
|
|
assert decision.due[0].entry.count == 2
|
|
|
|
|
|
def test_multiple_entries_due_at_same_offset():
|
|
decision = decide_schedule(plan=_plan(), window_seconds=7200, clock_offset_seconds=3600)
|
|
# entry 0 (offset 0) plus entries 1 and 2 (offset 3600) are all due.
|
|
assert [d.index for d in decision.due] == [0, 1, 2]
|
|
|
|
|
|
def test_spawned_entries_excluded_idempotent():
|
|
decision = decide_schedule(
|
|
plan=_plan(), window_seconds=7200, clock_offset_seconds=3600,
|
|
spawned_indices=[0, 1],
|
|
)
|
|
assert [d.index for d in decision.due] == [2]
|
|
|
|
|
|
def test_all_spawned_yields_no_due():
|
|
decision = decide_schedule(
|
|
plan=_plan(), window_seconds=7200, clock_offset_seconds=99999,
|
|
spawned_indices=[0, 1, 2],
|
|
)
|
|
assert decision.due == ()
|
|
|
|
|
|
# ── window end ───────────────────────────────────────────────────────────────
|
|
|
|
def test_not_finished_inside_window():
|
|
decision = decide_schedule(plan=_plan(), window_seconds=7200, clock_offset_seconds=7199)
|
|
assert decision.finished is False
|
|
|
|
|
|
def test_finished_at_window_end():
|
|
decision = decide_schedule(plan=_plan(), window_seconds=7200, clock_offset_seconds=7200)
|
|
assert decision.finished is True
|