~bigbes/sr-ht-dolt

ref: a41949952f01c47f58cc35fbba02205afd713d41 sr-ht-dolt/web/memory_test.go -rw-r--r-- 11.9 KiB
a4194995 — Eugene Blikh mcpsrv: report the projection's clip on get_issue and list_milestones 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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package web

import (
	"net/http"
	"strings"
	"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 projection — the prefix filter, the newline spellings, ?q=, the sort
// orders and the revision walk — is tested in the beads package. What is left
// here is the tab, the page, and the one thing this view renders that no other
// does: how old a memory is, in days.

// memoryCommits are the three commits the fixture's history has. Only the head
// and midHash ever touched config; rootHash predates the memories.
const (
	memoryMidHash  = "9f8e7d6c5b4a3928374655647382910a"
	memoryRootHash = "0a1b2c3d4e5f60718293a4b5c6d7e8f9"
)

// memoryFixture is a tracker with two memories, written at two different
// commits, over a three-commit history:
//
//	head (4 minutes ago)  handoff + metered  hash h2   wrote handoff
//	mid  (midAge ago)     metered            hash h1   wrote metered
//	root (200 days ago)   no memories        hash h0
//
// midAge is a parameter because the stale? marker is a question about exactly
// that number.
func memoryFixture(midAge time.Duration) *fakeSession {
	config := func(pairs ...[2]string) *browse.RowPage {
		page := &browse.RowPage{Columns: []string{"key", "value"}}
		for _, p := range pairs {
			page.Rows = append(page.Rows, []string{p[0], p[1]})
		}
		page.Total = len(page.Rows)
		return page
	}
	// The handoff memory carries the literal "\n" escape agents type into shell
	// strings, and a fragment that must not reach the page as markup.
	handoff := [2]string{"kv.memory.handoff-2026-08-12",
		`State after the 2026-08-12 round.\n\nSupersedes the two 2026-08-10 handoff memories, which asserted <b>otherwise</b>.`}
	metered := [2]string{"kv.memory.metered-link-calibration",
		"Owner is frequently on metered mobile internet.\n\nCalibrate downloads accordingly."}
	prefix := [2]string{"issue_prefix", "demo"}

	return &fakeSession{
		branches: []browse.Branch{{Name: "main", Head: headHash}},
		tables:   append(beadsTables(), memoryConfigTable()),
		commits: []browse.CommitInfo{
			{Hash: headHash, Author: "bigbes", Date: testNow.Add(-4 * time.Minute), Message: "bd: remember (auto-commit) by bigbes"},
			{Hash: memoryMidHash, Author: "bigbes", Date: testNow.Add(-midAge)},
			{Hash: memoryRootHash, Author: "alice", Date: testNow.Add(-200 * 24 * time.Hour)},
		},
		rowsByRef: map[string]map[string]*browse.RowPage{
			"main":         {"config": config(handoff, metered, prefix)},
			headHash:       {"config": config(handoff, metered, prefix)},
			memoryMidHash:  {"config": config(metered, prefix)},
			memoryRootHash: {"config": config(prefix)},
		},
		tableHashes: map[string]string{
			"main/config": "h2", headHash + "/config": "h2",
			memoryMidHash + "/config": "h1", memoryRootHash + "/config": "h0",
		},
	}
}

// memoryConfigTable is the shape AppliesMemories looks for on top of the beads
// fingerprint.
func memoryConfigTable() browse.TableInfo {
	return browse.TableInfo{Name: "config", Columns: []browse.ColumnInfo{
		{Name: "key", PrimaryKey: true}, {Name: "value"},
	}}
}

// The tab bar is registration order (views.go), and the design puts Memory after
// Milestones. This asserts the registry the process actually builds — not a
// hand-ordered slice a test injected — because init order is lexical file order
// and "memory.go" sorts before "milestones.go".
func TestMemoryTabFollowsMilestones(t *testing.T) {
	pos := map[string]int{}
	for i, v := range registeredViews {
		pos[v.Name()] = i
	}
	require.Contains(t, pos, "memory")
	require.Contains(t, pos, "milestones")
	assert.Less(t, pos["milestones"], pos["memory"], "the Memory tab comes after Milestones")
	assert.Less(t, pos["beads"], pos["milestones"], "and both after Beads")

	// And the same order on the rendered page, through the real registry.
	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 = memoryFixture(71 * 24 * time.Hour)

	rec := h.do("GET", "/~alice/db/view/memory", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
	body := rec.Body.String()
	iBeads := strings.Index(body, "/view/beads\"")
	iMilestones := strings.Index(body, "/view/milestones\"")
	iMemory := strings.Index(body, "/view/memory\"")
	require.NotEqual(t, -1, iBeads)
	require.NotEqual(t, -1, iMilestones)
	require.NotEqual(t, -1, iMemory)
	assert.Less(t, iBeads, iMilestones)
	assert.Less(t, iMilestones, iMemory, "the Memory tab is rendered after Milestones")
}

func TestMemoryAppliesNeedsAConfigTable(t *testing.T) {
	v := &memoryView{}
	assert.True(t, v.Applies(append(beadsTables(), memoryConfigTable())))
	assert.False(t, v.Applies(beadsTables()), "no config table, no Memory tab")
	assert.False(t, v.Applies([]browse.TableInfo{memoryConfigTable()}), "config alone is not a beads DB")
}

func TestMemoryRenders(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 = memoryFixture(71 * 24 * time.Hour)

	rec := h.do("GET", "/~alice/db/view/memory", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
	body := rec.Body.String()

	// Both memories, by slug, with the prefix stripped and the tracker's own
	// settings left where they belong.
	assert.Contains(t, body, ">handoff-2026-08-12<")
	assert.Contains(t, body, ">metered-link-calibration<")
	assert.NotContains(t, body, "kv.memory.")
	assert.NotContains(t, body, "issue_prefix")
	assert.Contains(t, body, "2 entries")

	// The revision of each, from the history: the head wrote the handoff, and the
	// short hash links to the commit page.
	assert.Contains(t, body, "written <span")
	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, "71 days ago", "the day-resolution spelling, not 2 months ago")
	assert.NotContains(t, body, "months ago")
	assert.Contains(t, body, `href="/~alice/db/commit/`+memoryMidHash+`"`)

	// Paragraphs, from both newline spellings: the literal escape is a break and
	// never reaches the page as backslash-n.
	assert.Contains(t, body, "<p>State after the 2026-08-12 round.</p>")
	assert.Contains(t, body, "<p>Owner is frequently on metered mobile internet.</p>")
	assert.NotContains(t, body, `\n`)

	// Stored text is text: it is escaped, never rendered as markup.
	assert.NotContains(t, body, "<b>otherwise</b>")
	assert.Contains(t, body, "&lt;b&gt;otherwise&lt;/b&gt;")

	// The freshness line the beads family shares.
	assert.Contains(t, body, `class="beads-freshness"`)
	assert.Contains(t, body, "main &middot; last commit")
}

// The stale? marker is a question asked at exactly 60 days, and it is a constant
// rather than a per-request knob: the only way to move it is to edit it.
func TestMemoryStaleMarkerAtTheBoundary(t *testing.T) {
	render := func(t *testing.T, midAge time.Duration) string {
		t.Helper()
		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 = memoryFixture(midAge)
		rec := h.do("GET", "/~alice/db/view/memory?key=metered-link-calibration", nil, nil)
		require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
		return rec.Body.String()
	}

	exactly := render(t, 60*24*time.Hour)
	assert.Contains(t, exactly, "metered-link-calibration")
	assert.NotContains(t, exactly, "stale?", "at the mark itself the question is not asked")

	past := render(t, 60*24*time.Hour+time.Second)
	assert.Contains(t, past, "stale?")
	assert.Contains(t, past, "60 days ago")

	fresh := render(t, 3*time.Hour)
	assert.NotContains(t, fresh, "stale?")
	assert.Contains(t, fresh, "3 hours ago")
}

// ?key= renders one memory and not the rest; an unknown slug says so rather than
// falling back to the whole list.
func TestMemorySingleKey(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 = memoryFixture(71 * 24 * time.Hour)

	one := h.do("GET", "/~alice/db/view/memory?key=handoff-2026-08-12", nil, nil)
	require.Equal(t, http.StatusOK, one.Code)
	assert.Contains(t, one.Body.String(), "State after the 2026-08-12 round.")
	assert.NotContains(t, one.Body.String(), "metered mobile internet")

	missing := h.do("GET", "/~alice/db/view/memory?key=nosuch", nil, nil)
	require.Equal(t, http.StatusOK, missing.Code)
	assert.Contains(t, missing.Body.String(), "No memory named")
	assert.NotContains(t, missing.Body.String(), "State after the 2026-08-12 round.")
}

// A tracker whose config holds no memory still gets the tab (Applies sees shapes
// and never rows) and renders an empty state that says what writes one.
func TestMemoryEmptyState(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 := memoryFixture(71 * 24 * time.Hour)
	sess.rowsByRef["main"] = map[string]*browse.RowPage{"config": {
		Columns: []string{"key", "value"},
		Rows:    [][]string{{"issue_prefix", "demo"}},
		Total:   1,
	}}
	h.browse.sess = sess

	rec := h.do("GET", "/~alice/db/view/memory", nil, nil)
	require.Equal(t, http.StatusOK, rec.Code, "memory view: %s", rec.Body.String())
	body := rec.Body.String()
	assert.Contains(t, body, "No memories.")
	assert.Contains(t, body, "bd remember")
	assert.Contains(t, body, "0 entries")
	// The tab is there either way, and so is the freshness line.
	assert.Contains(t, body, "/view/memory\"")
	assert.Contains(t, body, `class="beads-freshness"`)
}

// The sort toggle is a link that keeps the rest of the query, and the review
// queue is oldest first.
func TestMemorySortToggle(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 = memoryFixture(71 * 24 * time.Hour)

	bySlug := h.do("GET", "/~alice/db/view/memory?ref=main", nil, nil)
	require.Equal(t, http.StatusOK, bySlug.Code)
	assert.Contains(t, bySlug.Body.String(), "/view/memory?ref=main&amp;sort=age",
		"the toggle keeps ?ref= rather than re-listing the parameters it knows")

	byAge := h.do("GET", "/~alice/db/view/memory?sort=age", nil, nil)
	require.Equal(t, http.StatusOK, byAge.Code)
	body := byAge.Body.String()
	assert.Less(t, strings.Index(body, "metered-link-calibration"), strings.Index(body, "handoff-2026-08-12"),
		"age order is oldest first")
}

// agoDays is the Memory page's spelling of a duration: the same ladder as ago up
// to a day, and then days all the way — "71 days ago", never "2 months ago",
// because 60 days is the number this page marks memories at.
func TestAgoDaysStopsAtDays(t *testing.T) {
	pinClock(t, testNow)

	cases := []struct {
		name string
		d    time.Duration
		want string
	}{
		{"the instant itself", 0, "just now"},
		{"seconds", 45 * time.Second, "just now"},
		{"exactly a minute", time.Minute, "1 minute ago"},
		{"minutes", 4 * time.Minute, "4 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"},
		{"the stale mark", 60 * 24 * time.Hour, "60 days ago"},
		{"where ago says two months", 71 * 24 * time.Hour, "71 days ago"},
		{"where ago says a year", 365 * 24 * time.Hour, "365 days ago"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			assert.Equal(t, c.want, agoDays(testNow.Add(-c.d)))
		})
	}

	// A commit stamped ahead of this host's clock is skew, not a scheduled event.
	assert.Equal(t, "just now", agoDays(testNow.Add(48*time.Hour)))
}