package web import ( "errors" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" "sourcecraft.dev/bigbes/sr-ht-dolt/core" ) // The freshness line: handleView's Head on the envelope, the shared beadsHead // partial, and the ago func the two of them render. The line qualifies every // number on the page, so the tests that matter most here are the ones where it // is absent: a store with no history, and a log that cannot be read, must still // serve the board. // testNow is the instant the clock is pinned to in these tests. Nothing about // it is special beyond being fixed. var testNow = time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC) // pinClock replaces the package clock ago reads for the duration of a test, so // a relative time is a function of the fixture rather than of when the suite // ran. func pinClock(t *testing.T, at time.Time) { t.Helper() prev := timeNow timeNow = func() time.Time { return at } t.Cleanup(func() { timeNow = prev }) } // headHash is a dolt-shaped object id; its first 8 characters are what // shortsha renders. const headHash = "qk9j2n4bh0ktbfjrn3f7f8m3s9d1p5v2" // withHead gives a fixture session a head commit four minutes old. func withHead(s *fakeSession) *fakeSession { s.commits = []browse.CommitInfo{{ Hash: headHash, Author: "Eugene Blikh", Date: testNow.Add(-4 * time.Minute), Message: "bd: create sr-ht-dolt-44n.2 (auto-commit)", }} return s } func TestAgoGranularity(t *testing.T) { pinClock(t, testNow) cases := []struct { name string d time.Duration // how long before the pinned now the instant is want string }{ {"the instant itself", 0, "just now"}, {"seconds", 45 * time.Second, "just now"}, {"one second short of a minute", time.Minute - time.Second, "just now"}, {"exactly a minute", time.Minute, "1 minute ago"}, {"minutes", 4 * time.Minute, "4 minutes ago"}, {"one second short of an hour", time.Hour - time.Second, "59 minutes ago"}, {"exactly an hour", time.Hour, "1 hour ago"}, {"hours", 3 * time.Hour, "3 hours ago"}, {"one second short of a day", 24*time.Hour - time.Second, "23 hours ago"}, {"exactly a day", 24 * time.Hour, "1 day ago"}, {"days", 2 * 24 * time.Hour, "2 days ago"}, {"one second short of a month", 30*24*time.Hour - time.Second, "29 days ago"}, {"exactly a month", 30 * 24 * time.Hour, "1 month ago"}, {"months", 71 * 24 * time.Hour, "2 months ago"}, {"one second short of a year", 365*24*time.Hour - time.Second, "12 months ago"}, {"exactly a year", 365 * 24 * time.Hour, "1 year ago"}, {"years", 3 * 365 * 24 * time.Hour, "3 years ago"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { assert.Equal(t, c.want, ago(testNow.Add(-c.d))) }) } } // A commit stamped in the future is clock skew between whoever wrote it and // this host, not a scheduled event: it reads as "just now", never as a // forward-facing phrase and never as a negated count. func TestAgoOnAFutureInstant(t *testing.T) { pinClock(t, testNow) for _, ahead := range []time.Duration{time.Second, 3 * time.Minute, 48 * time.Hour} { got := ago(testNow.Add(ahead)) assert.Equal(t, "just now", got, "an instant %s ahead of the clock", ahead) assert.NotContains(t, got, "-", "a future instant must not render a negative count") assert.NotContains(t, got, "in ", "the freshness line is past-facing") } } func TestBeadsHeaderShowsFreshness(t *testing.T) { pinClock(t, testNow) h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = withHead(beadsFixture()) setViews(t, h, &beadsView{}) rec := h.do("GET", "/~alice/db/view/beads", nil, nil) require.Equal(t, http.StatusOK, rec.Code, "board: %s", rec.Body.String()) body := rec.Body.String() assert.Contains(t, body, `class="beads-freshness"`) // The branch being rendered, the relative age, and the short hash linking to // the commit page — the three things the line is for. assert.Contains(t, body, "main · last commit") assert.Contains(t, body, "4 minutes ago") assert.Contains(t, body, `href="/~alice/db/commit/`+headHash+`"`) assert.Contains(t, body, "qk9j2n4b") // The exact stamp stays one hover away. assert.Contains(t, body, `title="2026-08-12 11:56:00 UTC"`) // The board itself is unchanged around it. assert.Contains(t, body, "Rolling") } func TestMilestonesHeaderShowsFreshness(t *testing.T) { pinClock(t, testNow) h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) h.browse.sess = withHead(milestoneFixture()) setViews(t, h, &beadsView{}, &milestonesView{}) rec := h.do("GET", "/~alice/db/view/milestones", nil, nil) require.Equal(t, http.StatusOK, rec.Code, "milestones: %s", rec.Body.String()) body := rec.Body.String() assert.Contains(t, body, `class="beads-freshness"`) assert.Contains(t, body, "main · last commit") assert.Contains(t, body, "4 minutes ago") assert.Contains(t, body, `href="/~alice/db/commit/`+headHash+`"`) assert.Contains(t, body, "qk9j2n4b") assert.Contains(t, body, "ms-title\">m1<") } // A database with no commits renders the board without the line. This is the // one that protects the rule: the freshness line is decoration on top of an // answer and may never be the reason a reader gets a 500 instead of a board. func TestViewHeaderOmittedWithoutHistory(t *testing.T) { pinClock(t, testNow) h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) sess := beadsFixture() sess.commits = nil // an initialised store nobody has committed to h.browse.sess = sess setViews(t, h, &beadsView{}) rec := h.do("GET", "/~alice/db/view/beads", nil, nil) require.Equal(t, http.StatusOK, rec.Code, "board: %s", rec.Body.String()) body := rec.Body.String() // The class name still appears in the page's scoped stylesheet, which is // static; what must be absent is an element carrying it. assert.NotContains(t, body, `class="beads-freshness"`) assert.NotContains(t, body, "last commit") // The page is otherwise the page. assert.Contains(t, body, "Rolling") assert.Contains(t, body, "Ready to roll") } // A log that cannot be read degrades the same way, rather than failing the // request. func TestViewHeaderOmittedWhenLogFails(t *testing.T) { pinClock(t, testNow) h := newHarness(t) h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic}) sess := withHead(beadsFixture()) sess.logErr = errors.New("browse: walk commits: corrupt chunk") h.browse.sess = sess setViews(t, h, &beadsView{}) rec := h.do("GET", "/~alice/db/view/beads", nil, nil) require.Equal(t, http.StatusOK, rec.Code, "board: %s", rec.Body.String()) body := rec.Body.String() // The class name still appears in the page's scoped stylesheet, which is // static; what must be absent is an element carrying it. assert.NotContains(t, body, `class="beads-freshness"`) assert.NotContains(t, body, "last commit") assert.Contains(t, body, "Rolling") } // headCommit is the envelope's one read, and returns nil on both failure arms // so the template has nothing to render. func TestHeadCommitDegradesToNil(t *testing.T) { ctx := t.Context() empty := &fakeSession{branches: []browse.Branch{{Name: "main"}}} assert.Nil(t, headCommit(ctx, empty, "main"), "no commits") failing := &fakeSession{logErr: errors.New("boom")} assert.Nil(t, headCommit(ctx, failing, "main"), "log error") ok := withHead(&fakeSession{}) got := headCommit(ctx, ok, "main") require.NotNil(t, got) assert.Equal(t, headHash, got.Hash) }