package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/ci"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// schedStore is the trigger's record over a real forge database, the
// part a scheduled launch uses.
type schedStore struct {
ci.Store
db *oltp.DB
}
func (s schedStore) AddRun(runID, repo, checkin, workflow, pipeline, def, status string) error {
return s.db.AddCIRun(oltp.CIRun{RunID: runID, Repo: repo, Checkin: checkin, Workflow: workflow, Pipeline: pipeline, Status: status, DefHash: def})
}
func (s schedStore) Started(runID string, st ci.RunStart) error {
jobs := map[string]oltp.RunJobGrant{}
for k, g := range st.Jobs {
jobs[k] = oltp.RunJobGrant{Environment: g.Environment, Letters: g.Letters, Timeout: g.Timeout}
}
return s.db.RecordCIRunStart(runID, st.Event, st.Starter, jobs)
}
func (s schedStore) Note(string, string) error { return nil }
func (s schedStore) Refuse(string, string, string, string) error { return nil }
func (s schedStore) PushedBy(repo, a string) string { return s.db.PushedBy(repo, a) }
func (s schedStore) ClaimSchedule(repo, p string, m time.Time) (bool, error) {
return s.db.ClaimSchedule(repo, p, m)
}
func (s schedStore) ScheduleRunning(repo, p string) bool { return s.db.ScheduleRunning(repo, p) }
type schedRepo struct {
ci.Repo
tip string
defs map[string]string
}
func (r schedRepo) DefaultBranch() string { return "trunk" }
func (r schedRepo) BranchTip(b string) string { return map[string]string{"trunk": r.tip}[b] }
func (r schedRepo) Files(string) ([]string, error) {
return []string{"ci/nightly.yml", "ci/release.yml"}, nil
}
func (r schedRepo) Read(p, _ string) ([]byte, error) {
return []byte(r.defs[p]), nil
}
// A SCHEDULED RUN HAS NO STARTER: THE SCHEDULER'S RUN IS MINTED A TOKEN
// UNDER A POLICY THAT REQUIRES NO STARTER LETTER, AND NONE WHERE `a` IS
// REQUIRED (RFC 0021 §runs).
func TestAScheduledRunIsMintedOnlyWithoutAStarterRequirement(t *testing.T) {
ix, trunkTip, _ := closeIndex(t)
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
db.SyncProjectCode = func(string) (string, bool) { return "CODE", true }
if err := db.AddRepo("test", t.TempDir(), "", "private"); err != nil {
t.Fatal(err)
}
db.SetEnvironment("test", "release", nil, "alice")
db.SetEnvironmentTokens("test", "release", "y", "a", "alice")
n := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/v1/ci/compile":
var body struct{ Name string }
json.NewDecoder(r.Body).Decode(&body)
fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":{"schedule":[{"cron":"0 3 * * *"}]}}`, body.Name)
case r.URL.Path == "/runs" && r.Method == http.MethodPost:
n++
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{"run_id":"s%d"}`, n)
case r.URL.Path == "/runs":
fmt.Fprint(w, `[]`)
}
}))
defer srv.Close()
sched := "on:\n schedule:\n - cron: '0 3 * * *'\n"
repo := schedRepo{tip: trunkTip, defs: map[string]string{
"ci/nightly.yml": sched + "jobs:\n build:\n steps:\n - run: make\n",
"ci/release.yml": sched + "permissions:\n uv: write\njobs:\n publish:\n environment: release\n steps:\n - run: ./publish\n",
}}
if err := ci.FireSchedules(ci.New(srv.URL), schedStore{db: db}, "test", repo, "u", time.Date(2026, 10, 3, 3, 0, 0, 0, time.UTC)); err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("FAIL: the scheduler started %d runs; want 2", n)
}
d := Deps{Version: "v",
Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: &zitcli.Client{}}}),
Protect: stubProtect{rules: []oltp.Protection{{Branch: "trunk", Owners: []string{"alice"}}}},
Secrets: db,
}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
_ = context.Background()
runFor := func(p string) string {
runs, _ := db.CIRuns("test", 10)
for _, run := range runs {
if run.Pipeline == p {
return run.RunID
}
}
return ""
}
env, note, err := d.DeliverRunToken("test", runFor("ci/nightly.yml")+".build")
if err != nil || env["QUARRY_TOKEN"] == "" {
t.Errorf("FAIL: the scheduled build under the default policy got %v, %q, %v; want a token", env, note, err)
}
env, note, err = d.DeliverRunToken("test", runFor("ci/release.yml")+".publish")
if err != nil || len(env) != 0 || !strings.Contains(note, "no starter") {
t.Errorf("FAIL: the scheduled release where a is required got %v, %q, %v; want none, said why", env, note, err)
}
t.Log("ok: a scheduled run is minted where no starter letter is required and refused where one is")
}