package api
import (
"context"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// Assignment is a tracker-defined column, so "assigned to me" is a
// filter over what the tickets themselves carry — and a ticket nobody
// holds is not somebody's queue.
func TestTicketAcrossMineIsWhatYouAreAssigned(t *testing.T) {
ix := wikiIndex(t)
fold := func(id, assignee, title string) {
fields := []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: title}}
if assignee != "" {
fields = append(fields, artifact.Field{Name: "assignee", Value: assignee})
}
if err := ix.FoldNow([]index.Parsed{{Hash: id[:4], Art: &artifact.Artifact{
Type: artifact.Ticket, TicketID: id, Date: "2026-08-19T09:00:00",
User: "eve", Fields: fields}}}); err != nil {
t.Fatal(err)
}
}
fold(strings.Repeat("a", 40), "dan", "mine")
fold(strings.Repeat("b", 40), "eve", "hers")
fold(strings.Repeat("c", 40), "", "nobody holds this")
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix),
Accounts: stubAccounts{grants: map[string]map[string]string{
"dan": {"test": "write"},
}}}); err != nil {
t.Fatal(err)
}
invoke := func(in ticketAcrossIn) ticketAcrossOut {
t.Helper()
got, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "ticket.across", in)
if err != nil {
t.Fatal(err)
}
return got.(ticketAcrossOut)
}
out := invoke(ticketAcrossIn{Mine: true})
if out.Count != 1 || out.Tickets[0].Title != "mine" {
var titles []string
for _, tk := range out.Tickets {
titles = append(titles, tk.Title)
}
t.Fatalf("mine = %v, want only the one assigned to dan", titles)
}
// The header describes the rows beneath it, as the awaiting view's does.
if out.Open != 1 || out.Closed != 0 {
t.Errorf("counts = %d/%d, want the listing's own size", out.Open, out.Closed)
}
// Unassigned is not "mine": an empty assignee is a real value, and
// without AssigneeSet the filter would have matched every ticket.
if all := invoke(ticketAcrossIn{}); all.Count != 3 {
t.Errorf("without mine the listing holds %d, want all 3", all.Count)
}
}
// countingNumbers counts how often a tracker's numbers are read.
type countingNumbers struct{ reads int }
func (c *countingNumbers) TicketNumbersAssign(string, []string) error { return nil }
func (c *countingNumbers) TicketNumbers(string) (map[string]int, error) {
c.reads++
return map[string]int{}, nil
}
func (c *countingNumbers) TicketNumbered(string, int) (string, bool, error) { return "", false, nil }
func (c *countingNumbers) TicketNumberRekey(string, string, string) error { return nil }
func (c *countingNumbers) TicketNumberClaim(string, string, int) (bool, error) {
return false, nil
}
// A tracker with nothing to show in the listing is not numbered: the
// numbers are a scan of the whole tracker, and the dashboard's band asks
// every tracker the caller reaches, most of which have nothing for it.
func TestTicketAcrossNumbersOnlyTrackersWithRows(t *testing.T) {
ix := wikiIndex(t)
if err := ix.FoldNow([]index.Parsed{{Hash: "aaaa", Art: &artifact.Artifact{
Type: artifact.Ticket, TicketID: strings.Repeat("a", 40), Date: "2026-08-19T09:00:00",
User: "eve", Fields: []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: "eve's"}}}}}); err != nil {
t.Fatal(err)
}
nums := &countingNumbers{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix), Numbers: nums,
Accounts: stubAccounts{grants: map[string]map[string]string{"dan": {"test": "write"}}}}); err != nil {
t.Fatal(err)
}
// dan filed nothing, so nothing awaits him.
if out, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "ticket.across", ticketAcrossIn{Awaiting: true, Limit: 6}); err != nil || out.(ticketAcrossOut).Count != 0 {
t.Fatalf("awaiting: %+v %v", out, err)
}
if nums.reads != 0 {
t.Errorf("FAIL: a tracker with nothing to show was numbered %d times", nums.reads)
}
// The full listing shows eve's ticket, so that tracker is numbered.
if out, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "ticket.across", ticketAcrossIn{}); err != nil || out.(ticketAcrossOut).Count != 1 {
t.Fatalf("listing: %+v %v", out, err)
}
if nums.reads == 0 {
t.Errorf("FAIL: the tracker with a row was not numbered")
}
}
// Yours is what the caller has a stake in: assigned to them, or filed by
// them or by an agent acting for them — one they operate, or a forge
// agent they answer for. Somebody else's ticket in the same tracker is
// not theirs, which on a forge of mirrors is nearly every ticket.
func TestTicketAcrossYoursIsYouAndYourAgents(t *testing.T) {
ix := wikiIndex(t)
fold := func(id, by, assignee, title string) {
fields := []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: title}}
if assignee != "" {
fields = append(fields, artifact.Field{Name: "assignee", Value: assignee})
}
if err := ix.FoldNow([]index.Parsed{{Hash: id[:4], Art: &artifact.Artifact{
Type: artifact.Ticket, TicketID: id, Date: "2026-08-19T09:00:00",
User: by, Fields: fields}}}); err != nil {
t.Fatal(err)
}
}
fold(strings.Repeat("a", 40), "eve", "dmestas", "assigned to me")
fold(strings.Repeat("b", 40), "dmestas", "", "filed by me")
fold(strings.Repeat("c", 40), "bot", "", "filed by my agent")
fold(strings.Repeat("d", 40), "zeitgeist", "", "filed by the forge agent I answer for")
fold(strings.Repeat("e", 40), "eve", "eve", "somebody else's")
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
for _, who := range []string{"dmestas", "eve"} {
if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
t.Fatal(err)
}
}
if err := db.NewAgent("bot", "dmestas", -1); err != nil {
t.Fatal(err)
}
if err := db.NewForgeAgent("zeitgeist", "dmestas"); err != nil {
t.Fatal(err)
}
if err := db.Grant("dmestas", "test", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix), Teams: db, Accounts: acctAdapter{db},
CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(ops.WithUser(context.Background(), "dmestas"), "ticket.across",
ticketAcrossIn{Yours: true, Brief: true, State: "open"})
if err != nil {
t.Fatal(err)
}
titles := map[string]bool{}
for _, tk := range got.(ticketAcrossOut).Tickets {
titles[tk.Title] = true
}
for _, want := range []string{"assigned to me", "filed by me", "filed by my agent", "filed by the forge agent I answer for"} {
if !titles[want] {
t.Errorf("FAIL: yours is missing %q", want)
}
}
if titles["somebody else's"] || len(titles) != 4 {
t.Errorf("FAIL: yours holds %d tickets, somebody else's among them: %v", len(titles), titles)
}
}