Artifact f6087ef0a3718fe5b0d6ac2c3f5be66e5653ea0191a28f3244d7a52b0a5f5a3b:
- File
internal/zitcli/contract_test.go
— part of check-in
[00e90b88cc]
at
2026-08-31 13:25:43
on branch trunk
— leaf set keys on zit's abbreviation, not one subprocess per leaf
Leaves expanded every twelve-character hash zit printed into a full one with a zit info call each: 473 leaves on sqlite meant 357 spawns and 6.1s in a single page-load window. The one consumer asks a membership question, which a twelve-hex-character key answers. Contract test now pins the prefix contract and that a full hash finds its leaf. (user: quarry, size: 6156) [annotate] [blame] [check-ins using]
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) } // Keys are zit's own twelve-character abbreviation, NOT full // hashes. Expanding them cost one `zit info` subprocess per // leaf — 473 of them on sqlite, 6.1 seconds in one page-load // window — and the only consumer asks a membership question // that a prefix answers. LeafKey is how a caller looks one up. for h := range set { if len(h) != leafKeyWidth || !isHex(h) { t.Fatalf("leaf key is not zit's abbreviation: %q", h) } } // And a full hash from the same store must find its leaf. tip, _, err := c.Info(ctx, "trunk") if err != nil || tip == "" { t.Fatalf("Info(trunk): %q %v", tip, err) } if !set[LeafKey(tip)] { t.Errorf("the trunk tip %q is not in the leaf set via LeafKey", tip) } }) 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") } }) }