package harness
import (
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// A conflicted landing must render as BLOCKED, naming the files, with accept
// disabled — never offered-and-failing. zit#131 gives the forge the only safe
// way to know: ask the store (`landing show`), because on a server the
// attempt IS the damage.
//
// This drives a real store with genuinely divergent branches rather than a
// fixture, because the thing under test is the reading of zit's answer.
func TestConflictedLandingRendersAsBlocked(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
store := t.TempDir()
run := func(args ...string) string {
t.Helper()
cmd := exec.Command("zit", args...)
cmd.Dir = store
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("zit %v: %v\n%s", args, err, out)
}
return strings.TrimSpace(string(out))
}
write := func(body string) {
if err := os.WriteFile(filepath.Join(store, "f.txt"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
run("init")
write("alpha\nbeta\ngamma\n")
run("add", "f.txt")
run("commit", "-m", "base")
// zit's info/status lead-in has changed shape across versions;
// the check-in is wherever the first hex token of useful length
// is, so take that rather than a position.
base := ""
for _, f := range strings.Fields(run("info")) {
if len(f) >= 12 && strings.IndexFunc(f, func(r rune) bool {
return (r < '0' || r > '9') && (r < 'a' || r > 'f')
}) < 0 {
base = f
break
}
}
if base == "" {
t.Fatal("no check-in hash in zit info output")
}
// Two edits to the same line, on branches neither of which descends
// from the other: the shape a content merge cannot resolve.
write("ALPHA-from-feat\nbeta\ngamma\n")
run("commit", "-m", "feat edit", "--branch", "feat")
run("update", base)
write("ALPHA-from-trunk\nbeta\ngamma\n")
run("commit", "-m", "trunk moves")
run("landing", "open", "--branch", "feat", "-m", "land feat", "--user", "dan")
srv := ingestAndServe(t, map[string]string{"alpha": store}, t.TempDir())
resp, err := srv.Client().Get(srv.URL + "/r/alpha/landing")
if err != nil {
t.Fatal(err)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
id := betweenLast(string(b), `/r/alpha/landing/`, `"`)
if id == "" {
t.Fatalf("the landing never reached the listing:\n%s", b)
}
page, err := srv.Client().Get(srv.URL + "/r/alpha/landing/" + id)
if err != nil {
t.Fatal(err)
}
defer page.Body.Close()
pb, _ := io.ReadAll(page.Body)
html := string(pb)
if !strings.Contains(html, "conflicts with") {
t.Errorf("a conflicted landing does not say so:\n%s", html)
}
// Naming the file is the difference between a warning and an
// instruction: the reader has to know what to go and fix.
if !strings.Contains(html, "f.txt") {
t.Error("the conflicting path is not named")
}
// Offering a control that cannot work is the failure this whole
// mechanism exists to prevent.
if strings.Contains(html, `formaction="/r/alpha/landing/`+id+`/accept"`) {
t.Error("accept was offered on a conflicted landing")
}
}
func firstHash(status string) string {
for _, f := range strings.Fields(strings.ReplaceAll(status, ")", " ")) {
if len(f) >= 40 && strings.Trim(f, "0123456789abcdef") == "" {
return f
}
}
return ""
}
func betweenLast(s, open, close string) string {
i := strings.Index(s, open)
if i < 0 {
return ""
}
rest := s[i+len(open):]
j := strings.Index(rest, close)
if j < 0 {
return ""
}
return rest[:j]
}