Skip to content
quarry Artifact [1c9f8b34ba]
Login

Artifact [1c9f8b34ba]

Artifact 1c9f8b34ba1d0ca44b7a7cb0bb96c1f8ca38c1dfbdfe2d65fb0a1ea214b85029:


package api

import (
	"context"
	"craftdesign.group/quarry/internal/markup"
	"errors"
	"path/filepath"
	"strings"
	"testing"

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

// A forge numbers every ticket of a repository it owns, store and local
// together, oldest first; a ticket filed later takes the next number;
// "#2" opens the same ticket as its hex id; a mirror is never numbered
// (54c5175f option C).
func TestTicketsAreNumberedInCreationOrder(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "i.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	store := func(id, date, title string) index.Parsed {
		return index.Parsed{Hash: id, Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: id, Date: date, User: "dan",
			Fields: []artifact.Field{{Name: "title", Value: title}, {Name: "status", Value: "Open"}}}}
	}
	older := "aaaa000000000000000000000000000000000000000000000000000000000001"
	newer := "bbbb000000000000000000000000000000000000000000000000000000000002"
	if err := ix.FoldNow([]index.Parsed{store(newer, "2026-09-02T10:00:00", "second"), store(older, "2026-09-01T10:00:00", "first")}); 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() })
	mirrors := memMirrors{}
	_ = mirrors.SetMirror("upstream", "https://upstream.example/x", 3600)
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"q": {Index: ix}, "upstream": {Index: ix}}),
		Local: db, Idem: db, Numbers: db, Mirrors: mirrors}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "dan")

	filed, err := r.Invoke(ctx, "ticket.intake", ticketIntakeIn{Repo: "q", Title: "third"})
	if err != nil {
		t.Fatal(err)
	}
	third := filed.(ticketIntakeOut)
	if third.Number != 3 {
		t.Fatalf("FAIL: the new ticket is #%d, want 3 after the two older store tickets", third.Number)
	}
	list, err := r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "q", State: "all"})
	if err != nil {
		t.Fatal(err)
	}
	by := map[string]int{}
	for _, row := range list.(ticketListOut).Tickets {
		by[row.ID] = row.Number
	}
	if by[older] != 1 || by[newer] != 2 || by[third.ID] != 3 {
		t.Fatalf("FAIL: list numbers %v", by)
	}
	got, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "q", ID: "2"})
	if err != nil || got.(ticketGetOut).Ticket.ID != newer || got.(ticketGetOut).Number != 2 {
		t.Fatalf("FAIL: #2 opened %+v (%v)", got, err)
	}
	if loc, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "q", ID: "3"}); err != nil || loc.(ticketGetOut).Number != 3 {
		t.Fatalf("FAIL: #3 (a local ticket) did not open as itself: %v", err)
	}
	up, err := r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "upstream", State: "all"})
	if err != nil {
		t.Fatal(err)
	}
	for _, row := range up.(ticketListOut).Tickets {
		if row.Number != 0 {
			t.Fatalf("FAIL: a mirror's ticket was numbered #%d", row.Number)
		}
	}
	t.Log("ok: store tickets #1 #2 by creation, the new local ticket #3, #2 and #3 open by number, the mirror unnumbered")
}

// A number two forges both recorded names both tickets: ticket.named
// lists them, this forge's own first, and ticket.get refuses to pick.
// A number only one ticket carries opens it (54c5175f).
func TestASharedNumberOffersAChoice(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "i.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	tkt := func(id, date, title string, extra ...artifact.Field) index.Parsed {
		f := append([]artifact.Field{{Name: "title", Value: title}, {Name: "status", Value: "Open"}}, extra...)
		return index.Parsed{Hash: id, Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: id, Date: date, User: "dan", Fields: f}}
	}
	a := "aaaa000000000000000000000000000000000000000000000000000000000001"
	b := "bbbb000000000000000000000000000000000000000000000000000000000002"
	c := "cccc000000000000000000000000000000000000000000000000000000000003"
	if err := ix.FoldNow([]index.Parsed{
		tkt(a, "2026-09-01T10:00:00", "ours, first"),
		tkt(b, "2026-09-02T10:00:00", "ours, second"),
	}); 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() })
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"q": {Index: ix}}), Numbers: db}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "dan")
	// This forge numbers its own two first; then a ticket numbered 1 on
	// another forge is pushed here, after #1 is already taken.
	if _, err := r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "q", State: "all"}); err != nil {
		t.Fatal(err)
	}
	if err := ix.FoldNow([]index.Parsed{tkt(c, "2026-09-03T10:00:00", "numbered 1 by another forge", artifact.Field{Name: "number", Value: "1"})}); err != nil {
		t.Fatal(err)
	}
	named := func(n int) []string {
		out, err := r.Invoke(ctx, "ticket.named", ticketNamedIn{Repo: "q", Number: n})
		if err != nil {
			t.Fatal(err)
		}
		var ids []string
		for _, tk := range out.(ticketNamedOut).Tickets {
			ids = append(ids, tk.ID)
		}
		return ids
	}
	if got := named(1); len(got) != 2 || got[0] != a || got[1] != c {
		t.Fatalf("FAIL: #1 names %v, want ours then the other forge's", got)
	}
	if _, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "q", ID: "1"}); !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("FAIL: ticket.get #1 answered %v, want a refusal naming both", err)
	}
	if got := named(2); len(got) != 1 || got[0] != b {
		t.Fatalf("FAIL: #2 names %v", got)
	}
	if out, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "q", ID: "2"}); err != nil || out.(ticketGetOut).Ticket.ID != b {
		t.Fatalf("FAIL: #2 did not open ours (%v)", err)
	}
	t.Log("ok: #1 names ours and the other forge's, get refuses to pick; #2 opens ours")
}

