Skip to content
quarry Artifact [1481f6f1a5]
Login

Artifact [1481f6f1a5]

Artifact 1481f6f1a50065ec10204704dc041c98e9697406879885c18c124c5e8c25525e:


package web

import (
	"fmt"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
)

func testRegistry(t *testing.T) *ops.Registry {
	t.Helper()
	// An empty (but real) index so entity lookups answer not-found
	// rather than "no store configured".
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "9.9.9-test", Repos: map[string]*api.Repo{"test": {Index: ix}}}); err != nil {
		t.Fatal(err)
	}
	return r
}

func TestHomePageRendersThroughVersionOp(t *testing.T) {
	rec := httptest.NewRecorder()
	Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
	if rec.Code != 200 {
		t.Fatalf("status %d", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "quarry 9.9.9-test") {
		t.Fatalf("footer missing op-served version:\n%s", body)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
		t.Fatalf("content-type %q", ct)
	}
	if strings.Contains(body, "<script") {
		t.Fatal("no-JS baseline violated: script tag on the base page")
	}
	if !strings.Contains(body, "prefers-color-scheme") {
		t.Fatal("dark theme missing")
	}
}

func TestUnknownPathIs404(t *testing.T) {
	rec := httptest.NewRecorder()
	Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/nope", nil))
	if rec.Code != 404 {
		t.Fatalf("status %d", rec.Code)
	}
}

func TestThemed404Page(t *testing.T) {
	for _, path := range []string{"/nope", "/r/test/wiki/NoSuchPage", "/r/test/checkin/deadbeefdead", "/r/test/ticket/deadbeef"} {
		rec := httptest.NewRecorder()
		Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
		if rec.Code != 404 {
			t.Errorf("%s: status %d", path, rec.Code)
			continue
		}
		body := rec.Body.String()
		if !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") {
			t.Errorf("%s: content-type %q — bare default 404?", path, rec.Header().Get("Content-Type"))
		}
		if !strings.Contains(body, "quarry 9.9.9-test") {
			t.Errorf("%s: 404 page missing version footer", path)
		}
		if !strings.Contains(body, `href="/"`) {
			t.Errorf("%s: 404 page has no way back into the site", path)
		}
	}
}

func TestPersistentNavOnEveryPage(t *testing.T) {
	// Ticket 010: every repo page carries that repo's nav and search
	// form; every page (fleet and 404 included) carries the site link.
	for _, path := range []string{"/r/test/timeline", "/r/test/wiki", "/r/test/ticket", "/r/test/search"} {
		rec := httptest.NewRecorder()
		Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
		body := rec.Body.String()
		for _, want := range []string{`<nav>`, `href="/r/test/timeline"`, `href="/r/test/wiki"`, `href="/r/test/ticket"`, `action="/r/test/search"`} {
			if !strings.Contains(body, want) {
				t.Errorf("%s: missing %s in header", path, want)
			}
		}
	}
	for _, path := range []string{"/", "/nope"} {
		rec := httptest.NewRecorder()
		Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
		if !strings.Contains(rec.Body.String(), `class="site" href="/"`) {
			t.Errorf("%s: missing site link", path)
		}
	}
}

func TestFleetPageListsRepos(t *testing.T) {
	rec := httptest.NewRecorder()
	Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
	if rec.Code != 200 {
		t.Fatalf("status %d", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), `href="/r/test/timeline"`) {
		t.Fatalf("fleet page missing repo link:\n%s", rec.Body.String())
	}
}

func TestTablesScrollInWrapper(t *testing.T) {
	// Ticket 120: wide tables scroll inside .tw, not the page.
	rec := httptest.NewRecorder()
	Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/r/test/timeline", nil))
	body := rec.Body.String()
	if strings.Contains(body, "<table>") && !strings.Contains(body, `<div class="tw"><table>`) {
		t.Fatalf("timeline table not wrapped:\n%s", body)
	}
}

