Skip to content
quarry Artifact [5e150514fc]
Login

Artifact [5e150514fc]

Artifact 5e150514fcb7da9dd51b5f315579e3e545b9865b55dd8793d6b347bb618ac126:


package api

import (
	"context"
	"errors"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"

	"craftdesign.group/quarry/internal/ops"
)

func TestRegisterAllAndVersionOp(t *testing.T) {
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "1.2.3-test"}); err != nil {
		t.Fatal(err)
	}
	got, err := r.Invoke(context.Background(), "meta.version", versionIn{})
	if err != nil {
		t.Fatal(err)
	}
	if got.(versionOut).Version != "1.2.3-test" {
		t.Fatalf("got %+v", got)
	}
}

func TestRegisteredOpsAppearInSpec(t *testing.T) {
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v"}); err != nil {
		t.Fatal(err)
	}
	spec, err := ops.Spec(r, "quarry", "v")
	if err != nil {
		t.Fatal(err)
	}
	if spec.Paths["/api/v1/version"].Get == nil {
		t.Fatal("meta.version missing from generated spec")
	}
}

func wikiIndex(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() })
	batch := []index.Parsed{
		{Hash: "aaa1", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Design",
			Date: "2026-08-15T10:00:00", User: "dan", Mimetype: "text/x-markdown",
			Body: []byte("# Design\n\ndetails\n")}},
		{Hash: "aaa2", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Home",
			Date: "2026-08-15T10:00:01", User: "dan", Mimetype: "text/x-markdown",
			Body: []byte("See [[Design]] and [[Missing]].\n")}},
		{Hash: "aaa3", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Home",
			Date: "2026-08-15T11:00:00", User: "eve", Mimetype: "text/x-markdown",
			Body: []byte("Revised. See [[Design]].\n")}},
	}
	if err := ix.Fold(batch); err != nil {
		t.Fatal(err)
	}
	return ix
}

func TestWikiOps(t *testing.T) {
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Index: wikiIndex(t)}); err != nil {
		t.Fatal(err)
	}

	got, err := r.Invoke(context.Background(), "wiki.list", wikiListIn{})
	if err != nil {
		t.Fatal(err)
	}
	list := got.(wikiListOut)
	if list.Count != 2 || list.Pages[0].Title != "Design" || list.Pages[1].Title != "Home" {
		t.Fatalf("list = %+v", list)
	}
	if list.Pages[1].User != "eve" {
		t.Fatalf("latest edit not surfaced: %+v", list.Pages[1])
	}

	got, err = r.Invoke(context.Background(), "wiki.get", wikiGetIn{Name: "Home"})
	if err != nil {
		t.Fatal(err)
	}
	page := got.(wikiGetOut)
	if !strings.Contains(page.HTML, `<a href="/wiki/Design">Design</a>`) {
		t.Fatalf("existing page not resolved:\n%s", page.HTML)
	}
	if !strings.Contains(page.Source, "Revised") {
		t.Fatalf("stale edit served: %+v", page)
	}

	if _, err := r.Invoke(context.Background(), "wiki.get", wikiGetIn{Name: "Missing"}); err == nil {
		t.Fatal("want error for missing page")
	}
}

func TestWikiGetUnresolvedRefStaysText(t *testing.T) {
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Index: wikiIndex(t)}); err != nil {
		t.Fatal(err)
	}
	// The older Home revision links [[Missing]]; current revision does
	// not — fetch Design's referrer instead via a fresh page write.
	got, err := r.Invoke(context.Background(), "wiki.get", wikiGetIn{Name: "Design"})
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(got.(wikiGetOut).HTML, "/wiki/Missing") {
		t.Fatal("nonexistent page linked")
	}
}

