Skip to content
quarry Artifact [47c9468327]
Login

Artifact [47c9468327]

Artifact 47c9468327a33ad719ce561fa5fc4772a4039699cc77422f871705af87232773:


package api

import (
	"context"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/chat"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/oltp"
)

// A robot row stored before lines named things by number renders the
// landing reference hook with the landing's number; a person's line
// that happens to say "landing <hash>" is left exactly as written.
func TestOldRobotLinesRenderAsNumberedRefs(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "r.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	lid := strings.Repeat("d4", 20)
	if err := ix.FoldNow([]index.Parsed{
		{Hash: lid, Art: &artifact.Artifact{Type: artifact.Forum, ThreadTitle: "Land it", Date: "2026-08-18T09:00:00", User: "dan",
			Body: []byte("zit-landing: 1\nbranch: b\ntarget: trunk\n\nmessage\n")}},
	}); err != nil {
		t.Fatal(err)
	}
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	if err := db.LandingNumbersAssign("demo", []string{lid}); err != nil {
		t.Fatal(err)
	}
	ll := func(_ string, n int) (string, bool) { return lid, n == 1 }
	ln := func(_, id string) int {
		if id == lid {
			return 1
		}
		return 0
	}
	prevL, prevN := landingByNumber.Swap(&ll), landingNumberOfRef.Swap(&ln)
	t.Cleanup(func() { landingByNumber.Store(prevL); landingNumberOfRef.Store(prevN) })
	d := Deps{LandingNumbers: db, CanRead: func(context.Context, string) bool { return true },
		CapsFor: func(_, _ string) CapSet { return fakeCaps(anyCapLetters) }}
	repo := &Repo{Index: ix}
	render := func(kind, text string) chatMsgOut {
		return d.chatRenderAll(context.Background(), repo, "demo", []chat.Message{{Seq: 1, Kind: kind, User: "robot", Text: text}})[0]
	}
	const hook = `data-ref-kind="landing"`
	typed := render(chat.KindMsg, "!1 opened by zeitgeist")
	if !strings.Contains(typed.HTML, hook) {
		t.Fatalf("typed !1 has no landing hook: %s", typed.HTML)
	}
	old := render(chat.KindRobot, "landing "+lid[:12]+" opened by zeitgeist")
	if old.HTML != typed.HTML {
		t.Errorf("old robot row differs from the typed reference:\n%s\n%s", old.HTML, typed.HTML)
	}
	if len(old.Refs) != 1 {
		t.Errorf("refs = %v, want the one landing", old.Refs)
	}
	unknown := render(chat.KindRobot, "landing "+strings.Repeat("0", 12)+" opened by zeitgeist")
	if strings.Contains(unknown.HTML, hook) {
		t.Errorf("an unknown hash became a reference: %s", unknown.HTML)
	}
	human := render(chat.KindMsg, "landing "+lid[:12]+" opened by zeitgeist")
	if strings.Contains(human.HTML, hook) {
		t.Errorf("a human line was rewritten: %s", human.HTML)
	}
}

type countLandings struct {
	LandingNumberStore
	n atomic.Int32
}

func (c *countLandings) LandingNumbers(repo string) (map[string]int, error) {
	c.n.Add(1)
	return c.LandingNumberStore.LandingNumbers(repo)
}

type countTickets struct {
	TicketNumberStore
	n atomic.Int32
}

func (c *countTickets) TicketNumbers(repo string) (map[string]int, error) {
	c.n.Add(1)
	return c.TicketNumberStore.TicketNumbers(repo)
}

// A page of old robot rows loads each repository's numbers once per
// noun, not once per row.
func TestOldRobotRowsLoadNumbersOncePerRepoAndNoun(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "r.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	lid, tid := strings.Repeat("d4", 20), strings.Repeat("a1", 20)
	if err := ix.FoldNow([]index.Parsed{
		{Hash: lid, Art: &artifact.Artifact{Type: artifact.Forum, ThreadTitle: "Land it", Date: "2026-08-18T09:00:00", User: "dan",
			Body: []byte("zit-landing: 1\nbranch: b\ntarget: trunk\n\nmessage\n")}},
		{Hash: strings.Repeat("c1", 20), Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid,
			Date: "2026-08-15T10:00:00", User: "dan", Fields: []artifact.Field{{Name: "title", Value: "one"}}}},
	}); err != nil {
		t.Fatal(err)
	}
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	if err := db.LandingNumbersAssign("demo", []string{lid}); err != nil {
		t.Fatal(err)
	}
	repo := &Repo{Index: ix}
	// Numbered first, so the counts below are reads and not a backfill.
	Deps{Numbers: db}.ticketNumbers("demo", repo)
	run := func(rows int) (landings, tickets int32) {
		cl, ct := &countLandings{LandingNumberStore: db}, &countTickets{TicketNumberStore: db}
		d := Deps{LandingNumbers: cl, Numbers: ct}
		var msgs []chat.Message
		for i := 0; i < rows; i++ {
			msgs = append(msgs,
				chat.Message{Seq: uint64(2*i + 1), Kind: chat.KindRobot, User: "robot", Text: "landing " + lid[:12] + " opened"},
				chat.Message{Seq: uint64(2*i + 2), Kind: chat.KindRobot, User: "robot", Text: "ticket " + tid[:12] + " closed"})
		}
		d.chatRenderAll(context.Background(), repo, "demo", msgs)
		return cl.n.Load(), ct.n.Load()
	}
	l1, t1 := run(1)
	l5, t5 := run(5)
	if l1 != 1 || l5 != 1 {
		t.Errorf("landing map loads = %d for one row, %d for five, want 1 and 1", l1, l5)
	}
	if t1 == 0 || t5 != t1 {
		t.Errorf("ticket map loads = %d for one row, %d for five, want equal and nonzero", t1, t5)
	}
}

// A mirror numbers nothing itself but shows what its upstream recorded;
// an old robot row on one resolves through that, as a typed #N does.
func TestOldRobotRowOnAMirrorResolvesTheRecordedTicketNumber(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "r.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	tid := strings.Repeat("a1", 20)
	if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat("c1", 20), Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid,
		Date: "2026-08-15T10:00:00", User: "dan", Fields: []artifact.Field{{Name: "number", Value: "7"}}}}}); err != nil {
		t.Fatal(err)
	}
	d := Deps{}
	got := d.robotNumbered(&robotNums{}, &Repo{Index: ix}, "mirror", "ticket "+tid[:12]+" closed")
	if !strings.HasPrefix(got, "#7 ") {
		t.Fatalf("robotNumbered = %q, want the recorded number as #7", got)
	}
}