Skip to content
quarry Artifact [639f1ccf7e]
Login

Artifact [639f1ccf7e]

Artifact 639f1ccf7ee8aa926533243eb40fd97a0a20ff05a1b80df0172674b7860deee7:


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)
	}
}

// The A card's own artifact carries an N card, and fossil's attachment
// page reports the file's type from it. Folding it is what lets a reader
// be offered an inline image instead of a download.
func TestAttachmentMimetypeIsFolded(t *testing.T) {
	ix := openIndex(t)
	withType := func(hash, name, target, src, date, mime string) Parsed {
		p := attach(hash, name, target, src, date)
		p.Art.Mimetype = mime
		return p
	}
	if err := ix.FoldNow([]Parsed{
		withType("h1", "shot.png", "tkt", "src1", "2026-01-01T00:00:00", "image/png"),
		attach("h2", "notes.txt", "tkt", "src2", "2026-01-02T00:00:00"),
	}); err != nil {
		t.Fatal(err)
	}
	got, err := ix.AttachmentsFor("tkt")
	if err != nil {
		t.Fatal(err)
	}
	byName := map[string]Attachment{}
	for _, a := range got {
		byName[a.Name] = a
	}
	if byName["shot.png"].Mimetype != "image/png" {
		t.Fatalf("FAIL: mimetype not folded: %+v", byName["shot.png"])
	}
	if byName["notes.txt"].Mimetype != "" {
		t.Fatalf("FAIL: absent N card invented a type: %+v", byName["notes.txt"])
	}
	t.Log("ok: N card folded onto the attachment row")
}

// The head memo is a cache over immutable bytes: a miss answers "not
// known" rather than failing, a write is readable, and Reset drops it
// with every other derived table.
func TestAttachmentHeadMemo(t *testing.T) {
	ix := openIndex(t)
	if _, ok, err := ix.AttachmentHeadFor("blob1"); err != nil || ok {
		t.Fatalf("FAIL: a cold memo answered ok=%v err=%v", ok, err)
	}
	want := AttachmentHead{Size: 4096, Kind: "text", Lines: 8,
		Truncated: true, Text: "one\ntwo"}
	if err := ix.PutAttachmentHead("blob1", want); err != nil {
		t.Fatal(err)
	}
	got, ok, err := ix.AttachmentHeadFor("blob1")
	if err != nil || !ok {
		t.Fatalf("FAIL: ok=%v err=%v", ok, err)
	}
	if got != want {
		t.Fatalf("FAIL: memo = %+v, want %+v", got, want)
	}
	// A wider prefix or a new signature must be able to overwrite an
	// older verdict without a migration.
	want.Truncated, want.Lines = false, 12
	if err := ix.PutAttachmentHead("blob1", want); err != nil {
		t.Fatal(err)
	}
	if got, _, _ := ix.AttachmentHeadFor("blob1"); got != want {
		t.Fatalf("FAIL: the verdict did not overwrite: %+v", got)
	}
	if err := ix.Reset(); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := ix.AttachmentHeadFor("blob1"); ok {
		t.Fatal("FAIL: the memo survived a reset — it is derived, and reset drops derived tables")
	}
	t.Log("ok: the head memo caches a verdict, overwrites it, and is dropped on reset")
}