Artifact f719a0b18707a8d2cafa259bdc501c404b70d53c368f853de9d6de91603b15b5:
- File
internal/zitcli/zitcli_test.go
— part of check-in
[bb05986ad8]
at
2026-08-24 09:41:16
on branch trunk
— landing page: probe the tip pair instead of scanning for the record
zit#134: landing show has to walk the whole store to FIND the record before it can answer about it, and quarry already folded that record out of its own index. zit merge --probe names the two check-ins, so the walk disappears -- 0.005s against 0.18s on this forge's own store, and the gap widens with store size, not with the number of landings.
Probing the same tips the cache is keyed on also closes a race: the key was built from resolved tips while the probe re-resolved a branch name, so a push landing between the two cached an answer under a key it did not describe.
An unresolvable tip now skips the probe instead of keying on ':' -- every landing whose branch could not be resolved shared that one cache entry and could be served another landing's verdict. (user: dmestas, size: 11189) [annotate] [blame] [check-ins using]
package zitcli import ( "errors" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" ) // testRepo builds a throwaway zit repository with n one-file commits and // returns its path. Requires the zit binary; skips when absent. func testRepo(t *testing.T, n int) string { t.Helper() if _, err := exec.LookPath("zit"); err != nil { t.Skip("zit not on PATH") } dir := t.TempDir() run(t, dir, "init") for i := 0; i < n; i++ { name := string(rune('a'+i)) + ".txt" if err := os.WriteFile(filepath.Join(dir, name), []byte(name+" body\n"), 0o644); err != nil { t.Fatal(err) } run(t, dir, "add", name) run(t, dir, "commit", "-m", "add "+name) } return dir } func run(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("zit", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("zit %v: %v\n%s", args, err, out) } } func TestFramesBootstrapAndIncremental(t *testing.T) { repo := testRepo(t, 2) z := New(repo) // Bootstrap: full census. batch, err := z.Frames(Cursor{}) if err != nil { t.Fatal(err) } if len(batch.Artifacts) == 0 { t.Fatal("bootstrap emitted nothing") } for _, a := range batch.Artifacts { if len(a.Hash) != 64 { t.Fatalf("bad hash %q", a.Hash) } if len(a.Bytes) == 0 { t.Fatalf("%s: empty artifact", a.Hash) } } // Current cursor: no new artifacts, cursor unchanged. again, err := z.Frames(batch.Cursor) if err != nil { t.Fatal(err) } if len(again.Artifacts) != 0 { t.Fatalf("current cursor re-emitted %d artifacts", len(again.Artifacts)) } if again.Cursor != batch.Cursor { t.Fatalf("cursor moved without appends: %v -> %v", batch.Cursor, again.Cursor) } // Append: only the new artifacts stream. if err := os.WriteFile(filepath.Join(repo, "new.txt"), []byte("new\n"), 0o644); err != nil { t.Fatal(err) } run(t, repo, "add", "new.txt") run(t, repo, "commit", "-m", "add new") delta, err := z.Frames(batch.Cursor) if err != nil { t.Fatal(err) } if len(delta.Artifacts) == 0 || len(delta.Artifacts) >= len(batch.Artifacts) { t.Fatalf("delta emitted %d of %d total", len(delta.Artifacts), len(batch.Artifacts)) } if delta.Cursor == batch.Cursor { t.Fatal("cursor did not advance past appends") } } func TestFramesCursorInvalidAfterStoreRewrite(t *testing.T) { repo := testRepo(t, 3) z := New(repo) batch, err := z.Frames(Cursor{}) if err != nil { t.Fatal(err) } // Shun a blob out of the store — the reclaim removes bytes from the // consumed prefix, so the cursor must die. (A no-op `pack` on an // already-optimal store rightly leaves cursors valid, which is why // this test rewrites via shun instead.) head, err := exec.Command("zit", "ls", "-R", repo, "tip").Output() if err != nil { t.Fatal(err) } blob := strings.Fields(string(head))[0] run(t, repo, "shun", blob, "--force") _, err = z.Frames(batch.Cursor) if !errors.Is(err, ErrCursorInvalid) { t.Fatalf("want ErrCursorInvalid after pack, got %v", err) } // Recovery: bootstrap from zero succeeds. if _, err := z.Frames(Cursor{}); err != nil { t.Fatalf("bootstrap after invalidation failed: %v", err) } } func TestFramesBytesMatchArtifactRaw(t *testing.T) { repo := testRepo(t, 1) z := New(repo) batch, err := z.Frames(Cursor{}) if err != nil { t.Fatal(err) } a := batch.Artifacts[0] cmd := exec.Command("zit", "artifact", a.Hash, "--raw") cmd.Dir = repo raw, err := cmd.Output() if err != nil { t.Fatal(err) } if string(raw) != string(a.Bytes) { t.Fatalf("frames bytes differ from artifact --raw for %s", a.Hash) } } func TestFramesMissingRepo(t *testing.T) { if _, err := exec.LookPath("zit"); err != nil { t.Skip("zit not on PATH") } z := New(t.TempDir()) if _, err := z.Frames(Cursor{}); err == nil { t.Fatal("want error for non-repository, got nil") } } func TestLs(t *testing.T) { repo := testRepo(t, 2) z := New(repo) files, err := z.Ls(t.Context(), "tip") if err != nil { t.Fatal(err) } if len(files) != 2 || files[0].Name != "a.txt" || files[1].Name != "b.txt" { t.Fatalf("files = %+v", files) } if len(files[0].Blob) != 64 { t.Fatalf("blob id = %q", files[0].Blob) } if _, err := z.Ls(t.Context(), "nope"); err == nil { t.Fatal("want error for bad version") } } func TestCat(t *testing.T) { repo := testRepo(t, 2) z := New(repo) got, err := z.Cat(t.Context(), "a.txt", "tip") if err != nil { t.Fatal(err) } if string(got) != "a.txt body\n" { t.Fatalf("content = %q", got) } if _, err := z.Cat(t.Context(), "missing.txt", "tip"); err == nil { t.Fatal("want error for missing file") } } func TestDiff(t *testing.T) { repo := testRepo(t, 1) commitFileZ(t, repo, "a.txt", "a.txt body\nplus a line\n", "grow a") commitFileZ(t, repo, "new.txt", "fresh\n", "add new") head, err := exec.Command("zit", "timeline", "-R", repo).Output() if err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(string(head)), "\n") first := strings.Fields(lines[len(lines)-1])[0] unified, brief, err := New(repo).Diff(t.Context(), first, "tip") if err != nil { t.Fatal(err) } if !strings.Contains(unified, "+plus a line") || !strings.Contains(unified, "+fresh") { t.Fatalf("unified missing changes:\n%s", unified) } if len(brief) != 2 || brief[0].Status != "M" || brief[1].Status != "A" { t.Fatalf("brief = %+v", brief) } } func commitFileZ(t *testing.T, repo, name, body, msg string) { t.Helper() if err := os.WriteFile(filepath.Join(repo, name), []byte(body), 0o644); err != nil { t.Fatal(err) } run(t, repo, "add", name) run(t, repo, "commit", "-m", msg) } // A listing may only be memoised under a version that cannot move. This // is the guard on a cache that would otherwise serve a stale file tree // forever after the next commit. func TestOnlyHashVersionsAreCacheable(t *testing.T) { for _, v := range []string{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } { if !immutableVersion(v) { t.Errorf("hash %q rejected as cacheable", v) } } for _, v := range []string{ "trunk", "tip", "", "release-1.2", "2026-08-17", "aaaaaaaa", // a prefix still resolves by search, not identity "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", // uppercase is not the stored form "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", } { if immutableVersion(v) { t.Errorf("%q accepted as cacheable", v) } } } // zit#131's probe is the ONLY way a forge may learn whether a landing merges: // discovering it by attempting an accept is the one thing a server must not // do, because on a server the attempt is the damage. So the parse of its // answer is load-bearing, and these are the three shapes it emits — captured // from real runs, not from the issue text. func TestConflictPathsReadsWhatZitEmits(t *testing.T) { // The probe, on stdout. got := conflictPaths("landing c8d4\nbranch feat -> trunk\nmerges conflict (2)\nconflict f.txt\nconflict k/g.txt\n") if want := []string{"f.txt", "k/g.txt"}; !reflect.DeepEqual(got, want) { t.Errorf("probe paths = %v, want %v", got, want) } // A refused accept, on stderr, where every line carries the verb's // own prefix and one line is prose rather than a path. got = conflictPaths("zit landing: 87ba does not merge cleanly into 'trunk'; nothing was written\n" + "zit landing: conflict f.txt\n" + "zit landing: merge the target into the branch from a checkout, resolve, and push\n") if want := []string{"f.txt"}; !reflect.DeepEqual(got, want) { t.Errorf("refusal paths = %v, want %v — the prose lines are not paths", got, want) } // A clean landing names nothing. if got := conflictPaths("merges clean\n"); len(got) != 0 { t.Errorf("clean merge reported conflicts: %v", got) } } // zit#134: the probe names the two tips, so it does not walk the store to find // a record quarry already folded. These assert the contract quarry depends on: // the answer parses, "has conflicts" is an ANSWER rather than an error, and // asking changes nothing. // writeZ drops a file into the checkout without committing it. func writeZ(t *testing.T, repo, name, body string) { t.Helper() if err := os.WriteFile(filepath.Join(repo, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } func TestMergeProbeNamesTheTips(t *testing.T) { repo := t.TempDir() run(t, repo, "init", ".") writeZ(t, repo, "f.txt", "top\nMID\nbottom\n") run(t, repo, "add", "f.txt") run(t, repo, "commit", "-m", "root", "--user", "u") // A branch whose edit is in a different region of the same file: a // three-way merge resolves it, a hash-level one would not. writeZ(t, repo, "f.txt", "top\nMID\nBOTTOM\n") run(t, repo, "commit", "-m", "b", "--branch", "feat/ok", "--user", "u") run(t, repo, "update", "trunk") writeZ(t, repo, "f.txt", "TOP\nMID\nbottom\n") run(t, repo, "commit", "-m", "t", "--user", "u") c := &Client{repo: repo} state, conflicts, err := c.MergeProbe("feat/ok", "trunk", "") if err != nil { t.Fatalf("MergeProbe: %v", err) } if state != "clean" { t.Fatalf("state = %q, want clean — a content-mergeable branch must not read as conflicted", state) } if len(conflicts) != 0 { t.Fatalf("conflicts = %v, want none", conflicts) } } func TestMergeProbeReportsConflictsAsAnAnswer(t *testing.T) { repo := t.TempDir() run(t, repo, "init", ".") writeZ(t, repo, "g.txt", "base\n") run(t, repo, "add", "g.txt") run(t, repo, "commit", "-m", "root", "--user", "u") writeZ(t, repo, "g.txt", "THEIRS\n") run(t, repo, "commit", "-m", "b", "--branch", "feat/bad", "--user", "u") run(t, repo, "update", "trunk") writeZ(t, repo, "g.txt", "OURS\n") run(t, repo, "commit", "-m", "t", "--user", "u") c := &Client{repo: repo} // Not an error: zit exits 0 and puts the verdict on stdout, precisely so a // forge can render "blocked" without treating it as a failure to report. state, conflicts, err := c.MergeProbe("feat/bad", "trunk", "") if err != nil { t.Fatalf("MergeProbe returned an error for a conflicting pair; conflicts are an answer: %v", err) } if state != "conflict" { t.Fatalf("state = %q, want conflict", state) } if len(conflicts) == 0 || conflicts[0] != "g.txt" { t.Fatalf("conflicts = %v, want [g.txt] — the forge has to name the files", conflicts) } } func TestMergeProbeCachesOnTheTipPair(t *testing.T) { repo := t.TempDir() run(t, repo, "init", ".") writeZ(t, repo, "f.txt", "a\n") run(t, repo, "add", "f.txt") run(t, repo, "commit", "-m", "root", "--user", "u") writeZ(t, repo, "f.txt", "b\n") run(t, repo, "commit", "-m", "b", "--branch", "feat", "--user", "u") run(t, repo, "update", "trunk") c := &Client{repo: repo} if _, _, err := c.MergeProbe("feat", "trunk", "tipA:tipB"); err != nil { t.Fatal(err) } // A second ask under the same key must not re-probe. Pointing the client // at a repository that does not exist proves it: a cache miss would fail. c.repo = filepath.Join(repo, "gone") state, _, err := c.MergeProbe("feat", "trunk", "tipA:tipB") if err != nil { t.Fatalf("second ask re-probed instead of using the cache: %v", err) } if state == "" { t.Fatal("cached answer came back empty") } }