package api
import (
"errors"
"os"
"path/filepath"
"testing"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// Ticket #386: a branch whose landing was settled by a merge pushed from
// a clone, with no accept reply, could not be proposed again: landing.open
// answered 500. The new work opens as a new landing and the old one stays
// accepted.
func TestABranchMergedFromACloneOpensANewLanding(t *testing.T) {
dir, ids, ix, pipeSync := premergeStore(t)
zrun(t, dir, "update", "trunk")
zrun(t, dir, "merge", "one")
zrun(t, dir, "commit", "-m", "merge one from a clone", "--user", "u")
zrun(t, dir, "update", "one")
zwrite(t, dir, "one.txt", "one\nmore\n")
zrun(t, dir, "commit", "-m", "new work on one", "--user", "u")
if _, err := pipeSync(); err != nil {
t.Fatal(err)
}
if old, ok, err := ix.Landing(ids[0]); err != nil || !ok || old.State != "accepted" {
t.Fatalf("setup: the clone-merged landing reads %+v (found %v, %v), want accepted", old, ok, err)
}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", LandingNumbers: landingNumberDB(t),
Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(as("dan", "3"), "landing.open",
landingOpenIn{Repo: "test", Branch: "one", Target: "trunk", Message: "propose the new work"})
if err != nil {
t.Fatalf("FAIL: landing.open after a clone merge: %v", err)
}
got := out.(landingOpenOut)
if got.ID == "" || got.ID == ids[0] {
t.Fatalf("FAIL: landing.open answered id %q; want a new landing, not %s", got.ID, ids[0])
}
if l, ok, err := ix.OpenLandingOn("one"); err != nil || !ok || l.ID != got.ID {
t.Fatalf("FAIL: open landing on one = %+v (found %v, %v), want %s", l, ok, err, got.ID)
}
if old, ok, err := ix.Landing(ids[0]); err != nil || !ok || old.State != "accepted" {
t.Fatalf("FAIL: the clone-merged landing now reads %+v (found %v, %v), want accepted", old, ok, err)
}
// #390: the accepted landing lists what it landed, not the work
// committed on its branch afterwards, and the new landing lists only
// that new work, not the check-in the first one already merged.
comments := func(id string) (tip string, cs []string) {
t.Helper()
g, err := r.Invoke(as("dan", "3"), "landing.get", landingGetIn{Repo: "test", ID: id})
if err != nil {
t.Fatal(err)
}
lg := g.(landingGetOut)
for _, c := range lg.Commits {
cs = append(cs, c.Comment)
}
return lg.LandedTip, cs
}
if tip, cs := comments(ids[0]); tip == "" || len(cs) != 1 || cs[0] != "one" {
t.Errorf("FAIL: the accepted landing lists %q (landed tip %q), want only \"one\"", cs, tip)
}
if _, cs := comments(got.ID); len(cs) != 1 || cs[0] != "new work on one" {
t.Errorf("FAIL: the new landing lists %q, want only \"new work on one\"", cs)
}
// Rejected, it keeps the same list: what was proposed, not the
// branch's later work, and still not what the first landing merged.
if _, err := r.Invoke(as("dan", "i"), "landing.reject", landingSayIn{Repo: "test", ID: got.ID, Message: "not this"}); err != nil {
t.Fatal(err)
}
zwrite(t, dir, "one.txt", "one\nmore\nlater\n")
zrun(t, dir, "commit", "-m", "later work on one", "--user", "u")
if _, err := pipeSync(); err != nil {
t.Fatal(err)
}
if tip, cs := comments(got.ID); tip != "" || len(cs) != 1 || cs[0] != "new work on one" {
t.Errorf("FAIL: the rejected landing lists %q (landed tip %q), want only \"new work on one\"", cs, tip)
}
}
// A refusal from the store is the proposal's fault, said in the store's
// words: a 400, never a 500.
func TestAStoreRefusalToOpenALandingIsInvalidNotInternal(t *testing.T) {
dir, _, ix, pipeSync := premergeStore(t)
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v",
Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}); err != nil {
t.Fatal(err)
}
fakeZitScript(t, `echo "zit landing: 'one' already has an open landing (0123456789ab)" >&2; exit 1`)
_, err := r.Invoke(as("dan", "3"), "landing.open",
landingOpenIn{Repo: "test", Branch: "three", Target: "trunk", Message: "m"})
if !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("FAIL: a store refusal answered %v; want ops.ErrInvalid", err)
}
}
// fakeZitScript puts a zit on PATH that runs script and nothing else.
func fakeZitScript(t *testing.T, script string) {
t.Helper()
bin := t.TempDir()
if err := os.WriteFile(filepath.Join(bin, "zit"), []byte("#!/bin/sh\n"+script+"\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
}