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: "*"}}}},
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 folds the rungs under it 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, "3 segments") {
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 _, badge := range []string{">1/3<", ">2/3<", ">3/3<"} {
if !strings.Contains(html, badge) {
t.Errorf("rung badge %s missing", badge)
}
}
if !strings.Contains(html, "<details class=\"strata\">") {
t.Error("the rungs are not folded under the base without script")
}
// The top rung passed CI, and its tip contains everything below:
// that is the strata's verdict, on the row.
if !strings.Contains(html, "stack <span 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 accept control is disabled and says why.
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, `class="strata-map"`) || !strings.Contains(top, "3 segments") {
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 !strings.Contains(top, "stack <span class=\"tl-badge ci-pass\"") {
t.Error("the map does not carry the strata's verdict")
}
if !strings.Contains(top, "phase one</a>") || !strings.Contains(top, "· landable") {
t.Error("the landable base is not named in the map")
}
if !strings.Contains(top, "stacked — the segment below lands first") {
t.Error("the accept control above the bottom is not disabled with the reason")
}
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 !strings.Contains(top, `formaction="/r/alpha/landing/c3c3c3c3c3c3/land"`) || !strings.Contains(top, "Land up to here (3 rungs)") {
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 := 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, `<details class="strata">`) {
t.Error("a too-tall strata was folded anyway")
}
if !strings.Contains(html, "too tall to fold here") {
t.Error("the page does not say the strata is too tall")
}
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")
}