package api
import (
"context"
"errors"
"os/exec"
"path/filepath"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// strataRetargetIndex holds three open landings — base on trunk, mid on base,
// and solo on trunk by itself — and one rejected one.
func strataRetargetIndex(t *testing.T, extra ...index.Parsed) *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, who, branch, target string) index.Parsed {
return index.Parsed{Hash: hash, Art: &artifact.Artifact{
Type: artifact.Forum, ThreadTitle: branch, Date: "2026-01-01T10:00:00", User: who,
Body: []byte("zit-landing: 1\nbranch: " + branch + "\ntarget: " + target + "\n\nready\n")}}
}
batch := []index.Parsed{
open("b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", "ada", "base", "trunk"),
open("c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "ada", "mid", "base"),
open("d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "bob", "solo", "trunk"),
open("e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3", "bob", "done", "trunk"),
{Hash: "f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4", Art: &artifact.Artifact{Type: artifact.Forum,
ThreadRoot: "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3", Date: "2026-01-01T11:00:00", User: "mai",
Body: []byte("zit-landing-state: rejected\n\ndone\n")}},
}
if err := ix.FoldNow(append(batch, extra...)); err != nil {
t.Fatal(err)
}
return ix
}
func stackOps(t *testing.T, extra ...index.Parsed) *ops.Registry {
t.Helper()
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
// An empty store, so a call that passes the gate fails in the
// write rather than panicking on a nil client.
dir := t.TempDir()
if err := zitcli.Init(context.Background(), dir); err != nil {
t.Fatal(err)
}
deps := Deps{
Repos: NewRepos(map[string]*Repo{"test": {Index: strataRetargetIndex(t, extra...), Zit: zitcli.New(dir)}}),
// trunk is owned by mai; the feature branches by nobody.
Protect: stubProtect{rules: []oltp.Protection{{Branch: "trunk", Owners: []string{"mai"}}}},
}
r := ops.NewRegistry()
if err := registerLandingRetarget(r, deps); err != nil {
t.Fatal(err)
}
return r
}
func retarget(r *ops.Registry, ctx context.Context, id, to string) error {
_, err := r.Invoke(ctx, "landing.retarget", landingRetargetIn{Repo: "test", ID: id, To: to})
return err
}
// A stacked rung's own target is a feature branch nobody protects, so
// asking who may join THAT would let any developer reorder a strata.
// Who may move it is its author or an owner of where the strata lands.
func TestAStackedRungMovesOnlyForItsAuthorOrTheStrataTargetsOwner(t *testing.T) {
r := stackOps(t)
mid := "c1c1c1c1c1c1"
if err := retarget(r, as("bob", "i"), mid, "trunk"); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a developer who owns nothing moved somebody else's stacked rung: %v", err)
}
for _, who := range []string{"ada", "mai"} {
// Past the gate, the write fails for want of a store; that is
// not the refusal this test is about.
if err := retarget(r, as(who, "i"), mid, "trunk"); errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: %s may not unstack the rung: %v", who, err)
}
}
}
// The trunk owner may not carry work into a destination they do not
// own, nor anyone stack another's landing into a strata whose target
// they cannot accept onto.
func TestStackingAsksTheOwnerOfWhereTheStrataWouldLand(t *testing.T) {
r := stackOps(t)
// solo (bob's) onto mid: the strata lands on trunk either way, so
// mai may and a stranger may not.
if err := retarget(r, as("eve", "i"), "d2d2d2d2d2d2", "!c1c1c1c1c1c1"); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a stranger stacked bob's landing: %v", err)
}
if err := retarget(r, as("mai", "i"), "d2d2d2d2d2d2", "!c1c1c1c1c1c1"); errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: trunk's owner may not order what lands on trunk: %v", err)
}
}
// Stacking a rung onto one that already stands on it closes a loop no
// walk could land; a settled landing has no strata to join.
func TestStackingRefusesALoopAndASettledLanding(t *testing.T) {
r := stackOps(t)
if err := retarget(r, as("ada", "i"), "b0b0b0b0b0b0", "!c1c1c1c1c1c1"); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: base stacked onto the rung above it: %v", err)
}
if err := retarget(r, as("bob", "i"), "d2d2d2d2d2d2", "!e3e3e3e3e3e3"); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: stacked onto a rejected landing: %v", err)
}
if err := retarget(r, as("bob", "i"), "d2d2d2d2d2d2", "!0000000000"); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: stacked onto no landing at all: %v", err)
}
}
func TestAStrataIsGreenOnlyWhenEveryRungIs(t *testing.T) {
seg := func(ci ...string) []index.StackSegment {
var out []index.StackSegment
for _, c := range ci {
out = append(out, index.StackSegment{CI: c})
}
return out
}
for _, c := range []struct {
in []string
want string
}{
{[]string{"ci-pass", "ci-pass"}, "ci-pass"},
{[]string{"ci-pass", "ci-run", "ci-fail"}, "ci-fail"},
{[]string{"", "ci-pass"}, ""},
{[]string{"ci-waiting", "ci-pass"}, "ci-waiting"},
{nil, ""},
} {
if got := strataVerdict(seg(c.in...)); got != c.want {
t.Errorf("FAIL: strataVerdict(%v) = %q, want %q", c.in, got, c.want)
}
}
}
// The author may arrange their own proposal, but moving a base carries
// every rung stacked on it; when one of those is somebody else's, it is
// the destination owner's act, not the base author's.
func TestABaseAuthorMayNotCarryOthersRungs(t *testing.T) {
r := stackOps(t, index.Parsed{Hash: "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", Art: &artifact.Artifact{
Type: artifact.Forum, ThreadTitle: "top", Date: "2026-01-01T10:00:00", User: "cat",
Body: []byte("zit-landing: 1\nbranch: top\ntarget: mid\n\nready\n")}})
if err := retarget(r, as("ada", "i"), "b0b0b0b0b0b0", "release"); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: ada carried cat's rung off trunk without owning it: %v", err)
}
// mid is ada's and carries cat's top too; trunk's owner may move it.
if err := retarget(r, as("mai", "i"), "c1c1c1c1c1c1", "trunk"); errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: trunk's owner may not unstack: %v", err)
}
// cat's own top rung carries nobody: cat may move it.
if err := retarget(r, as("cat", "i"), "a5a5a5a5a5a5", "trunk"); errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: cat may not move cat's own top rung: %v", err)
}
}