package api
import (
"context"
"encoding/json"
"path/filepath"
"slices"
"testing"
"time"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// AN INHERITED VERDICT IS THE CHECK-IN'S OWN (RFC 0009 §inheritance).
//
// checkin.get lists it among the check-in's checks with its state and
// the pair run it came from, and the required-checks rule a deploy
// gates on reads it as it reads a run: an inherited pass is met, an
// inherited fail is unmet.
func TestAnInheritedVerdictIsTheCheckinsOwn(t *testing.T) {
ix := redTrunkIndex(t)
runs := runsStub{runs: []CIRun{
{RunID: "p-quarry", InheritedFrom: "p-quarry", Checkin: redTrunkTip, Workflow: "quarry.quarry.1", Pipeline: "ci/quarry.json", Status: "completed"},
{RunID: "p-race", InheritedFrom: "p-race", Checkin: redTrunkTip, Workflow: "quarry.race.1", Pipeline: "ci/race.json", Status: "failed"},
}}
r := ops.NewRegistry()
d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix}}), CI: runs, Checks: checksStub{required: []string{"quarry", "race"}}}
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: redTrunkTip})
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(got)
var m struct {
Checks []map[string]any `json:"checks"`
Unmet []string `json:"unmet"`
}
if err := json.Unmarshal(b, &m); err != nil {
t.Fatal(err)
}
from := map[string]any{}
for _, c := range m.Checks {
from[c["pipeline"].(string)] = c["inheritedFrom"]
}
if from["ci/quarry.json"] != "p-quarry" || from["ci/race.json"] != "p-race" {
t.Fatalf("FAIL: checks name their sources %v; want p-quarry and p-race", from)
}
if len(m.Unmet) != 1 || m.Unmet[0] != "race" {
t.Fatalf("FAIL: unmet = %v; want the inherited fail, race, and only it", m.Unmet)
}
t.Log("ok: inherited verdicts are listed with their source and judged as the check-in's own")
}
// pushedStub files runs under a check-in or under a pair, apart, and
// names the newest pair tested for a tip.
type pushedStub struct {
runsStub
tip, pair map[string][]CIRun
latestPair map[string]string
}
func (p pushedStub) CIRunsFor(_, c string) ([]CIRun, error) { return p.tip[c], nil }
func (p pushedStub) CIRunsForPair(_, s string) ([]CIRun, error) { return p.pair[s], nil }
func (p pushedStub) LatestPairFor(_, tip string) (string, error) { return p.latestPair[tip], nil }
// A PROPOSED BRANCH CHECK-IN'S CHECKS ARE ITS MERGE'S (RFC 0009 §once).
//
// A push to a branch with an open landing is tested as its merge with
// the landing's target, so the check-in's own runs are only what stayed
// on its tip; checkin.get lists that merge's runs beside them, and
// judges the branch's required checks on either. A merge that is not
// its landing's (the branch was never proposed, or it merged into
// another branch) is not the check-in's: its tip must answer for itself.
func TestAProposedCheckinShowsItsMergesChecks(t *testing.T) {
const tip = "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1"
const trunk = "t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0"
get := func(ix *index.Index, hash, pair string) checkinGetOut {
t.Helper()
stub := pushedStub{
tip: map[string][]CIRun{hash: {{RunID: "d", Checkin: hash, Workflow: "w.deploy", Pipeline: "ci/deploy.yml", Status: "completed"}}},
pair: map[string][]CIRun{pair: {
{RunID: "q", Checkin: pair, Pair: pair, Workflow: "w.quarry", Pipeline: "ci/quarry.json", Status: "completed"},
{RunID: "r", Checkin: pair, Pair: pair, Workflow: "w.race", Pipeline: "ci/race.json", Status: "failed"},
}},
latestPair: map[string]string{hash: pair},
}
r := ops.NewRegistry()
d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix}}), CI: stub,
Checks: checksStub{required: []string{"deploy", "quarry", "race"}}}
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: hash})
if err != nil {
t.Fatal(err)
}
return got.(checkinGetOut)
}
out := get(openLandingIndex(t, "feat", tip), tip, "pair:"+tip+":"+trunk)
if len(out.Checks) != 3 {
t.Fatalf("FAIL: checks %+v; want the tip's deploy and the merge's two runs", out.Checks)
}
if len(out.Unmet) != 1 || out.Unmet[0] != "race" {
t.Fatalf("FAIL: unmet = %v; want race, failed on the merge", out.Unmet)
}
t.Log("ok: the open landing's merge runs are the branch check-in's checks")
out = get(openLandingIndex(t, "feat", tip), tip, "pair:"+tip+":"+redTrunkTip)
if len(out.Checks) != 1 || len(out.Unmet) != 2 {
t.Fatalf("FAIL: a merge into another tip read as checks %+v, unmet %v; want the tip's own only", out.Checks, out.Unmet)
}
out = get(redTrunkIndex(t), redFeatureTip, "pair:"+redFeatureTip+":"+redTrunkTip)
if len(out.Checks) != 1 || len(out.Unmet) != 2 {
t.Fatalf("FAIL: an unproposed tip read a merge's runs: checks %+v, unmet %v; want its own only", out.Checks, out.Unmet)
}
t.Log("ok: a merge that is not the open landing's is not the check-in's")
}
// A CLOSED LANDING'S MERGE IS STILL ITS TIP'S CHECKS.
//
// A push made while the landing was open was tested as its merge with
// the landing's target, and that verdict is the tip's (its tag, and the
// reason a closing landing does not test the tip again). Rejected, the
// landing no longer holds, but the merge was still the one the branch
// was proposed into: checkin.get lists its runs and judges the required
// checks on them, so checks, Required and the badge agree.
func TestAClosedLandingsMergeIsStillItsTipsChecks(t *testing.T) {
const tip = "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1"
const trunk = "t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0"
pair := "pair:" + tip + ":" + trunk
ix := openLandingIndex(t, "feat", tip)
if err := ix.FoldNow([]index.Parsed{{Hash: "bbbb2222bbbb2222bbbb2222bbbb2222",
Art: &artifact.Artifact{Type: artifact.Forum, ThreadRoot: landingHash, Date: "2026-01-01T11:00:00", User: "carol",
Body: []byte("zit-landing-state: rejected\n\nnot now\n")}}}); err != nil {
t.Fatal(err)
}
if _, open, err := ix.OpenLandingOn("feat"); err != nil || open {
t.Fatalf("FAIL: the landing is still open (%v)", err)
}
stub := pushedStub{
pair: map[string][]CIRun{pair: {
{RunID: "q", Checkin: pair, Pair: pair, Workflow: "w.quarry", Pipeline: "ci/quarry.json", Status: "completed"},
{RunID: "r", Checkin: pair, Pair: pair, Workflow: "w.race", Pipeline: "ci/race.json", Status: "failed"},
}},
latestPair: map[string]string{tip: pair},
}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix}}), CI: stub,
Checks: checksStub{required: []string{"quarry", "race"}}}); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: tip})
if err != nil {
t.Fatal(err)
}
out := got.(checkinGetOut)
if len(out.Checks) != 2 || out.CIRun != "q" {
t.Fatalf("FAIL: checks %+v, ciRun %q; want the merge's two runs, q behind the badge", out.Checks, out.CIRun)
}
if !slices.Equal(out.Unmet, []string{"race"}) {
t.Fatalf("FAIL: unmet = %v; want race, failed on the merge, and quarry met by it", out.Unmet)
}
t.Log("ok: a closed landing's merge stays its tip's checks")
}
// storedPairs answers checkin.get's pair questions from a real store.
type storedPairs struct {
runsStub
db *oltp.DB
}
func (s storedPairs) CIRunsFor(string, string) ([]CIRun, error) { return nil, nil }
func (s storedPairs) LatestPairFor(repo, tip string) (string, error) {
return s.db.LatestPairFor(repo, tip)
}
func (s storedPairs) CIRunsForPair(repo, pair string) ([]CIRun, error) {
rows, err := s.db.CIRunsForPair(repo, pair)
out := make([]CIRun, 0, len(rows))
for _, r := range rows {
out = append(out, CIRun{RunID: r.RunID, Checkin: r.Checkin, Pair: r.Pair, Workflow: r.Workflow, Pipeline: r.Pipeline, Status: r.Status})
}
return out, err
}
// A MERGE THE CLOSING LANDING CANCELLED IS NOBODY'S VERDICT.
//
// The push's merge into trunk passed; trunk moved and the landing sweep
// merged the tip into the new trunk tip; the landing was rejected and
// closing cancelled that newer merge. The tip's verdict is still the
// push's merge (it carries the tag, and the closing did not test the
// tip again), so checkin.get reads its checks from it, not from the
// newer merge that holds only cancelled runs.
func TestACancelledNewerMergeLeavesThePushsVerdict(t *testing.T) {
const tip = "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1"
const t1 = "t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0"
const t2 = "t2t2t2t2t2t2t2t2t2t2t2t2t2t2t2t2t2t2t2t2"
ix := openLandingIndex(t, "feat", tip)
if err := ix.FoldNow([]index.Parsed{
{Hash: t2, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-01-01T10:30:00", User: "ada", Comment: "trunk moves",
Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "trunk"}}}},
{Hash: "bbbb2222bbbb2222bbbb2222bbbb2222", Art: &artifact.Artifact{Type: artifact.Forum, ThreadRoot: landingHash,
Date: "2026-01-01T11:00:00", User: "carol", Body: []byte("zit-landing-state: rejected\n\nnot now\n")}},
}); err != nil {
t.Fatal(err)
}
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
run := func(id, pair, status string) {
t.Helper()
if err := db.AddCIRun(oltp.CIRun{RunID: id, Repo: "test", Checkin: pair, Pair: pair, Workflow: "w.quarry", Pipeline: "ci/quarry.json", Status: "running"}); err != nil {
t.Fatal(err)
}
if err := db.SetCIStatus(id, status); err != nil {
t.Fatal(err)
}
}
run("pp", "pair:"+tip+":"+t1, "completed")
time.Sleep(1100 * time.Millisecond) // started is stamped to the second
run("lp", "pair:"+tip+":"+t2, "cancelled")
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix}}), CI: storedPairs{db: db},
Checks: checksStub{required: []string{"quarry"}}}); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: tip})
if err != nil {
t.Fatal(err)
}
out := got.(checkinGetOut)
if len(out.Checks) != 1 || out.Checks[0].RunID != "pp" || out.CIRun != "pp" {
t.Fatalf("FAIL: checks %+v, ciRun %q; want the push's merge run pp", out.Checks, out.CIRun)
}
if len(out.Unmet) != 0 {
t.Fatalf("FAIL: unmet = %v; want quarry met by the push's merge", out.Unmet)
}
t.Log("ok: a cancelled newer merge leaves the push's merge as the tip's verdict")
}
// A PROPOSED CHECK-IN'S RUN IS ITS MERGE'S when nothing ran on the tip
// itself: ciRun links the badge to the run behind it, as before.
func TestAProposedCheckinsRunIsItsMerges(t *testing.T) {
const tip = "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1"
pair := "pair:" + tip + ":t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0"
stub := pushedStub{
pair: map[string][]CIRun{pair: {{RunID: "q", Checkin: pair, Pair: pair, Workflow: "w.quarry", Pipeline: "ci/quarry.json", Status: "completed"}}},
latestPair: map[string]string{tip: pair},
}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: openLandingIndex(t, "feat", tip)}}), CI: stub}); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: tip})
if err != nil {
t.Fatal(err)
}
if run := got.(checkinGetOut).CIRun; run != "q" {
t.Fatalf("FAIL: ciRun = %q; want the merge's run q", run)
}
t.Log("ok: the merge's run is the check-in's")
}