Skip to content
quarry Artifact [d7f9294755]
Login

Artifact [d7f9294755]

Artifact d7f92947553717fd6e41f7b29d1ae20d463757e5f16844d340dc42fc4ef2d003:


package web

import (
	"regexp"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
	"net/http/httptest"
	"path/filepath"
)

// Three landings chained feat-c → feat-b → feat-a → trunk, all open,
// plus one standalone. Four open landings; two decisions.
func strataIndex(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() })
	root := func(hash, branch, target, title, date string) index.Parsed {
		return index.Parsed{Hash: hash, Art: &artifact.Artifact{Type: artifact.Forum,
			ThreadTitle: title, Date: date, User: "dan", Mimetype: "text/x-markdown",
			Body: []byte("zit-landing: 1\nbranch: " + branch + "\ntarget: " + target + "\n\n" + title + "\n")}}
	}
	if err := ix.FoldNow([]index.Parsed{
		// A tip on the top branch with a passing verdict: the strata's.
		{Hash: strings.Repeat("00", 20), Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-08-19T09:00:00", User: "dan", Comment: "base"}},
		{Hash: strings.Repeat("cc", 20), Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-08-20T09:25:00", User: "dan", Comment: "top of the stack",
			Parents: []string{strings.Repeat("00", 20)},
			Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "feat-c"},
				{Name: "*sym-feat-c", Target: "*"}, {Name: "+ci-pass", Target: "*"}}}},
		// Each lower rung's tip passes too: a strata is green only when
		// every rung is.
		{Hash: strings.Repeat("aa", 20), Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-08-20T08:50:00", User: "dan", Comment: "phase one work",
			Parents: []string{strings.Repeat("00", 20)},
			Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "feat-a"},
				{Name: "*sym-feat-a", Target: "*"}, {Name: "+ci-pass", Target: "*"}}}},
		{Hash: strings.Repeat("bb", 20), Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-08-20T08:55:00", User: "dan", Comment: "phase two work",
			Parents: []string{strings.Repeat("aa", 20)},
			Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "feat-b"},
				{Name: "*sym-feat-b", Target: "*"}, {Name: "+ci-pass", Target: "*"}}}},
		root(strings.Repeat("a1", 20), "feat-a", "trunk", "phase one", "2026-08-20T09:00:00"),
		root(strings.Repeat("b2", 20), "feat-b", "feat-a", "phase two", "2026-08-20T09:10:00"),
		root(strings.Repeat("c3", 20), "feat-c", "feat-b", "phase three", "2026-08-20T09:20:00"),
		root(strings.Repeat("d4", 20), "lone", "trunk", "a loner", "2026-08-20T09:30:00"),
	}); err != nil {
		t.Fatal(err)
	}
	if err := ix.RecomputeBranches(); err != nil {
		t.Fatal(err)
	}
	return ix
}

func strataServer(t *testing.T, ix *index.Index) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i8"})}))
	t.Cleanup(srv.Close)
	return srv
}

// RFC 0006: a strata that looks like three landings is worse than no
// strata feature. The open queue shows it as ONE row, counts it as one
// decision, and draws its rungs on a rail with their positions.
func TestStrataListsAsOneDecision(t *testing.T) {
	srv := strataServer(t, strataIndex(t))
	html := asDan(t, srv, "/r/alpha/landing")
	if strings.Count(html, `<li class="t-row`) != 2 {
		t.Fatalf("want 2 rows (the stack and the loner), got %d:\n%.1200s", strings.Count(html, `<li class="t-row`), html)
	}
	if !strings.Contains(html, `t-strata`) || !strings.Contains(html, `data-hook="strata-row" data-rungs="3"`) {
		t.Errorf("the stack row is not marked as a strata of 3")
	}
	if !strings.Contains(html, `title="2">2</span> Open<`) {
		i := strings.Index(html, "Open</a>")
		if i < 0 {
			i = len(html)
		}
		t.Errorf("the count chip does not count decisions (want 2 Open): %q", html[max(0, i-160):min(len(html), i+12)])
	}
	for _, pos := range []string{`data-pos="1"`, `data-pos="2"`, `data-pos="3"`} {
		if !strings.Contains(html, pos) {
			t.Errorf("rung %s missing", pos)
		}
	}
	if strings.Contains(html, "<details") {
		t.Error("the rungs are folded; the list draws them open")
	}
	// The top rung passed CI, and its tip contains everything below:
	// that is the strata's verdict, on the row.
	if m := html[strings.Index(html, `data-hook="strata-row"`):]; !strings.Contains(m[:strings.Index(m, "</p>")], `class="tl-badge ci-pass"`) {
		t.Errorf("the stack's verdict is not on the row:\n%.900s", html)
	}
	// The settled views stay flat: nothing to group.
	if all := asDan(t, srv, "/r/alpha/landing?state=all"); strings.Contains(all, "t-strata") {
		t.Error("the flat view grouped")
	}
}

