AgentEvalTool/tests/unit/test_campaign_scheduler.py
sinohqb c6b102a9b5 feat(campaigns): add scheduling decision and child-Run spawning
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).
2026-07-30 12:06:31 +08:00

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