package web
import (
"bytes"
"strings"
"testing"
)
// A stack landed in one press: two merges on trunk, the upper branch
// forked from the lower. Shaped like !1069 and !1070.
func stackEntries() []timelineEntry {
return []timelineEntry{
{Kind: "ci", Hash: "m2", Date: "2026-10-29T00:10:30", Branch: "trunk", IsMerge: true, Parents: []string{"m1", "f1"},
Comment: "land agent-follow\n\nChecks tested the merge with trunk at f1e85a3ebe16; trunk has moved since.",
Landing: "L1070", LandingNumber: 1070, LandingTitle: "A site agent follows its person", LandingApprover: "dmestas"},
{Kind: "ci", Hash: "m1", Date: "2026-10-29T00:10:10", Branch: "trunk", IsMerge: true, Parents: []string{"t0", "r1"},
Comment: "land rfc21-reach", Landing: "L1069", LandingNumber: 1069, LandingTitle: "RFC 0021: reach and following", LandingApprover: "dmestas"},
{Kind: "ci", Hash: "f1", Date: "2026-10-28T23:47:00", Branch: "agent-follow", Parents: []string{"r1"}, Comment: "A site agent follows"},
{Kind: "ci", Hash: "r1", Date: "2026-10-28T23:39:00", Branch: "rfc21-reach", Parents: []string{"t0"}, Comment: "RFC 0021"},
{Kind: "ci", Hash: "t0", Date: "2026-10-28T22:38:00", Branch: "trunk", Comment: "land team-room-tab"},
}
}
func rowOf(days []tlDay, hash string) *tlRow {
for d := range days {
for r := range days[d].Rows {
if days[d].Rows[r].Hash == hash {
return &days[d].Rows[r]
}
}
}
return nil
}
func TestALandedStackIsOneCardOnOneLane(t *testing.T) {
days := buildTimeline("r", "trunk", stackEntries())
m2, m1, f1, r1 := rowOf(days, "m2"), rowOf(days, "m1"), rowOf(days, "f1"), rowOf(days, "r1")
if m2 == nil || m1 == nil || f1 == nil || r1 == nil {
t.Fatal("timeline dropped a row")
}
if m2.Stack == nil || m2.Span != 2 || m2.Skip {
t.Fatalf("FAIL: the newest merge does not carry the stack card: stack=%v span=%d skip=%v", m2.Stack, m2.Span, m2.Skip)
}
if !m1.Skip || m1.Stack != nil {
t.Error("FAIL: the older merge still draws its own card")
}
if n := len(m2.Stack.Lines); n != 2 || m2.Stack.Lines[0].Number != 1070 || m2.Stack.Lines[1].Number != 1069 {
t.Errorf("FAIL: stack lines %+v", m2.Stack.Lines)
}
if !strings.Contains(string(m2.Stack.Lines[0].Note), "f1e85a3ebe16") || m2.Stack.Lines[1].Note != "" {
t.Error("FAIL: the tested-against note is not under its own landing")
}
// One lane: both branches tinted alike, each still named for itself.
if f1.Tint == "" || f1.Tint != r1.Tint || f1.Tint != m2.StackTint {
t.Errorf("FAIL: the stack is not one colour: %q %q %q", f1.Tint, r1.Tint, m2.StackTint)
}
if f1.Branch != "agent-follow" || r1.Branch != "rfc21-reach" {
t.Errorf("FAIL: a check-in lost its own branch name: %q %q", f1.Branch, r1.Branch)
}
}
func TestMergesThatAreNotAStackKeepTheirCards(t *testing.T) {
e := stackEntries()
e[2].Parents = []string{"t0"} // agent-follow forked from trunk, not from rfc21-reach
days := buildTimeline("r", "trunk", e)
if m2 := rowOf(days, "m2"); m2.Stack != nil || m2.Skip {
t.Error("FAIL: two unrelated landings were drawn as a stack")
}
e = stackEntries()
e[1].LandingApprover = "jva" // decided by somebody else: not one press
days = buildTimeline("r", "trunk", e)
if m2 := rowOf(days, "m2"); m2.Stack != nil {
t.Error("FAIL: merges approved by two people were drawn as one press")
}
}
func TestAStackCardRendersItsLandingsAndFoldsTheRest(t *testing.T) {
st := &tlStack{Approver: "dmestas"}
for i := 0; i < 5; i++ {
st.Lines = append(st.Lines, tlStackLine{Landing: "L", Number: 1070 + i, Title: "t"})
}
row := tlRow{Kind: "ci", Hash: "h", Short: "h", URL: "/u", Stack: st, Span: 5}
var b bytes.Buffer
if err := noHooks.template(timelinePage).ExecuteTemplate(&b, "tl-stack", map[string]any{"Repo": "r", "Row": row}); err != nil {
t.Fatal(err)
}
out := b.String()
if !strings.Contains(out, `data-hook="tl-stack" data-count="5"`) {
t.Error("FAIL: no stack card")
}
if got := strings.Count(out, `data-hook="tl-stack-line"`); got != 5 {
t.Errorf("FAIL: %d landing lines, want 5", got)
}
if !strings.Contains(out, `data-hook="tl-stack-more"`) {
t.Error("FAIL: the landings past the first three are not folded")
}
if !strings.Contains(out, `#i-strata`) {
t.Error("FAIL: the card has no strata icon")
}
}