// The record page: a map with this rung marked and the landable one
// named; above the bottom, the one action is Land stack, which says
// what it lands.
func TestStrataMapOnTheRecord(t *testing.T) {
	srv := strataServer(t, strataIndex(t))
	top := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("c3", 6))
	if !strings.Contains(top, `data-hook="strata-map" data-rungs="3"`) {
		t.Fatalf("no stack map on the top rung:\n%.1200s", top)
	}
	if !strings.Contains(top, `aria-current="true"`) {
		t.Error("the current rung is not marked")
	}
	if m := top[strings.Index(top, `data-hook="strata-map"`):]; !strings.Contains(m[:strings.Index(m, "</p>")], `class="tl-badge ci-pass"`) {
		t.Error("the map does not carry the strata's verdict")
	}
	if !strings.Contains(top, "phase one</a>") || !regexp.MustCompile(`data-pos="1" data-landable="1"`).MatchString(top) {
		t.Error("the landable base is not named in the map")
	}
	if strings.Contains(top, `formaction="/r/alpha/landing/c3c3c3c3c3c3/accept"`) {
		t.Error("above the bottom, Accept is still offered")
	}
	base := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if !strings.Contains(base, `formaction="/r/alpha/landing/a1a1a1a1a1a1/accept"`) {
		t.Error("the base's accept control is missing")
	}
	// Landing the whole strata is offered at the base, aimed at the top;
	// landing up to a rung is offered on that rung.
	// The FORMACTION is the contract — it names the top rung, which is
	// what "land the whole strata" means. The rung COUNT must reach the
	// reader too, but its wording is display text and changes with the
	// viewport, so the assertion is on the number, not the sentence.
	if !strings.Contains(base, `formaction="/r/alpha/landing/c3c3c3c3c3c3/land"`) || !strings.Contains(base, "3") {
		t.Error("the base does not offer to land the strata")
	}
	if !regexp.MustCompile(`data-accept="stacked" formaction="/r/alpha/landing/c3c3c3c3c3c3/land"`).MatchString(top) {
		t.Error("the top rung does not offer to land up to itself")
	}
	// A standalone landing has no map.
	if lone := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("d4", 6)); strings.Contains(lone, `class="strata-map"`) {
		t.Error("a standalone landing drew a stack map")
	}
}

// Past twenty rungs the folded list and the map stop helping and start
// scrolling (GitLab's cap). The chain is still read and landable; the
// page says how tall it is and points at the base.
func TestTooTallStrataIsNotDrawn(t *testing.T) {
	var g landingGroup
	g.ID, g.Title, g.Branch, g.Target, g.State = strings.Repeat("a1", 20), "base", "r1", "trunk", "open"
	g.Stack, g.Position, g.Depth = g.ID, 1, index.MaxDrawnRungs+1
	for i := 1; i <= index.MaxDrawnRungs+1; i++ {
		g.Segments = append(g.Segments, landingRow{ID: strings.Repeat("b2", 20), Title: "rung", State: "open",
			Stack: g.ID, Position: i, Depth: index.MaxDrawnRungs + 1})
	}
	if !g.TooTall() {
		t.Fatal("21 rungs is not too tall")
	}
	var buf strings.Builder
	if err := noHooks.template(landingListPage).ExecuteTemplate(&buf, "content", pageData{Repo: "alpha",
		LandingGroups: []landingGroup{g}, LandingOpen: 1, Nav: ticketNav{Repo: "alpha"}}); err != nil {
		t.Fatal(err)
	}
	html := buf.String()
	if strings.Contains(html, `class="sm-rungs"`) {
		t.Error("a too-tall strata was drawn anyway")
	}
	if !strings.Contains(html, `>open the base</a>`) {
		t.Error("a too-tall strata does not lead to its base")
	}
	g.Segments = g.Segments[:index.MaxDrawnRungs]
	if g.TooTall() {
		t.Error("exactly the cap counts as too tall")
	}
}

