package index
import (
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
)
// Fossil tags are a LOG, not a set: a later artifact can cancel an
// earlier one. TagsOf and the timeline's tag filter must agree on which
// tags are standing, or a chip on the check-in page links to a timeline
// that disagrees with the chip.
func TestTagsOfAndTimelineFilterAgreeOnStandingTags(t *testing.T) {
ix := openIndex(t)
a, b := strings.Repeat("a1", 32), strings.Repeat("b2", 32)
ci := func(hash, date string, tags ...artifact.Tag) Parsed {
return Parsed{Hash: hash, Art: &artifact.Artifact{
Type: artifact.Manifest, Date: date, User: "dan",
Comment: "c", Tags: tags}}
}
if err := ix.FoldNow([]Parsed{
ci(a, "2026-08-18T10:00:00",
artifact.Tag{Name: "sym-release", Target: "*"},
artifact.Tag{Name: "sym-version-1.0", Target: "*"}),
ci(b, "2026-08-19T10:00:00",
artifact.Tag{Name: "sym-doomed", Target: "*"}),
}); err != nil {
t.Fatal(err)
}
got, err := ix.TagsOf(a)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || got[0] != "release" || got[1] != "version-1.0" {
t.Fatalf("TagsOf = %v, want [release version-1.0] sorted", got)
}
// The filter finds exactly the check-in the chip sits on.
tl, _, err := ix.Timeline(TimelineFilter{Limit: 10, Tag: "release"})
if err != nil {
t.Fatal(err)
}
if len(tl) != 1 || tl[0].Hash != a {
t.Fatalf("tag filter returned %d rows, want just %.4s", len(tl), a)
}
// A tag nobody carries is an empty timeline, not an error and not
// the whole timeline — the failure mode that would matter most.
if tl, _, err := ix.Timeline(TimelineFilter{Limit: 10, Tag: "nonesuch"}); err != nil || len(tl) != 0 {
t.Fatalf("unknown tag returned %d rows (err %v)", len(tl), err)
}
// Cancel one. A later artifact carrying the minus sign is fossil's
// way of retracting a tag, and both readers must stop reporting it.
if err := ix.FoldNow([]Parsed{
{Hash: strings.Repeat("c3", 32), Art: &artifact.Artifact{
Type: artifact.Manifest, Date: "2026-08-20T10:00:00", User: "dan",
Comment: "untag", Tags: []artifact.Tag{{Name: "-sym-doomed", Target: b}}}},
}); err != nil {
t.Fatal(err)
}
got, err = ix.TagsOf(b)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("TagsOf still reports a cancelled tag: %v", got)
}
if tl, _, err := ix.Timeline(TimelineFilter{Limit: 10, Tag: "doomed"}); err != nil || len(tl) != 0 {
t.Errorf("the filter still finds a cancelled tag: %d rows (err %v)", len(tl), err)
}
}
// Fossil lets an author correct a check-in comment after the fact with
// a `comment` tag, and the correction is what fossil displays. quarry
// showed the superseded C card instead — not an omission but a wrong
// answer, and 697 fossil check-ins are in that state.
func TestAttachCIAppliesCommentCorrectionsAndFlags(t *testing.T) {
ix := openIndex(t)
a := strings.Repeat("a1", 32)
if err := ix.FoldNow([]Parsed{
{Hash: a, Art: &artifact.Artifact{
Type: artifact.Manifest, Date: "2026-08-18T10:00:00", User: "dan",
Comment: "fixs the thing"}},
{Hash: strings.Repeat("b2", 32), Art: &artifact.Artifact{
Type: artifact.Manifest, Date: "2026-08-19T10:00:00", User: "dan",
Comment: "amend", Tags: []artifact.Tag{
{Name: "+comment", Target: a, Value: "fixes the thing"},
{Name: "*hidden", Target: a},
}}},
}); err != nil {
t.Fatal(err)
}
e, _, ok, err := ix.CheckinByPrefix(a)
if err != nil || !ok {
t.Fatalf("checkin not found (err %v)", err)
}
// Undecorated, the stored C card is untouched: it is an artifact
// fact and the index must not rewrite it.
if e.Comment != "fixs the thing" {
t.Errorf("stored comment was rewritten to %q", e.Comment)
}
entries := []TimelineEntry{e}
if err := ix.AttachCI(entries); err != nil {
t.Fatal(err)
}
if entries[0].Comment != "fixes the thing" {
t.Errorf("shown comment = %q, want the correction", entries[0].Comment)
}
if entries[0].CommentOrig != "fixs the thing" {
t.Errorf("superseded comment = %q, want it kept", entries[0].CommentOrig)
}
if !entries[0].Hidden {
t.Error("a standing hidden tag was not reported")
}
if entries[0].Closed {
t.Error("closed was reported without a closed tag")
}
// A check-in nobody corrected must not claim it was corrected.
plain := []TimelineEntry{{Hash: strings.Repeat("b2", 32), Comment: "amend"}}
if err := ix.AttachCI(plain); err != nil {
t.Fatal(err)
}
if plain[0].CommentOrig != "" {
t.Errorf("uncorrected check-in reports an original of %q", plain[0].CommentOrig)
}
// The history is the whole log, with the superseded row marked.
hist, err := ix.TagHistory(a)
if err != nil {
t.Fatal(err)
}
if len(hist) != 2 {
t.Fatalf("history has %d rows, want 2: %+v", len(hist), hist)
}
for _, h := range hist {
if !h.Standing {
t.Errorf("row %+v should stand — nothing overruled it", h)
}
}
}
// A branch diff is the branch against its FORK POINT, which is not the
// same base a landing uses (the target's current tip). Getting this
// wrong shows the right file count against the wrong baseline.
func TestForkPointIsWhereTheBranchLeftItsParent(t *testing.T) {
ix := openIndex(t)
root, mid, br1, br2 := strings.Repeat("a1", 32), strings.Repeat("b2", 32),
strings.Repeat("c3", 32), strings.Repeat("d4", 32)
ci := func(hash, date string, tags []artifact.Tag, parents ...string) Parsed {
return Parsed{Hash: hash, Art: &artifact.Artifact{
Type: artifact.Manifest, Date: date, User: "dan",
Comment: "c", Parents: parents, Tags: tags}}
}
onto := func(name string) []artifact.Tag {
return []artifact.Tag{{Name: "*branch", Target: "*", Value: name},
{Name: "*sym-" + name, Target: "*"}}
}
if err := ix.FoldNow([]Parsed{
ci(root, "2026-08-01T10:00:00", nil),
ci(mid, "2026-08-02T10:00:00", nil, root),
ci(br1, "2026-08-03T10:00:00", onto("feature"), mid),
ci(br2, "2026-08-04T10:00:00", nil, br1),
}); err != nil {
t.Fatal(err)
}
// The fork is where the branch left trunk — mid, not the trunk tip
// and not the branch's own first check-in.
got, err := ix.ForkPoint("feature")
if err != nil {
t.Fatal(err)
}
if got != mid {
t.Errorf("fork point = %.4s, want %.4s", got, mid)
}
// The branch's own tip is its newest member, not the repository's.
tip, ok, err := ix.TipOf("feature")
if err != nil || !ok {
t.Fatalf("no tip for the branch (err %v)", err)
}
if tip.Tip != br2 {
t.Errorf("tip = %.4s, want %.4s", tip.Tip, br2)
}
// A line that forked from nothing has no fork point, and must say
// so rather than inventing one — the page uses this to decide
// whether to offer the link at all.
if got, err := ix.ForkPoint("trunk"); err != nil || got != "" {
t.Errorf("trunk fork point = %q (err %v), want empty", got, err)
}
if got, err := ix.ForkPoint("no-such-branch"); err != nil || got != "" {
t.Errorf("unknown branch = %q (err %v), want empty", got, err)
}
}
// TagTarget is the check-in a tag NAME stands on: the newest one
// carrying it, whether or not it propagates, and never one where it was
// cancelled. An archive of tag:v1.0 is built from it.
func TestTagTargetFindsTheNewestStandingTag(t *testing.T) {
ix := openIndex(t)
a, b, c := strings.Repeat("a1", 32), strings.Repeat("b2", 32), strings.Repeat("c3", 32)
if err := ix.FoldNow([]Parsed{
{Hash: a, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-18T10:00:00", User: "dan", Comment: "a",
Tags: []artifact.Tag{{Name: "+sym-v1.0", Target: "*"}}}},
{Hash: b, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-19T10:00:00", User: "dan", Comment: "b",
Tags: []artifact.Tag{{Name: "+sym-v1.0", Target: "*"}}}},
{Hash: c, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-20T10:00:00", User: "dan", Comment: "untag b",
Tags: []artifact.Tag{{Name: "-sym-v1.0", Target: b}}}},
}); err != nil {
t.Fatal(err)
}
got, ok, err := ix.TagTarget("v1.0")
if err != nil || !ok || got != a {
t.Fatalf("FAIL: TagTarget(v1.0) = %.8s %v %v, want %.8s: b's tag was cancelled", got, ok, err, a)
}
if _, ok, err := ix.TagTarget("nonesuch"); ok || err != nil {
t.Fatalf("FAIL: an unknown tag answered %v %v", ok, err)
}
t.Log("ok")
}
// TipBranches names the branches a check-in is the tip of, with whether
// each is closed: an archive of an open branch's tip is shared work, a
// closed one's is not.
func TestTipBranchesNamesTheBranchesACheckinTips(t *testing.T) {
ix := openIndex(t)
a, b, c := strings.Repeat("a1", 32), strings.Repeat("b2", 32), strings.Repeat("c3", 32)
if err := ix.FoldNow([]Parsed{
{Hash: a, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-18T10:00:00", User: "dan", Comment: "a",
Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "trunk"}, {Name: "*sym-trunk", Target: "*"}}}},
{Hash: b, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-19T10:00:00", User: "dan", Comment: "b",
Parents: []string{a}}},
{Hash: c, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-20T10:00:00", User: "dan", Comment: "c",
Parents: []string{a}, Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "old"}, {Name: "*sym-old", Target: "*"},
{Name: "-sym-trunk", Target: "*"}, {Name: "+closed", Target: "*"}}}},
}); err != nil {
t.Fatal(err)
}
got, err := ix.TipBranches(b)
if err != nil || len(got) != 1 || got[0].Name != "trunk" || got[0].Closed {
t.Fatalf("FAIL: TipBranches(b) = %+v %v, want open trunk", got, err)
}
got, err = ix.TipBranches(c)
if err != nil || len(got) != 1 || got[0].Name != "old" || !got[0].Closed {
t.Fatalf("FAIL: TipBranches(c) = %+v %v, want closed old", got, err)
}
if got, err := ix.TipBranches(a); err != nil || len(got) != 0 {
t.Fatalf("FAIL: TipBranches(a) = %+v %v, want none", got, err)
}
t.Log("ok")
}