package api
import (
"context"
"reflect"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
// A tracker's own listing carries the assignee. It was built from a
// second hand-written literal that omitted the field, so a board grouped
// by assignee drew one "unassigned" column over a tracker — tcl's — whose
// index held an assignee for every one of its 8,940 tickets.
func TestTicketListCarriesTheAssignee(t *testing.T) {
ix := wikiIndex(t)
if err := ix.FoldNow([]index.Parsed{{Hash: "asg1", Art: &artifact.Artifact{
Type: artifact.Ticket, TicketID: strings.Repeat("a", 40),
Date: "2026-09-16T09:00:00", User: "eve",
Fields: []artifact.Field{{Name: "status", Value: "Open"},
{Name: "title", Value: "held by dgp"}, {Name: "assignee", Value: "dgp"}}}}}); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil {
t.Fatal(err)
}
got, err := r.Invoke(context.Background(), "ticket.list", ticketListIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
out := got.(ticketListOut)
if len(out.Tickets) != 1 {
t.Fatalf("want 1 ticket, got %d", len(out.Tickets))
}
if out.Tickets[0].Assignee != "dgp" {
t.Fatalf("assignee = %q, want dgp — the listing dropped it", out.Tickets[0].Assignee)
}
}
// The two conversions must not drift apart again: every field summaryOf
// sets from an index summary is a field this listing carries, because it
// IS summaryOf. Compared as whole values rather than field by field, so a
// field added to one side and not the other fails here.
func TestTicketListUsesTheSharedConversion(t *testing.T) {
in := index.TicketSummary{
ID: "id", Title: "t", Status: "Open", Updated: "u", Created: "c",
Type: "Bug", Severity: "High", Priority: "Medium", Subsystem: "web",
Resolution: "None", Author: "eve", LastUser: "dan", Comments: 3,
Assignee: "dgp", Refs: 1,
}
want := summaryOf(in)
if want.Assignee != "dgp" {
t.Fatal("summaryOf itself does not carry the assignee")
}
// Every exported field summaryOf fills must be non-zero here, so a
// new field that summaryOf forgets shows up as a zero value.
v := reflect.ValueOf(want)
for i := 0; i < v.NumField(); i++ {
f := v.Type().Field(i)
if !f.IsExported() || f.Name == "Local" {
continue // Local is set by the local tier, not from the index
}
if v.Field(i).IsZero() {
t.Errorf("summaryOf leaves %s unset from a fully populated index summary", f.Name)
}
}
}