fix: reconcile admitted schedule candidates
This commit is contained in:
@@ -38,6 +38,8 @@ from wf_core import InterruptRequest, RunState, RunStatus
|
||||
from wf_platform import CapabilitySource
|
||||
from wf_scheduling.history import FileScheduleHistoryRecorder, HistoryEntry
|
||||
from wf_scheduling.models import PendingCandidate
|
||||
from wf_scheduling.ownership import SchedulerOwnership
|
||||
from wf_scheduling.recovery import recover
|
||||
from wf_scheduling.store import (
|
||||
FileScheduleStore,
|
||||
ScheduleExistsError,
|
||||
@@ -787,6 +789,66 @@ async def test_occurrences_pending_synthesis_first_page_only(tmp_path: Path) ->
|
||||
assert all(row["kind"] != "pending" for row in plain["occurrences"])
|
||||
|
||||
|
||||
async def test_admitted_occurrence_replaces_retained_candidate_after_recovery(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A torn candidate clear cannot expose an admitted occurrence as pending."""
|
||||
root = tmp_path / "admitted_candidate"
|
||||
api, sched_store, run_store, artifact_store, _ = _harness(root)
|
||||
await api.create_schedule(
|
||||
schedule_id="s", deployment_id="dep.personal", trigger=_cron()
|
||||
)
|
||||
intended = ts(2026, 9, 8, 13, 0)
|
||||
admission = persist_admission(
|
||||
store=run_store,
|
||||
run_id=run_store.allocate_run_id(),
|
||||
environment=_env(artifact_store),
|
||||
resolved_input={},
|
||||
max_steps=None,
|
||||
scheduled_at=intended,
|
||||
schedule_id="s",
|
||||
schedule_revision=1,
|
||||
)
|
||||
FileScheduleHistoryRecorder(sched_store).record(
|
||||
HistoryEntry(
|
||||
schedule_id="s",
|
||||
kind="admitted",
|
||||
resolved_at=intended,
|
||||
run_id=admission.id,
|
||||
revision=1,
|
||||
reason="rev=1",
|
||||
created_at=intended,
|
||||
)
|
||||
)
|
||||
# Fault injection: admission/history are durable, but candidate clearing
|
||||
# was lost at the following persistence boundary.
|
||||
sched_store.save_candidate(
|
||||
PendingCandidate(schedule_id="s", intended_at=intended, revision=1),
|
||||
schedule_id="s",
|
||||
)
|
||||
|
||||
before_recovery = await api.list_schedule_occurrences(schedule_id="s", limit=1)
|
||||
assert before_recovery["total"] == 1
|
||||
assert before_recovery["occurrences"][0]["kind"] == "admitted"
|
||||
|
||||
ownership = SchedulerOwnership(root, owner="test").acquire()
|
||||
try:
|
||||
recover(
|
||||
schedule_store=sched_store,
|
||||
run_store=run_store,
|
||||
now=intended,
|
||||
ownership=ownership,
|
||||
)
|
||||
finally:
|
||||
ownership.release()
|
||||
|
||||
assert sched_store.get_candidate("s") is None
|
||||
after_recovery = await api.list_schedule_occurrences(schedule_id="s", limit=1)
|
||||
assert after_recovery["total"] == 1
|
||||
assert after_recovery["occurrences"][0]["kind"] == "admitted"
|
||||
assert after_recovery["occurrences"][0]["run_id"] == admission.id
|
||||
|
||||
|
||||
async def test_occurrences_pending_limit_one_traverses_every_row(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user