package api
import (
"context"
"errors"
"path/filepath"
"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"),
tkt(c, "2026-09-03T10:00:00", "numbered 1 by another forge", artifact.Field{Name: "number", Value: "1"}),
}); 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")
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")
}