// numberedIndex holds four store tickets, oldest first: two recorded by
// another forge as #3 and #8, two with no number at all.
func numberedIndex(t *testing.T) (*index.Index, []string) {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "i.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	ids := []string{
		"a100000000000000000000000000000000000000000000000000000000000001",
		"b200000000000000000000000000000000000000000000000000000000000002",
		"c300000000000000000000000000000000000000000000000000000000000003",
		"d400000000000000000000000000000000000000000000000000000000000004",
	}
	tk := func(id, date string, extra ...artifact.Field) index.Parsed {
		f := append([]artifact.Field{{Name: "title", Value: "t " + id[:4]}, {Name: "status", Value: "Open"}}, extra...)
		return index.Parsed{Hash: id, Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: id, Date: date, User: "dan", Fields: f}}
	}
	if err := ix.FoldNow([]index.Parsed{
		tk(ids[0], "2026-09-01T10:00:00"),
		tk(ids[1], "2026-09-02T10:00:00", artifact.Field{Name: "number", Value: "3"}),
		tk(ids[2], "2026-09-03T10:00:00"),
		tk(ids[3], "2026-09-04T10:00:00", artifact.Field{Name: "number", Value: "8"}),
	}); err != nil {
		t.Fatal(err)
	}
	return ix, ids
}

// A mirror numbers nothing but shows what its upstream recorded, and
// #N opens it; a ticket the upstream never numbered stays unnumbered.
func TestAMirrorShowsItsUpstreamsNumbers(t *testing.T) {
	ix, ids := numberedIndex(t)
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	mirrors := memMirrors{}
	_ = mirrors.SetMirror("m", "https://upstream.example/m", 3600)
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"m": {Index: ix}}), Numbers: db, Mirrors: mirrors}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "dan")
	list, err := r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "m", State: "all"})
	if err != nil {
		t.Fatal(err)
	}
	by := map[string]int{}
	for _, row := range list.(ticketListOut).Tickets {
		by[row.ID] = row.Number
	}
	if by[ids[1]] != 3 || by[ids[3]] != 8 || by[ids[0]] != 0 || by[ids[2]] != 0 {
		t.Fatalf("FAIL: mirror numbers %v, want the recorded 3 and 8 only", by)
	}
	if got, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "m", ID: "8"}); err != nil || got.(ticketGetOut).Ticket.ID != ids[3] {
		t.Fatalf("FAIL: #8 on the mirror did not open the recorded ticket (%v)", err)
	}
	if own, _ := db.TicketNumbers("m"); len(own) != 0 {
		t.Fatalf("FAIL: the mirror assigned numbers of its own: %v", own)
	}
	out, _ := markup.Render([]byte("see #3"), resolver{ix: ix, repo: "m"})
	if !strings.Contains(string(out), "/r/m/ticket/"+ids[1]) {
		t.Fatalf("FAIL: #3 in prose does not link the recorded ticket:\n%s", out)
	}
	t.Log("ok: the mirror shows #3 and #8 as recorded, opens #8, links #3, numbers nothing itself")
}

// A fork keeps the numbers its upstream recorded and numbers the rest
// after the highest, oldest first — never renumbering from 1.
func TestAForkKeepsRecordedNumbers(t *testing.T) {
	ix, ids := numberedIndex(t)
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"f": {Index: ix}}), Numbers: db}
	if err := RegisterAll(ops.NewRegistry(), d); err != nil {
		t.Fatal(err)
	}
	got := d.ticketNumbers("f", &Repo{Index: ix})
	want := map[string]int{ids[1]: 3, ids[3]: 8, ids[0]: 9, ids[2]: 10}
	for id, n := range want {
		if got[id] != n {
			t.Fatalf("FAIL: fork numbers %v, want %v", got, want)
		}
	}
	t.Log("ok: recorded #3 and #8 kept; the unnumbered two became #9 and #10, oldest first")
}

// After the first numbering pass, a ticket that arrives by push takes
// the next number whatever it carries — a recorded number cannot jump
// the sequence — and no caller can set a number through the API.
func TestAPushedTicketTakesTheNextNumber(t *testing.T) {
	ix, ids := numberedIndex(t)
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"f": {Index: ix}}), Numbers: db}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	repo := &Repo{Index: ix}
	d.ticketNumbers("f", repo) // the first pass: #3, #8, #9, #10
	pushed := "e500000000000000000000000000000000000000000000000000000000000005"
	plain := "f600000000000000000000000000000000000000000000000000000000000006"
	if err := ix.FoldNow([]index.Parsed{
		{Hash: pushed, Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: pushed, Date: "2026-09-05T10:00:00", User: "mallory",
			Fields: []artifact.Field{{Name: "title", Value: "squats 500"}, {Name: "status", Value: "Open"}, {Name: "number", Value: "500"}}}},
		{Hash: plain, Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: plain, Date: "2026-09-06T10:00:00", User: "dan",
			Fields: []artifact.Field{{Name: "title", Value: "filed offline"}, {Name: "status", Value: "Open"}}}},
	}); err != nil {
		t.Fatal(err)
	}
	got := d.ticketNumbers("f", repo)
	if got[pushed] != 11 || got[plain] != 12 || got[ids[3]] != 8 {
		t.Fatalf("FAIL: after the push %v, want the squatter at #11 and the offline ticket at #12", got)
	}
	_, err = r.Invoke(ops.WithUser(context.Background(), "dan"), "ticket.set",
		ticketSetIn{Repo: "f", ID: ids[0], Fields: map[string]string{"number": "7"}})
	if !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("FAIL: setting a number through the API answered %v", err)
	}
	t.Log("ok: a pushed number=500 is ignored (#11), an offline ticket is #12, and number cannot be set through ticket.set")
}