From 82d997d27047a6f8a0be69a4b50414203b123618 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Thu, 13 Aug 2026 10:53:57 +0300 Subject: [PATCH] web: the detail pane says when its read was clipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The board has always carried a truncation banner; the detail pane it shares a template with carried nothing, so an issue assembled from tables clipped at 2000 rows looked complete, and an id in the tail of a big tracker was answered "Issue not found." — which the page cannot know. Two lines, both in the banner's idiom. A pane built from a clipped read says so and names the tracker's true size when the issues table itself was cut; a miss over such a read says the id was not among the rows read, which is not the same as saying it does not exist. A complete read keeps the plain "Issue not found.", and the board's own banner is untouched. --- web/beads_test.go | 206 +++++++++++++++++++++++++++++++++++++++ web/templates/beads.html | 18 ++++ 2 files changed, 224 insertions(+) diff --git a/web/beads_test.go b/web/beads_test.go index 3a926999092199d31c2881154393a78aba9af4e5..c48631dcc9766fc5a57394f6fb4f4b8c0e9bb439 100644 --- a/web/beads_test.go +++ b/web/beads_test.go @@ -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) { diff --git a/web/templates/beads.html b/web/templates/beads.html index 7db6f2ba5859ecc1c910d5f8e3116b92a97d435e..01b32340ec685aa34a645830f418916f09420b45 100644 --- a/web/templates/beads.html +++ b/web/templates/beads.html @@ -217,6 +217,15 @@ pre.field-body { {{/* ------------- detail / epic pane ------------- */}}

← Back to the parade

{{with .Data.Issue}} +{{/* The board's banner below can say "the first N of M" because it knows how + many cards it placed. A detail pane has no such N — it read one issue out + of tables that were clipped at 2000 rows each — so it says the part it can + support: the read was partial, and, when the issues table itself was cut, + how many issues the tracker really holds. Same banner, same words for the + same fact; it is not a second style. */}} +{{if $.Data.Truncated}} +
This read was clipped, so what is below may be short.{{if $.Data.IssuesClipped}} The tracker holds {{$.Data.ShownOf}} issues and only the first of them were read.{{end}}
+{{end}}

{{.ID}} {{.Title}} @@ -399,8 +408,17 @@ pre.field-body {

{{else}} +{{/* Two ways an issue can be missing, and the page may only claim the one it + can see. A complete read that does not carry the id is an absence; a read + clipped at 2000 rows is not, because the id may sit in the tail this page + never looked at, and "not found" there would be the page stating something + it does not know. */}} +{{if $.Data.MissingBeyondCap}} +
Not among the issues read. The tracker holds {{$.Data.ShownOf}} issues and only the first of them were read, so this id may sit in the tail this page never saw rather than not exist.
+{{else}}
Issue not found.
{{end}} +{{end}} {{else}} {{/* ---------------- board ---------------- */}}