Skip to content
quarry Artifact [c0687a74b8]
Login

Artifact [c0687a74b8]

Artifact c0687a74b80b7975b66cf108228bd13949e472760d70494f6160a26fff2d1827:


package index

import (
	"path/filepath"
	"testing"

	"craftdesign.group/quarry/internal/artifact"
)

func openIndex(t *testing.T) *Index {
	t.Helper()
	ix, err := Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	return ix
}

func attach(hash, name, target, src, date string) Parsed {
	return Parsed{Hash: hash, Art: &artifact.Artifact{
		Type: artifact.Attachment, Date: date, User: "dan",
		Attachment: &artifact.Attach{Name: name, Target: target, Source: src},
	}}
}

// Fossil models attachments as a log, not a set: the latest A card per
// filename wins, and one with no source is a deletion. Getting that
// wrong resurrects deleted files.
func TestAttachmentsResolveLikeFossil(t *testing.T) {
	ix := openIndex(t)
	tkt := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	if err := ix.FoldNow([]Parsed{
		attach("h1", "spec.pdf", tkt, "src1", "2026-01-01T00:00:00"),
		attach("h2", "spec.pdf", tkt, "src2", "2026-01-02T00:00:00"), // replaced
		attach("h3", "notes.txt", tkt, "src3", "2026-01-03T00:00:00"),
		attach("h4", "notes.txt", tkt, "", "2026-01-04T00:00:00"), // deleted
		attach("h5", "other.txt", "different-target", "src5", "2026-01-05T00:00:00"),
	}); err != nil {
		t.Fatal(err)
	}
	got, err := ix.AttachmentsFor(tkt)
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 1 {
		t.Fatalf("live attachments = %+v, want just the replaced spec.pdf", got)
	}
	if got[0].Name != "spec.pdf" || got[0].Src != "src2" {
		t.Fatalf("wrong revision won: %+v", got[0])
	}

	// A deletion followed by a re-attach brings the file back.
	if err := ix.FoldNow([]Parsed{
		attach("h6", "notes.txt", tkt, "src6", "2026-01-06T00:00:00"),
	}); err != nil {
		t.Fatal(err)
	}
	got, _ = ix.AttachmentsFor(tkt)
	if len(got) != 2 {
		t.Fatalf("re-attach did not restore the file: %+v", got)
	}

	// Every card is still counted — the census matches fossil's
	// attachment table, deletions included.
	n, err := ix.CountAttachments()
	if err != nil {
		t.Fatal(err)
	}
	if n != 6 {
		t.Fatalf("counted %d attachment artifacts, want 6", n)
	}
}

// Folds are idempotent: redelivery must not double-count.
func TestAttachmentFoldIsIdempotent(t *testing.T) {
	ix := openIndex(t)
	batch := []Parsed{attach("h1", "a.txt", "tgt", "src", "2026-01-01T00:00:00")}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	n, _ := ix.CountAttachments()
	if n != 1 {
		t.Fatalf("redelivery produced %d rows", n)
	}
}

// AttachmentName is the gate on the download route: it must resolve only
// blobs that are LIVE attachments. Anything looser turns the route into
// a read-any-artifact-by-hash endpoint, and a deleted file must stop
// being reachable by URL.
func TestAttachmentNameGatesDownloads(t *testing.T) {
	ix := openIndex(t)
	if err := ix.FoldNow([]Parsed{
		attach("h1", "spec.pdf", "tkt", "livesrc", "2026-01-01T00:00:00"),
		attach("h2", "gone.txt", "tkt", "deadsrc", "2026-01-02T00:00:00"),
		attach("h3", "gone.txt", "tkt", "", "2026-01-03T00:00:00"), // deletion
	}); err != nil {
		t.Fatal(err)
	}
	if name, ok, err := ix.AttachmentName("livesrc"); err != nil || !ok || name != "spec.pdf" {
		t.Fatalf("live attachment: name=%q ok=%v err=%v", name, ok, err)
	}
	// Deleted: the content artifact still exists in the store, but the
	// newest card for that filename is a deletion.
	if _, ok, err := ix.AttachmentName("deadsrc"); err != nil || ok {
		t.Fatalf("deleted attachment still downloadable: ok=%v err=%v", ok, err)
	}
	// An artifact that was never attached — a check-in manifest, a file
	// blob — must not be reachable.
	if _, ok, err := ix.AttachmentName("some-unrelated-artifact"); err != nil || ok {
		t.Fatalf("unattached artifact served: ok=%v err=%v", ok, err)
	}
}