package main
import (
"context"
"os/exec"
"path/filepath"
"sync/atomic"
"testing"
"time"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ingest"
"craftdesign.group/quarry/internal/work"
"craftdesign.group/quarry/internal/zitcli"
)
// A check-in folded without its changed files is retried on the poll's
// timer even when nobody writes to the store again, once its backoff
// has passed and not before: a quiet repository would otherwise say its
// index is behind until the next push.
func TestAPendingCheckinIsRetriedOnAQuietRepository(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
store := t.TempDir()
if out, err := exec.Command("zit", "init", store).CombinedOutput(); err != nil {
t.Fatalf("zit init: %v\n%s", err, out)
}
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
const hash = "c0ffee0000000000000000000000000000000000000000000000000000000000"
if _, err := ix.Fold([]index.Parsed{{Hash: hash, ChangesPending: true,
Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-11-14T00:00:00", User: "ana"}}}); err != nil {
t.Fatal(err)
}
// Tried once already, next due in an hour.
if err := ix.TriedPending(hash, "refused", time.Now().Add(time.Hour)); err != nil {
t.Fatal(err)
}
rg := newTestRegistrar(t, 25)
rg.ingestLane, rg.interval = work.NewLane("ingest", 1, 0), 20*time.Millisecond
var rounds atomic.Int64
sync := func(admit ingest.Admit) (int, error) {
return 0, admit(func(func()) error {
rounds.Add(1)
// What the pipeline's retry does with a due row.
if due, _ := ix.PendingDue(time.Now()); due {
return ix.ResolveChanges(hash, []string{"a.txt"}, nil)
}
return nil
})
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
var indexing atomic.Bool
var current atomic.Int64
go rg.poll(ctx, done, "quiet", store, zitcli.New(store), ix, sync, &indexing, ¤t)
t.Cleanup(func() { cancel(); <-done })
// The first ingest and the first tick run; then the store is quiet
// and the row is not due: no more rounds.
time.Sleep(300 * time.Millisecond)
settled := rounds.Load()
time.Sleep(300 * time.Millisecond)
if n := rounds.Load(); n != settled {
t.Errorf("FAIL: %d rounds on a quiet store whose pending row is not due", n-settled)
}
// Due now: the timer runs a round, which completes it.
if err := ix.TriedPending(hash, "refused", time.Now()); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(5 * time.Second)
for {
pending, err := ix.ChangesPending()
if err != nil {
t.Fatal(err)
}
if !pending {
break
}
if time.Now().After(deadline) {
t.Fatalf("FAIL: the due row was never retried on a quiet store (%d rounds)", rounds.Load())
}
time.Sleep(20 * time.Millisecond)
}
if !t.Failed() {
t.Log("ok")
}
}