~bigbes/sr-ht-dolt

ref: 3b06523cff250c254af560c8a32c339d760ffc37 sr-ht-dolt/web/freshness_test.go -rw-r--r-- 7.7 KiB
3b06523c — Eugene Blikh web: offer the bd command for the issue on screen 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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, "<code>qk9j2n4b</code>")
	// 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 &middot; last commit")
	assert.Contains(t, body, "4 minutes ago")
	assert.Contains(t, body, `href="/~alice/db/commit/`+headHash+`"`)
	assert.Contains(t, body, "<code>qk9j2n4b</code>")
	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)
}