package api
import (
"context"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// A check-in past largeDiffLines is answered by its file list and each
// file's counts, with no hunks computed; file= narrows zit's diff to
// that one path; unified=true is still the whole patch, for a script.
func TestALargeCheckinIsDiffedFileByFile(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
zrun(t, dir, "init")
zwrite(t, dir, "a.txt", "one\n")
zwrite(t, dir, "b.txt", "two\n")
zrun(t, dir, "add", "a.txt", "b.txt")
zrun(t, dir, "commit", "-m", "first", "--user", "u")
base := ztip(t, dir, "trunk")
zwrite(t, dir, "a.txt", "one\nmore a\n")
zwrite(t, dir, "b.txt", "TWO\n")
zrun(t, dir, "commit", "-m", "second", "--user", "u")
tip := ztip(t, dir, "trunk")
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
if err := ix.FoldNow([]index.Parsed{
{Hash: base, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-10-30T10:00:00", User: "u", Comment: "first"},
Changes: []string{"a.txt", "b.txt"}},
{Hash: tip, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-10-30T11:00:00", User: "u", Comment: "second", Parents: []string{base}},
Changes: []string{"a.txt", "b.txt"}, Lines: map[string]index.LineCount{"a.txt": {Added: 1}, "b.txt": {Added: 1, Removed: 1}}},
}); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
deps := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: zitcli.New(filepath.Join(dir, ".zit"))}})}
if err := RegisterAll(r, deps); err != nil {
t.Fatal(err)
}
defer func(n int) { largeDiffLines = n }(largeDiffLines)
largeDiffLines = 2
diffOf := func(in diffGetIn) diffGetOut {
t.Helper()
in.Repo, in.Hash = "test", tip
out, err := r.Invoke(context.Background(), "checkin.diff", in)
if err != nil {
t.Fatal(err)
}
return out.(diffGetOut)
}
whole := diffOf(diffGetIn{})
if !whole.Large || len(whole.Files) != 2 || len(whole.Structured) != 0 || whole.Counts["b.txt"].Removed != 1 || whole.TotalLines != 3 {
t.Errorf("FAIL: a large check-in answered large=%v files=%d hunks=%d counts=%v total=%d",
whole.Large, len(whole.Files), len(whole.Structured), whole.Counts, whole.TotalLines)
}
one := diffOf(diffGetIn{File: "b.txt"})
if one.Large || len(one.Structured) != 1 || one.Structured[0].Path != "b.txt" {
t.Errorf("FAIL: file=b.txt answered large=%v with %d files", one.Large, len(one.Structured))
}
// A large check-in's whole patch is not built into one answer: the
// op refuses and points at the streamed download (jva, ^3920).
for _, in := range []diffGetIn{{Unified: true}, {Unified: true, Raw: true}, {Raw: true}} {
in.Repo, in.Hash = "test", tip
if _, err := r.Invoke(context.Background(), "checkin.diff", in); err == nil || !strings.Contains(err.Error(), "/patch") {
t.Errorf("FAIL: a large check-in's whole unified diff answered (%v), not the pointer to /patch", err)
}
}
largeDiffLines = 20000
if small := diffOf(diffGetIn{}); small.Large || len(small.Structured) != 2 {
t.Errorf("FAIL: a small check-in answered large=%v with %d files", small.Large, len(small.Structured))
}
// A small one still answers its whole patch, raw: nothing parsed.
raw := diffOf(diffGetIn{Unified: true, Raw: true})
if len(raw.Structured) != 0 || !strings.Contains(raw.Unified, "a.txt") || !strings.Contains(raw.Unified, "b.txt") {
t.Errorf("FAIL: a small check-in's raw patch answered %d parsed files, %d bytes", len(raw.Structured), len(raw.Unified))
}
// The streamed download writes the same bytes, and names the check-in
// before it writes any.
var buf strings.Builder
named := ""
if err := deps.PatchTo(context.Background(), "test", tip, "", false, func(to string) {
if buf.Len() != 0 {
t.Error("FAIL: named after bytes were written")
}
named = to
}, &buf); err != nil {
t.Fatal(err)
}
if buf.String() != raw.Unified || named != tip {
t.Errorf("FAIL: the streamed patch (%d bytes, named %.12s) differs from checkin.diff's (%d bytes)", buf.Len(), named, len(raw.Unified))
}
// file.get reads at most maxFileGetBytes: past it the answer says so
// and carries no content (jva, ^3926).
defer func(n int) { maxFileGetBytes = n }(maxFileGetBytes)
maxFileGetBytes = 2
if out, err := r.Invoke(context.Background(), "file.get", fileGetIn{Repo: "test", Hash: tip, Path: "b.txt"}); err != nil {
t.Fatal(err)
} else if f := out.(fileGetOut); !f.TooLarge || f.Content != "" || f.Size != 2 {
t.Errorf("FAIL: a file past the read bound answered tooLarge=%v size=%d content=%q", f.TooLarge, f.Size, f.Content)
}
maxFileGetBytes = 16 << 20
// A range diff has no index count; it is sized by its file list.
defer func(n int) { largeDiffFiles = n }(largeDiffFiles)
largeDiffFiles = 1
if rng := diffOf(diffGetIn{From: base}); !rng.Large || len(rng.Files) != 2 || len(rng.Structured) != 0 {
t.Errorf("FAIL: a range past largeDiffFiles answered large=%v files=%d hunks=%d", rng.Large, len(rng.Files), len(rng.Structured))
}
largeDiffFiles = 500
if rng := diffOf(diffGetIn{From: base}); rng.Large || len(rng.Structured) != 2 {
t.Errorf("FAIL: a small range answered large=%v with %d files", rng.Large, len(rng.Structured))
}
}