@@ 2,6 2,7 @@ package web
import (
"errors"
+ "fmt"
"html"
"net/http"
"net/url"
@@ 9,6 10,7 @@ import (
"strings"
"testing"
+ "sourcecraft.dev/bigbes/sr-ht-dolt/beads"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
@@ 167,6 169,75 @@ func beadsEpicFixture() *fakeSession {
}
}
+// beadsClippedFixture is a tracker of beads.Max+5 issues as a capped read hands
+// it to the projection: the first beads.Max rows, and a Total saying the rest
+// exists. Ids run i-0000 upwards in table order, so i-2000 and up are the tail
+// no page here ever sees; beadsClippedTail names one.
+//
+// The rows are built whole and then clipped, rather than a short page given a
+// large Total by hand: the tail id has to be *genuinely* absent from what the
+// projection reads, or a test asserts the wording while never producing the
+// situation the wording is about. The clip is applied here because this
+// package's fake session answers every read whole — the store applies it at
+// limit, and beads/truncation_test.go drives the same fixtures through a seam
+// that does.
+func beadsClippedFixture() *fakeSession {
+ statuses := &browse.RowPage{
+ Columns: []string{"name", "category"},
+ Rows: [][]string{
+ {"open", "open"}, {"in_progress", "in_progress"}, {"closed", "closed"},
+ },
+ Total: 3,
+ }
+ return &fakeSession{
+ branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}},
+ tables: beadsTables(),
+ rowsByTable: map[string]*browse.RowPage{
+ "issues": clipPage(manyIssues(beads.Max+5), beads.Max),
+ "custom_statuses": statuses,
+ "dependencies": {
+ Columns: []string{"id", "issue_id", "depends_on_issue_id", "type"},
+ Rows: [][]string{{"d1", "i-0007", "i-0001", "blocks"}},
+ Total: 1,
+ },
+ },
+ }
+}
+
+// manyIssues builds n issue rows, ids i-0000 upwards, cycling the three status
+// categories so every lane is populated.
+func manyIssues(n int) *browse.RowPage {
+ statuses := []string{"open", "in_progress", "closed"}
+ rows := make([][]string, 0, n)
+ for i := range n {
+ rows = append(rows, []string{
+ fmt.Sprintf("i-%04d", i),
+ fmt.Sprintf("Issue %d", i),
+ statuses[i%len(statuses)],
+ "1", "task", "alice", "2024-01-01", "0",
+ })
+ }
+ return &browse.RowPage{
+ Columns: []string{"id", "title", "status", "priority", "issue_type", "assignee", "created_at", "is_blocked"},
+ Rows: rows,
+ Total: n,
+ }
+}
+
+// clipPage is the store's own answer to a read of limit rows: the first limit of
+// them, and the table's true row count beside them.
+func clipPage(p *browse.RowPage, limit int) *browse.RowPage {
+ rows := p.Rows
+ if len(rows) > limit {
+ rows = rows[:limit]
+ }
+ return &browse.RowPage{Columns: p.Columns, Rows: rows, Total: len(p.Rows)}
+}
+
+// beadsClippedTail is an id of beadsClippedFixture that exists in the tracker and
+// sits past the rows any read of it returns.
+const beadsClippedTail = "i-2003"
+
// --- end to end --------------------------------------------------------------
func TestBeadsHandleViewBoard(t *testing.T) {
@@ 302,6 373,141 @@ func TestBeadsDetailShowsCloseReason(t *testing.T) {
}
}
+// --- a read that was clipped -------------------------------------------------
+
+// The projection reads at most beads.Max rows of a table. A detail pane built
+// from a clipped read says so, quietly and in the board banner's own idiom: the
+// edges, the thread and the history on it may be short.
+func TestBeadsDetailReportsAClippedRead(t *testing.T) {
+ h := newHarness(t)
+ h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
+ h.browse.sess = beadsClippedFixture()
+ setViews(t, h, &beadsView{})
+
+ rec := h.do("GET", "/~alice/db/view/beads?issue=i-0007", nil, nil)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("detail: got %d, want 200; body=%s", rec.Code, rec.Body.String())
+ }
+ body := rec.Body.String()
+ if !strings.Contains(body, "This read was clipped") {
+ t.Errorf("clipped detail pane says nothing about the clip; body=%s", body)
+ }
+ // The number is the tracker's true size, which is what makes the line
+ // checkable rather than a bare warning.
+ if !strings.Contains(body, "The tracker holds 2005 issues") {
+ t.Errorf("clipped detail pane does not name the tracker's true size; body=%s", body)
+ }
+ if !strings.Contains(body, "Issue 7") {
+ t.Errorf("the issue itself still renders; body=%s", body)
+ }
+}
+
+// And a complete read makes no such claim: the line is a fact about the read,
+// not decoration on every detail pane.
+func TestBeadsDetailOnACompleteReadReportsNoClip(t *testing.T) {
+ h := newHarness(t)
+ h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
+ h.browse.sess = beadsFixture()
+ setViews(t, h, &beadsView{})
+
+ rec := h.do("GET", "/~alice/db/view/beads?issue=i-open", nil, nil)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("detail: got %d, want 200; body=%s", rec.Code, rec.Body.String())
+ }
+ if body := rec.Body.String(); strings.Contains(body, "This read was clipped") {
+ t.Errorf("nothing was clipped, so the pane must not say it was; body=%s", body)
+ }
+}
+
+// An id past the cap is not an absence: the tracker carries i-2003, the page did
+// not read that far, and saying "not found" would be the page claiming something
+// it cannot see.
+func TestBeadsDetailMissPastTheCapIsNotAbsence(t *testing.T) {
+ h := newHarness(t)
+ h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
+ sess := beadsClippedFixture()
+ h.browse.sess = sess
+ setViews(t, h, &beadsView{})
+
+ // The id exists in the tracker and not in the rows a read of it returns.
+ if got := sess.rowsByTable["issues"].Rows; len(got) != beads.Max {
+ t.Fatalf("the fixture is meant to hand over %d rows, got %d", beads.Max, len(got))
+ }
+ if strings.Contains(rowsText(sess.rowsByTable["issues"]), beadsClippedTail) {
+ t.Fatalf("%s is supposed to be past the rows read", beadsClippedTail)
+ }
+
+ rec := h.do("GET", "/~alice/db/view/beads?issue="+beadsClippedTail, nil, nil)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("detail: got %d, want 200; body=%s", rec.Code, rec.Body.String())
+ }
+ body := rec.Body.String()
+ if !strings.Contains(body, "Not among the issues read") {
+ t.Errorf("a miss past the cap must say the id was not read; body=%s", body)
+ }
+ if !strings.Contains(body, "The tracker holds 2005 issues") {
+ t.Errorf("and name what it did not read all of; body=%s", body)
+ }
+ if strings.Contains(body, "Issue not found.") {
+ t.Errorf("this read cannot support \"not found\"; body=%s", body)
+ }
+}
+
+// The other half of the split: nothing was clipped, so the id genuinely is not
+// there and the page says the plain thing it always did.
+func TestBeadsDetailMissOnACompleteReadIsAbsence(t *testing.T) {
+ h := newHarness(t)
+ h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
+ h.browse.sess = beadsFixture()
+ setViews(t, h, &beadsView{})
+
+ rec := h.do("GET", "/~alice/db/view/beads?issue=i-nope", nil, nil)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("detail: got %d, want 200; body=%s", rec.Code, rec.Body.String())
+ }
+ body := rec.Body.String()
+ if !strings.Contains(body, "Issue not found.") {
+ t.Errorf("a complete read that lacks the id is an absence; body=%s", body)
+ }
+ if strings.Contains(body, "Not among the issues read") {
+ t.Errorf("and it must not be dressed up as a clip; body=%s", body)
+ }
+}
+
+// The board's own banner is untouched by all of the above: same sentence, same
+// two numbers, in the branch it has always lived in.
+func TestBeadsBoardTruncationBannerIsUnchanged(t *testing.T) {
+ h := newHarness(t)
+ h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
+ h.browse.sess = beadsClippedFixture()
+ setViews(t, h, &beadsView{})
+
+ rec := h.do("GET", "/~alice/db/view/beads", nil, nil)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("board: got %d, want 200; body=%s", rec.Code, rec.Body.String())
+ }
+ body := rec.Body.String()
+ if !strings.Contains(body, "Showing the first 2000 of 2005 issues.") {
+ t.Errorf("the board banner changed; body=%s", body)
+ }
+ if strings.Contains(body, "This read was clipped") {
+ t.Errorf("the detail pane's line is the detail pane's; body=%s", body)
+ }
+}
+
+// rowsText is every cell of a page as one string, for asserting that an id is
+// nowhere in the rows a fixture hands over.
+func rowsText(p *browse.RowPage) string {
+ var b strings.Builder
+ for _, r := range p.Rows {
+ for _, c := range r {
+ b.WriteString(c)
+ b.WriteByte(' ')
+ }
+ }
+ return b.String()
+}
+
// --- the stream layout -------------------------------------------------------
func TestBeadsStreamRender(t *testing.T) {