package api
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
const (
stackBase = "bbbb1111bbbb1111bbbb1111bbbb1111"
stackTop = "cccc2222cccc2222cccc2222cccc2222"
)
// stackIndex folds a two-rung stack: base lands feature-a on trunk, top
// lands feature-b on feature-a. baseState settles the base ("" leaves
// it open), which is the fact the top's answer has to follow.
func stackIndex(t *testing.T, baseState string) *index.Index {
t.Helper()
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
open := func(hash, branch, target, when string) index.Parsed {
return index.Parsed{Hash: hash, Art: &artifact.Artifact{
Type: artifact.Forum, ThreadTitle: "land " + branch, Date: when, User: "bob",
Body: []byte("zit-landing: 1\nbranch: " + branch + "\ntarget: " + target + "\n\nready\n"),
}}
}
batch := []index.Parsed{
open(stackBase, "feature-a", "trunk", "2026-01-01T10:00:00"),
open(stackTop, "feature-b", "feature-a", "2026-01-01T10:30:00"),
}
if baseState != "" {
batch = append(batch, index.Parsed{Hash: "dddd3333dddd3333dddd3333dddd3333",
Art: &artifact.Artifact{Type: artifact.Forum, ThreadRoot: stackBase,
Date: "2026-01-01T11:00:00", User: "carol",
Body: []byte("zit-landing-state: " + baseState + "\n\ndone\n")}})
}
if err := ix.FoldNow(batch); err != nil {
t.Fatal(err)
}
return ix
}
func stackRegistry(t *testing.T, baseState string) *ops.Registry {
t.Helper()
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(stackIndex(t, baseState))}); err != nil {
t.Fatal(err)
}
return r
}
func withI() context.Context {
return ops.WithCaps(ops.WithUser(context.Background(), "bob"), oltp.CapSet("i"))
}
// The answer a client reads before pressing is the answer the press
// gives. landing.get on a rung above an open base says it may NOT land
// and why; landing.accept on it refuses with that same reason — and
// never reaches zit, whose refusal was a command line (e85bd39d).
func TestARungAboveAnOpenBaseSaysWhyItCannotLandAndAcceptAgrees(t *testing.T) {
r := stackRegistry(t, "")
got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackTop})
if err != nil {
t.Fatal(err)
}
out := got.(landingGetOut)
if !out.CanAccept {
t.Fatalf("FAIL: canAccept = false; it means PERMISSION and the caller holds i")
}
if out.MayLand {
t.Fatalf("FAIL: mayLand = true on a rung above an open base")
}
if !strings.Contains(out.MayLandReason, short(stackBase)) || !strings.Contains(out.MayLandReason, "first") {
t.Fatalf("FAIL: mayLandReason does not name the base or the remedy: %q", out.MayLandReason)
}
t.Logf("ok: get: canAccept=%v mayLand=%v reason=%q", out.CanAccept, out.MayLand, out.MayLandReason)
_, err = r.Invoke(withI(), "landing.accept", landingSayIn{Repo: "test", ID: stackTop})
if !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("FAIL: accept err = %v, want ErrInvalid", err)
}
if !strings.Contains(err.Error(), out.MayLandReason) {
t.Fatalf("FAIL: accept refused with a different reason than get reported:\n get: %q\n accept: %v", out.MayLandReason, err)
}
if strings.Contains(err.Error(), "zit landing") {
t.Fatalf("FAIL: the API's refusal is zit's command line: %v", err)
}
t.Logf("ok: accept: %v", err)
}
// A rung on a REJECTED base is sitting on work nobody will land. The
// remedy is different — retarget, not wait — and both surfaces say so.
func TestARungOnARejectedBaseSaysRetarget(t *testing.T) {
r := stackRegistry(t, "rejected")
got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackTop})
if err != nil {
t.Fatal(err)
}
out := got.(landingGetOut)
if out.MayLand || !strings.Contains(out.MayLandReason, "rejected") || !strings.Contains(out.MayLandReason, "trunk") {
t.Fatalf("FAIL: mayLand=%v reason=%q; want false with 'rejected' and the base's target", out.MayLand, out.MayLandReason)
}
_, err = r.Invoke(withI(), "landing.accept", landingSayIn{Repo: "test", ID: stackTop})
if !errors.Is(err, ops.ErrInvalid) || !strings.Contains(err.Error(), out.MayLandReason) {
t.Fatalf("FAIL: accept = %v; want ErrInvalid carrying %q", err, out.MayLandReason)
}
t.Logf("ok: %q on both surfaces", out.MayLandReason)
}
// The base itself, and a standalone landing, are not blocked by any of
// this: the reason is empty and mayLand follows permission alone.
func TestTheBaseOfAStackIsNotBlockedByTheStack(t *testing.T) {
r := stackRegistry(t, "")
got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackBase})
if err != nil {
t.Fatal(err)
}
out := got.(landingGetOut)
if !out.MayLand || out.MayLandReason != "" {
t.Fatalf("FAIL: the base reads mayLand=%v reason=%q; the bottom rung is the one accept acts on", out.MayLand, out.MayLandReason)
}
t.Log("ok: the base may land, with no reason attached")
}