~bigbes/sr-ht-dolt

82d997d27047a6f8a0be69a4b50414203b123618 — Eugene Blikh 5 days ago a419499
web: the detail pane says when its read was clipped

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.
2 files changed, 224 insertions(+), 0 deletions(-)

M web/beads_test.go
M web/templates/beads.html
M web/beads_test.go => web/beads_test.go +206 -0
@@ 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) {

M web/templates/beads.html => web/templates/beads.html +18 -0
@@ 217,6 217,15 @@ pre.field-body {
{{/* ------------- detail / epic pane ------------- */}}
<p><a href="/~{{.Repo.OwnerName}}/{{.Repo.Name}}/view/beads">&larr; Back to the parade</a></p>
{{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}}
<div class="alert alert-warning">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}}</div>
{{end}}
<div class="bead-detail">
  <h2 class="ticket-title">
    <code>{{.ID}}</code> <span>{{.Title}}</span>


@@ 399,8 408,17 @@ pre.field-body {
  </div>
</div>
{{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}}
<div class="alert alert-warning">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.</div>
{{else}}
<div class="alert alert-warning">Issue not found.</div>
{{end}}
{{end}}

{{else}}
{{/* ---------------- board ---------------- */}}