package zitcli
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// The zit contract canary. Every text-output verb this package parses,
// exercised against a scratch store, each subtest named for its verb —
// so the NEXT zit upgrade that changes an output shape fails loudly
// here, naming the seam, instead of going quietly wrong in production
// the way 0.2.0 (clone auto-open) and 0.3.0 (ls, leaves, pull wording,
// status lead-in) did. Assertions are deliberately shallow: "the
// parser still extracts something sane", never exact wording.
func TestZitContract(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
sh := func(args ...string) string {
t.Helper()
cmd := exec.Command("zit", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("zit %v: %v\n%s", args, err, out)
}
return string(out)
}
sh("init")
if err := os.WriteFile(filepath.Join(dir, "plain.txt"), []byte("plain body\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "release notes.md"), []byte("notes\n"), 0o644); err != nil {
t.Fatal(err)
}
sh("add", "plain.txt", "release notes.md")
sh("commit", "-m", "base")
if err := os.WriteFile(filepath.Join(dir, "plain.txt"), []byte("plain body\ngrown\n"), 0o644); err != nil {
t.Fatal(err)
}
sh("commit", "-m", "grow", "--branch", "feat")
c := New(dir)
ctx := context.Background()
var tip string
t.Run("info", func(t *testing.T) {
hash, branch, err := c.Info(ctx, "feat")
if err != nil || len(hash) != 64 || !isHex(hash) {
t.Fatalf("Info: hash %q err %v", hash, err)
}
if branch != "feat" {
t.Fatalf("Info branch = %q", branch)
}
tip = hash
})
t.Run("infobranch", func(t *testing.T) {
b, err := c.InfoBranch(ctx, "trunk")
if err != nil || b != "trunk" {
t.Fatalf("InfoBranch = %q, %v", b, err)
}
})
t.Run("lsnames", func(t *testing.T) {
got, err := c.LsNames(ctx, "trunk")
if err != nil {
t.Fatal(err)
}
names := map[string]bool{}
for _, e := range got {
names[e.Name] = true
}
if !names["plain.txt"] || !names["release notes.md"] {
t.Fatalf("names parse lost files: %v", names)
}
})
t.Run("ls-annotated", func(t *testing.T) {
got, err := c.Ls(ctx, "trunk")
if err != nil || len(got) == 0 {
t.Fatalf("Ls: %v, %v", got, err)
}
for _, e := range got {
if len(e.Blob) != 64 || !isHex(e.Blob) || e.Name == "" {
t.Fatalf("annotated entry lost its shape: %+v", e)
}
}
})
t.Run("cat", func(t *testing.T) {
b, err := c.Cat(ctx, "plain.txt", "trunk")
if err != nil || string(b) != "plain body\n" {
t.Fatalf("Cat = %q, %v", b, err)
}
})
t.Run("diff", func(t *testing.T) {
trunkHash, _, err := c.Info(ctx, "trunk")
if err != nil {
t.Fatal(err)
}
unified, brief, err := c.Diff(ctx, trunkHash, tip)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(unified, "+grown") {
t.Fatalf("unified lost the change:\n%s", unified)
}
if len(brief) == 0 || brief[0].Status == "" || brief[0].Name == "" {
t.Fatalf("brief parse lost its shape: %+v", brief)
}
})
t.Run("leaves", func(t *testing.T) {
set, err := c.Leaves(ctx)
if err != nil || len(set) == 0 {
t.Fatalf("Leaves: %v, %v", set, err)
}
for h := range set {
if len(h) != 64 || !isHex(h) {
t.Fatalf("leaf key is not a full hash: %q", h)
}
}
})
t.Run("wiki", func(t *testing.T) {
hash, err := c.WikiCommit("canary page", "dan", "text/x-markdown", []byte("body\n"))
if err != nil || len(hash) != 64 || !isHex(hash) {
t.Fatalf("WikiCommit = %q, %v", hash, err)
}
})
t.Run("ticket", func(t *testing.T) {
id, hash, err := c.TicketNew("dan", map[string]string{"title": "canary", "status": "Open"})
if err != nil || len(id) != 40 || len(hash) != 64 {
t.Fatalf("TicketNew = %q %q, %v", id, hash, err)
}
if _, err := c.TicketSet(id, "dan", map[string]string{"status": "Fixed"}); err != nil {
t.Fatalf("TicketSet: %v", err)
}
})
var landing string
t.Run("landing", func(t *testing.T) {
id, err := c.LandingOpen("dan", "feat", "trunk", "canary landing", "text/x-markdown")
if err != nil || len(id) != 64 {
t.Fatalf("LandingOpen = %q, %v", id, err)
}
landing = id
if _, err := c.LandingSay("comment", landing, "dan", "a remark", "text/x-markdown", ""); err != nil {
t.Fatalf("LandingSay: %v", err)
}
})
t.Run("tag", func(t *testing.T) {
if err := c.TagAdd("ci-pass", tip, "run-canary", false); err != nil {
t.Fatalf("TagAdd: %v", err)
}
})
t.Run("commitfile", func(t *testing.T) {
hash, err := CommitFile(filepath.Join(dir, ".zit"), "trunk", "canary-edit", "dan", "canary", "ci/canary.json", []byte("{}\n"))
if err != nil || len(hash) != 64 {
t.Fatalf("CommitFile = %q, %v", hash, err)
}
b, err := c.Cat(ctx, "ci/canary.json", "canary-edit")
if err != nil || string(b) != "{}\n" {
t.Fatalf("committed file unreadable: %q, %v", b, err)
}
})
t.Run("pull-wordings", func(t *testing.T) {
for out, want := range map[string]bool{
"already up to date": false,
"pull from http://x: already in sync": false,
"received 12 artifacts": true,
} {
got, err := pullChanged([]byte(out))
if err != nil || got != want {
t.Fatalf("pullChanged(%q) = %v, %v", out, got, err)
}
}
if _, err := pullChanged([]byte("some future wording")); err == nil {
t.Fatal("an unknown wording must be an error, never a guess")
}
})
}