Artifact 856d1394d525c852180610db24d12ac5878e6c5dbd2ba207c93acf7b527f77f0:
- File
internal/zitcli/zitcli_test.go
— part of check-in
[35bd00468a]
at
2026-08-15 09:16:51
on branch trunk
— zitcli: subprocess client for zit frames (zit 0.1.3)
Bootstrap/incremental/invalid-cursor handling per exit codes 0/3/4; rfc 0003 ledger notes zit#66/#67 shipped. (user: dmestas, size: 3842) [annotate] [blame] [check-ins using]
package zitcli import ( "errors" "os" "os/exec" "path/filepath" "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") } }