package api
import (
"errors"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// A landing may be stacked on a branch it was never built on: it lands
// after it, and its checks test it merged onto it. What it may not be
// stacked on is a branch it shares no history with, since no merge
// between the two exists. The store is real (a Git repository with an
// orphan branch, imported), because merge bases are the store's answer.
func TestStackingNeedsSharedHistoryNotAncestry(t *testing.T) {
for _, bin := range []string{"git", "zit"} {
if _, err := exec.LookPath(bin); err != nil {
t.Skip(bin + " not on PATH")
}
}
dir := t.TempDir()
run := func(name string, args ...string) string {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(cmd.Environ(), "GIT_AUTHOR_NAME=a", "GIT_AUTHOR_EMAIL=a@b", "GIT_COMMITTER_NAME=a", "GIT_COMMITTER_EMAIL=a@b")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %v: %v\n%s", name, args, err, out)
}
return string(out)
}
run("git", "init", "-q", "-b", "main", "g")
git := func(args ...string) { run("git", append([]string{"-C", "g"}, args...)...) }
git("commit", "-q", "--allow-empty", "-m", "A")
git("checkout", "-q", "-b", "side")
git("commit", "-q", "--allow-empty", "-m", "C")
git("checkout", "-q", "-b", "beside", "main")
git("commit", "-q", "--allow-empty", "-m", "E")
git("checkout", "-q", "--orphan", "alien")
git("commit", "-q", "--allow-empty", "-m", "D")
git("checkout", "-q", "main")
run("zit", "import-git", "g", "s")
// One manifest per branch tip, by the store's own hash, so the
// index and the store agree on what each branch is.
var batch []index.Parsed
for _, line := range strings.Split(strings.TrimSpace(run("zit", "timeline", "-F", "oneline", "-R", "s")), "\n") {
f := strings.Fields(line)
if len(f) < 4 {
continue
}
batch = append(batch, index.Parsed{Hash: f[0], Art: &artifact.Artifact{Type: artifact.Manifest,
Date: f[1], User: "a", Comment: f[3],
Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: f[2]}, {Name: "*sym-" + f[2], Target: "*"}}}})
}
open := func(hash, branch string) index.Parsed {
return index.Parsed{Hash: hash, Art: &artifact.Artifact{Type: artifact.Forum, ThreadTitle: branch,
Date: "2026-01-02T10:00:00", User: "ada",
Body: []byte("zit-landing: 1\nbranch: " + branch + "\ntarget: trunk\n\nready\n")}}
}
batch = append(batch,
open("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0", "side"),
open("b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", "beside"),
open("c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", "alien"))
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
if err := ix.FoldNow(batch); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
if err := registerLandingRetarget(r, Deps{Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(filepath.Join(dir, "s"))}})}); err != nil {
t.Fatal(err)
}
// side onto beside: siblings off trunk, neither built on the other.
// Past the history check, the write fails for want of a zit record
// of the landing; only a refusal of the move itself is a failure.
if err := retarget(r, as("ada", "i"), "a0a0a0a0a0a0", "!b0b0b0b0b0b0"); errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: a sibling was refused: %v", err)
}
if err := retarget(r, as("ada", "i"), "a0a0a0a0a0a0", "!c0c0c0c0c0c0"); !errors.Is(err, ops.ErrInvalid) || !strings.Contains(err.Error(), "no history") {
t.Errorf("FAIL: a branch with no shared history was accepted: %v", err)
}
}