func TestTimelineRendersAllRowsWithBranches(t *testing.T) {
	// Regression: a template field mismatch once truncated the page at
	// the first row — Execute's error was discarded. Render a POPULATED
	// timeline and assert every row, branch labels, and a complete page.
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	var batch []index.Parsed
	for i := 0; i < 5; i++ {
		h := strings.Repeat(fmt.Sprintf("%02d", i+1), 32)
		art := &artifact.Artifact{Type: artifact.Manifest, User: "dan",
			Date:    fmt.Sprintf("2026-08-15T0%d:00:00", i),
			Comment: fmt.Sprintf("commit %d", i)}
		if i == 0 {
			art.Tags = []artifact.Tag{{Name: "*branch", Target: "*", Value: "trunk"}}
		}
		if i > 0 {
			art.Parents = []string{strings.Repeat(fmt.Sprintf("%02d", i), 32)}
		}
		batch = append(batch, index.Parsed{Hash: h, Art: art})
	}
	if err := ix.Fold(batch); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "9.9.9-test",
		Repos: map[string]*api.Repo{"test": {Index: ix}}}); err != nil {
		t.Fatal(err)
	}
	rec := httptest.NewRecorder()
	Handler(r).ServeHTTP(rec, httptest.NewRequest("GET", "/r/test/timeline", nil))
	body := rec.Body.String()
	if got := strings.Count(body, `class="tl-card"`); got != 5 {
		t.Fatalf("rendered %d rows, want 5:\n%s", got, body)
	}
	if got := strings.Count(body, "<svg "); got != 10 { // head + tail per row
		t.Fatalf("rendered %d graph layers, want 10", got)
	}
	if got := strings.Count(body, `<svg class="tl-head`); got != 5 {
		t.Fatalf("rendered %d head layers, want 5", got)
	}
	if !strings.Contains(body, `<span class="tl-pill">2026-08-15</span>`) {
		t.Fatal("date pill missing")
	}
	if !strings.Contains(body, `href="/r/test/timeline?branch=trunk"><b>trunk</b></a>`) {
		t.Fatalf("branch label missing:\n%s", body)
	}
	if !strings.HasSuffix(strings.TrimSpace(body), "</html>") {
		t.Fatal("page truncated — template error swallowed")
	}
}

func TestTimelineGraphRails(t *testing.T) {
	// DAG:  m (merge of c2+b1) ← newest
	//       b1 (branch dev, parent c1)
	//       c2 (trunk, parent c1)
	//       c1 (trunk root)
	c1, c2, b1, m := strings.Repeat("aa", 32), strings.Repeat("bb", 32), strings.Repeat("cc", 32), strings.Repeat("dd", 32)
	entries := []timelineEntry{
		{Hash: m, Date: "2026-08-15T04:00:00", Branch: "trunk", Comment: "merge", IsMerge: true, Parents: []string{c2, b1}},
		{Hash: b1, Date: "2026-08-15T03:00:00", Branch: "dev", Comment: "dev work", Parents: []string{c1}},
		{Hash: c2, Date: "2026-08-15T02:00:00", Branch: "trunk", Comment: "trunk work", Parents: []string{c1}},
		{Hash: c1, Date: "2026-08-15T01:00:00", Branch: "trunk", Comment: "root"},
	}
	days := buildTimeline(entries)
	if len(days) != 1 || len(days[0].Rows) != 4 {
		t.Fatalf("days = %+v", days)
	}
	svg := func(i int) string { return string(days[0].Rows[i].Head) + string(days[0].Rows[i].Tail) }
	// The merge row curves out to a second rail.
	if !strings.Contains(svg(0), "<path") {
		t.Fatalf("merge row has no curve:\n%s", svg(0))
	}
	// The dev row sits on a different x than the trunk rail and carries
	// the dev branch color.
	if !strings.Contains(svg(1), branchStroke("dev")) {
		t.Fatalf("dev row not dev-colored:\n%s", svg(1))
	}
	if !strings.Contains(string(days[0].Rows[1].Head), `cx="21"`) {
		t.Fatalf("dev node not on second rail:\n%s", days[0].Rows[1].Head)
	}
	// The trunk row between branch point and merge shows the dev rail
	// passing through.
	if !strings.Contains(svg(2), `x1="21"`) {
		t.Fatalf("dev rail not passing through trunk row:\n%s", svg(2))
	}
	// The root row converges both rails (a join curve).
	if !strings.Contains(svg(3), "<path") {
		t.Fatalf("root row has no join curve:\n%s", svg(3))
	}
	// Root has nothing below: no line from cy to unitH on the node rail.
	if strings.Contains(svg(3), `y1="13" x2="7" y2="26"`) {
		t.Fatalf("root row continues below:\n%s", svg(3))
	}
}