package oltp
import (
"testing"
"time"
)
// A SCHEDULED MINUTE IS CLAIMED ONCE, WHOEVER ASKS AND HOWEVER OFTEN;
// A LATER MINUTE IS A NEW CLAIM, AN EARLIER ONE NEVER IS.
func TestAScheduledMinuteIsClaimedOnce(t *testing.T) {
db := tokenDB(t, "alpha")
m := time.Date(2026, 10, 3, 3, 0, 0, 0, time.UTC)
claim := func(at time.Time) bool {
ok, err := db.ClaimSchedule("alpha", "ci/nightly.yml", at)
if err != nil {
t.Fatal(err)
}
return ok
}
if !claim(m) || claim(m) || claim(m.Add(-time.Hour)) {
t.Errorf("FAIL: a minute was claimed twice, or an earlier one after it")
}
if !claim(m.Add(24 * time.Hour)) {
t.Errorf("FAIL: the next day's minute was refused")
}
if ok, _ := db.ClaimSchedule("alpha", "ci/other.yml", m); !ok {
t.Errorf("FAIL: another workflow's minute was refused")
}
t.Log("ok: one claim per (repository, workflow, minute), never backwards")
}
// A scheduled run still going is found by its pipeline; a push run is not.
func TestAScheduledRunStillGoingIsFound(t *testing.T) {
db := tokenDB(t, "alpha")
db.AddCIRun(CIRun{RunID: "s1", Repo: "alpha", Checkin: "t", Workflow: "w", Pipeline: "ci/nightly.yml", Status: "running"})
db.AddCIRun(CIRun{RunID: "p1", Repo: "alpha", Checkin: "t", Workflow: "w", Pipeline: "ci/test.yml", Status: "running"})
db.RecordCIRunStart("s1", "schedule", "", nil)
db.RecordCIRunStart("p1", "push", "dev", nil)
if !db.ScheduleRunning("alpha", "ci/nightly.yml") || db.ScheduleRunning("alpha", "ci/test.yml") {
t.Errorf("FAIL: ScheduleRunning misread the runs")
}
if runs, _ := db.CIRuns("alpha", 10); len(runs) != 2 || (runs[0].Event == "" && runs[1].Event == "") {
t.Errorf("FAIL: CIRuns carries no event: %+v", runs)
}
db.CompleteCIRun("s1", "succeeded", "")
if db.ScheduleRunning("alpha", "ci/nightly.yml") {
t.Errorf("FAIL: a finished scheduled run still counts as running")
}
t.Log("ok: a still-running scheduled run is found; the event rides on the run list")
}
// A scheduled run older than its jobs' timeout (at most six hours) no
// longer blocks the next fire, however it is still listed.
func TestAStaleScheduledRunDoesNotBlock(t *testing.T) {
db := tokenDB(t, "alpha")
db.AddCIRun(CIRun{RunID: "s1", Repo: "alpha", Checkin: "t", Workflow: "w", Pipeline: "ci/nightly.yml", Status: "running"})
db.RecordCIRunStart("s1", "schedule", "", map[string]RunJobGrant{"build": {Letters: "go", Timeout: 30}})
if !db.ScheduleRunning("alpha", "ci/nightly.yml") {
t.Fatalf("FAIL: a fresh scheduled run does not block")
}
db.w.Exec(`UPDATE ci_run SET started = ? WHERE run_id = 's1'`, time.Now().Add(-31*time.Minute).UTC().Format(time.RFC3339))
if db.ScheduleRunning("alpha", "ci/nightly.yml") {
t.Errorf("FAIL: a run past its 30-minute timeout still blocks")
}
db.AddCIRun(CIRun{RunID: "s2", Repo: "alpha", Checkin: "t", Workflow: "w", Pipeline: "ci/weekly.yml", Status: "running"})
db.RecordCIRunStart("s2", "schedule", "", map[string]RunJobGrant{"build": {Letters: "go"}})
db.w.Exec(`UPDATE ci_run SET started = ? WHERE run_id = 's2'`, time.Now().Add(-5*time.Hour).UTC().Format(time.RFC3339))
if !db.ScheduleRunning("alpha", "ci/weekly.yml") {
t.Errorf("FAIL: a run with no stated timeout stopped blocking before six hours")
}
db.w.Exec(`UPDATE ci_run SET started = ? WHERE run_id = 's2'`, time.Now().Add(-7*time.Hour).UTC().Format(time.RFC3339))
if db.ScheduleRunning("alpha", "ci/weekly.yml") {
t.Errorf("FAIL: a run older than six hours still blocks")
}
t.Log("ok: a scheduled run blocks only within its timeout, six hours at most")
}
// A claimed minute that did not start is kept with its reason.
func TestAScheduleFailureIsKept(t *testing.T) {
db := tokenDB(t, "alpha")
m := time.Date(2026, 10, 3, 3, 0, 0, 0, time.UTC)
db.ClaimSchedule("alpha", "ci/nightly.yml", m)
db.ScheduleFailed("alpha", "ci/nightly.yml", m, "engine unreachable")
why, at := db.LastScheduleFailure("alpha", "ci/nightly.yml")
if why != "engine unreachable" || !at.Equal(m) {
t.Errorf("FAIL: last failure %q at %s", why, at)
}
t.Log("ok: the last failed minute and its reason are kept")
}