Skip to content
quarry Artifact [2d538f6434]
Login

Artifact [2d538f6434]

Artifact 2d538f6434ee077ee5f19ce1b14f11a449b97ad53e5472a0ac22c59db169a5c7:


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, &current)
	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")
	}
}