// A rung is one line: a landing whose description is a paragraph shows
// its first sentence clipped, not the paragraph as a link.
func TestAStrataRungIsOneLine(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	long := strings.Repeat("A landing description written as one long paragraph of prose. ", 6)
	root := func(hash, branch, target string) index.Parsed {
		return index.Parsed{Hash: hash, Art: &artifact.Artifact{Type: artifact.Forum, ThreadTitle: long, Date: "2026-08-20T09:00:00",
			User: "dan", Mimetype: "text/x-markdown", Body: []byte("zit-landing: 1\nbranch: " + branch + "\ntarget: " + target + "\n\n" + long + "\n")}}
	}
	if err := ix.FoldNow([]index.Parsed{root(strings.Repeat("a1", 20), "one", "trunk"), root(strings.Repeat("b2", 20), "two", "one")}); err != nil {
		t.Fatal(err)
	}
	if err := ix.RecomputeBranches(); err != nil {
		t.Fatal(err)
	}
	html := asDan(t, strataServer(t, ix), "/r/alpha/landing")
	links := regexp.MustCompile(`class="rung-t"[^>]*>([^<]*)</a>`).FindAllStringSubmatch(html, -1)
	if len(links) != 2 {
		t.Fatalf("FAIL: %d rung links, want 2", len(links))
	}
	for _, l := range links {
		if n := len([]rune(l[1])); n > 73 {
			t.Errorf("FAIL: a rung prints %d characters of its description", n)
		}
	}
	t.Log("ok: both rungs clipped to a line")
}

// A landing on its own offers to stack; the picker lists the open
// landings it may land after, and never one already standing on it,
// which would make a loop. A stacked rung offers to unstack onto the
// branch its strata lands on.
func TestALandingOffersToStackAndARungToUnstack(t *testing.T) {
	srv := strataServer(t, strataIndex(t))
	base := "/r/alpha/landing/" + strings.Repeat("a1", 6)
	page := asDan(t, srv, base)
	if !strings.Contains(page, `data-hook="stack-open"`) {
		t.Fatal("FAIL: a landing on its own does not offer to stack")
	}
	pick := asDan(t, srv, base+"?act=stack")
	sel := pick[strings.Index(pick, `data-hook="stack-on"`):]
	sel = sel[:strings.Index(sel, "</select>")]
	if !strings.Contains(sel, `value="!`+strings.Repeat("d4", 6)+`"`) {
		t.Errorf("FAIL: the loner is not offered:\n%s", sel)
	}
	for _, above := range []string{"b2", "c3"} {
		if strings.Contains(sel, `value="!`+strings.Repeat(above, 6)+`"`) {
			t.Errorf("FAIL: %s stands on this landing and is offered anyway, a loop", above)
		}
	}
	if !strings.Contains(pick, `data-hook="stack-go"`) || !strings.Contains(pick, base+`/retarget"`) {
		t.Error("FAIL: the picker does not post to retarget")
	}
	top := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("c3", 6))
	if !regexp.MustCompile(`name="to" value="trunk" formaction="/r/alpha/landing/c3c3c3c3c3c3/retarget" data-hook="unstack"`).MatchString(top) {
		t.Error("FAIL: a stacked rung does not offer to unstack onto trunk")
	}
}

// The strata's verdict is the worst rung's: the top passing says
// nothing about a rung below it that was stacked without being built
// on.
func TestAStrataWithAnUncheckedRungIsNotGreen(t *testing.T) {
	ix := strataIndex(t)
	if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat("ab", 20), Art: &artifact.Artifact{Type: artifact.Manifest,
		Date: "2026-08-20T09:40:00", User: "dan", Comment: "more phase one",
		Parents: []string{strings.Repeat("aa", 20)},
		Tags:    []artifact.Tag{{Name: "*branch", Target: "*", Value: "feat-a"}}}}}); err != nil {
		t.Fatal(err)
	}
	top := asDan(t, strataServer(t, ix), "/r/alpha/landing/"+strings.Repeat("c3", 6))
	m := top[strings.Index(top, `data-hook="strata-map"`):]
	if strings.Contains(m[:strings.Index(m, "</p>")], `ci-pass`) {
		t.Error("FAIL: a strata whose bottom rung has no verdict reads as green")
	}
}