package api
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
// filterIndex folds three tickets. Two carry declared vocabulary words;
// the third carries a status the vocabulary does NOT list, which is the
// case that matters — the forge accepts such an artifact over the wire
// on purpose, so a filter has to be able to find it.
func filterIndex(t *testing.T) *index.Index {
t.Helper()
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
tk := func(hash, id, status string) index.Parsed {
return index.Parsed{Hash: hash, Art: &artifact.Artifact{
Type: artifact.Ticket, TicketID: id, Date: "2026-01-01T00:00:00", User: "dan",
Fields: []artifact.Field{{Name: "title", Value: "t " + id[:4]},
{Name: "status", Value: status}, {Name: "severity", Value: "Minor"}},
}}
}
if err := ix.FoldNow([]index.Parsed{
tk("k1", strings.Repeat("a1", 20), "Open"),
tk("k2", strings.Repeat("b2", 20), "Closed"),
tk("k3", strings.Repeat("c3", 20), "Ratified_Upstream"), // arrived over the wire
}); err != nil {
t.Fatal(err)
}
return ix
}
func filterRegistry(t *testing.T) *ops.Registry {
t.Helper()
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(filterIndex(t))}); err != nil {
t.Fatal(err)
}
return r
}
func listTickets(t *testing.T, r *ops.Registry, in ticketListIn) (ticketListOut, error) {
t.Helper()
in.Repo = "test"
got, err := r.Invoke(context.Background(), "ticket.list", in)
if err != nil {
return ticketListOut{}, err
}
return got.(ticketListOut), nil
}
// A filter value that cannot match anything is refused, and the
// refusal names the words that would have worked.
//
// It used to answer 200 with an empty list, which is exactly what a
// tracker with no matching tickets answers. Indistinguishable from a
// correct answer (G-132).
func TestAnUnmatchableTicketFilterIsRefused(t *testing.T) {
r := filterRegistry(t)
_, err := listTickets(t, r, ticketListIn{Status: "zzz-not-a-value"})
if !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("FAIL: err = %v, want ErrInvalid", err)
}
for _, want := range []string{"zzz-not-a-value", "Open", "Closed"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("FAIL: the refusal does not mention %q: %v", want, err)
}
}
t.Logf("ok: %v", err)
}
// A status the tracker HOLDS but the vocabulary does not declare is
// still filterable, and is named in a refusal alongside the declared
// words.
//
// This is the half that keeps the refusal honest. The forge accepts an
// artifact carrying an undeclared status deliberately — a DAG holds
// history that predates any convention. Validating the filter against
// the vocabulary alone would refuse the only word that finds those
// tickets, turning a silent empty answer into a confident refusal of
// something real, which is worse.
func TestAValuePresentButUndeclaredStaysFilterable(t *testing.T) {
r := filterRegistry(t)
out, err := listTickets(t, r, ticketListIn{Status: "Ratified_Upstream", State: "all"})
if err != nil {
t.Fatalf("FAIL: a status sitting in the tracker was refused: %v", err)
}
if len(out.Tickets) != 1 {
t.Fatalf("FAIL: filter found %d tickets, want the 1 carrying that status", len(out.Tickets))
}
t.Log("ok: an undeclared status that exists is filterable")
_, err = listTickets(t, r, ticketListIn{Status: "zzz-not-a-value"})
if err == nil || !strings.Contains(err.Error(), "Ratified_Upstream") {
t.Errorf("FAIL: the refusal hides a value the tracker actually holds: %v", err)
} else {
t.Log("ok: the refusal names the undeclared value too")
}
}
// The words the vocabulary declares still pass, and a field with no
// declared words stays free text.
func TestTheDeclaredFilterValuesStillWork(t *testing.T) {
r := filterRegistry(t)
for _, in := range []ticketListIn{
{Status: "Open"},
{Severity: "Minor", State: "all"},
{Subsystem: "anything at all", State: "all"}, // free text, no vocabulary
{},
} {
if _, err := listTickets(t, r, in); err != nil {
t.Errorf("FAIL: %+v was refused: %v", in, err)
}
}
t.Log("ok: declared values and free-text fields are not narrowed")
}
// An unrecognised sort is refused rather than silently ignored. It used
// to be discarded and the default used, so a caller who misspelled a
// sort believed it had sorted and it had not.
func TestAnUnknownOrderIsRefusedRatherThanIgnored(t *testing.T) {
r := filterRegistry(t)
_, err := listTickets(t, r, ticketListIn{Order: "zzz-not-an-order"})
if !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("FAIL: err = %v, want ErrInvalid", err)
}
if !strings.Contains(err.Error(), "updated") {
t.Errorf("FAIL: the refusal does not name the sorts that work: %v", err)
}
t.Logf("ok: %v", err)
// Every key of the map the SQL comes from still works, so the
// refusal cannot drift from what the query supports.
for k := range index.TicketOrders {
if _, err := listTickets(t, r, ticketListIn{Order: k, State: "all"}); err != nil {
t.Errorf("FAIL: order %q is in TicketOrders but was refused: %v", k, err)
}
}
t.Log("ok: every key of index.TicketOrders is accepted")
}