func TestCheckinGetAndHashResolution(t *testing.T) {
	ix := wikiIndex(t)
	h1 := strings.Repeat("ab", 32)
	h2 := "abcd" + strings.Repeat("ef", 30)
	if err := ix.Fold([]index.Parsed{
		{Hash: h1, Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-08-15T09:00:00", User: "dan", Comment: "first"}},
		{Hash: h2, Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-08-15T09:30:00", User: "dan", Comment: "second", Parents: []string{h1}}},
		{Hash: "bbb1", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Refs",
			Date: "2026-08-15T12:00:00", User: "dan", Mimetype: "text/x-markdown",
			Body: []byte("Fixed in " + h2[:12] + ". Ambiguous: ab.\n")}},
	}); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Index: ix}); err != nil {
		t.Fatal(err)
	}

	// Prefix lookup: unique resolves, ambiguous does not.
	got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Hash: h2[:12]})
	if err != nil {
		t.Fatal(err)
	}
	ci := got.(checkinGetOut)
	if ci.Hash != h2 || len(ci.Parents) != 1 || ci.Parents[0] != h1 {
		t.Fatalf("checkin = %+v", ci)
	}
	if _, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Hash: "abcdzz"}); err == nil {
		t.Fatal("want error for unknown hash")
	}

	// Wiki body hash references resolve through the same lookup.
	got, err = r.Invoke(context.Background(), "wiki.get", wikiGetIn{Name: "Refs"})
	if err != nil {
		t.Fatal(err)
	}
	html := got.(wikiGetOut).HTML
	if !strings.Contains(html, `<a href="/checkin/`+h2+`">`+h2[:12]+`</a>`) {
		t.Fatalf("hash ref not resolved:\n%s", html)
	}
}

func TestTicketOps(t *testing.T) {
	ix := wikiIndex(t)
	tid := strings.Repeat("cd", 20)
	if err := ix.Fold([]index.Parsed{
		{Hash: "ccc1", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid,
			Date: "2026-08-15T09:00:00", User: "dan",
			Fields: []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: "crash on empty input"}}}},
		{Hash: "ccc2", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid,
			Date: "2026-08-15T10:00:00", User: "eve",
			Fields: []artifact.Field{{Name: "resolution", Value: "Fixed"}, {Name: "status", Value: "Closed"}}}},
	}); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Index: ix}); err != nil {
		t.Fatal(err)
	}

	got, err := r.Invoke(context.Background(), "ticket.list", ticketListIn{})
	if err != nil {
		t.Fatal(err)
	}
	list := got.(ticketListOut)
	if list.Count != 1 || list.Tickets[0].Status != "Closed" || list.Tickets[0].Title != "crash on empty input" {
		t.Fatalf("list = %+v", list)
	}

	got, err = r.Invoke(context.Background(), "ticket.get", ticketGetIn{ID: tid[:8]})
	if err != nil {
		t.Fatal(err)
	}
	tk := got.(ticketGetOut).Ticket
	if tk.ID != tid || len(tk.Changes) != 2 {
		t.Fatalf("ticket = %+v", tk)
	}
	if fieldValue(tk.Fields, "status") != "Closed" || fieldValue(tk.Fields, "resolution") != "Fixed" {
		t.Fatalf("fold wrong: %+v", tk.Fields)
	}
	if tk.Created != "2026-08-15T09:00:00" || tk.Updated != "2026-08-15T10:00:00" {
		t.Fatalf("created/updated: %+v", tk)
	}
}

func TestSearchOp(t *testing.T) {
	ix := wikiIndex(t)
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Index: ix}); err != nil {
		t.Fatal(err)
	}
	got, err := r.Invoke(context.Background(), "search.query", searchIn{Q: "Design"})
	if err != nil {
		t.Fatal(err)
	}
	res := got.(searchOut)
	if res.Count == 0 {
		t.Fatalf("no hits: %+v", res)
	}
}

func TestEntityNotFoundWrapsSentinel(t *testing.T) {
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Index: wikiIndex(t)}); err != nil {
		t.Fatal(err)
	}
	for name, in := range map[string]any{
		"wiki.get":     wikiGetIn{Name: "NoSuchPage"},
		"ticket.get":   ticketGetIn{ID: "deadbeef"},
		"checkin.get":  checkinGetIn{Hash: "deadbeefdeadbeef"},
		"checkin.diff": diffGetIn{Hash: "deadbeefdeadbeef"},
	} {
		_, err := r.Invoke(context.Background(), name, in)
		if !errors.Is(err, ops.ErrNotFound) {
			t.Errorf("%s: err %v does not wrap ErrNotFound", name, err)
		}
	}
}