~bigbes/sr-ht-dolt

ref: a674ddb16a28934ee97a48a3938c866ad1bc508f sr-ht-dolt/beads/memory_test.go -rw-r--r-- 15.3 KiB
a674ddb1 — Eugene Blikh ci: publish the apk into artifacts.sr.ht as well 3 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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
package beads

import (
	"context"
	"fmt"
	"net/url"
	"sort"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
)

// --- fixture -----------------------------------------------------------------

// memoryNow is the instant the memory tests measure staleness against. Nothing
// about it is special beyond being fixed: staleness is a function of the
// fixture's dates and this clock, never of when the suite ran.
var memoryNow = time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC)

// fakeHistory is a MemorySession over canned per-ref content: the config table
// at each commit, that table's content hash there, and a commit log. It records
// every ref a row read was issued at, which is how the walk's central claim —
// that a commit whose table hash is unchanged is skipped without reading a
// single row — is asserted rather than assumed.
type fakeHistory struct {
	commits []browse.CommitInfo          // newest first, as Log returns them
	config  map[string]map[string]string // ref → key → value; a missing ref has no config table
	hashes  map[string]string            // ref → config's content hash; "" = table absent

	reads     []string // refs Rows was called at, in order
	hashCalls int
	logCalls  int
}

func (f *fakeHistory) Rows(_ context.Context, ref, table string, _, _ int) (*browse.RowPage, error) {
	f.reads = append(f.reads, ref)
	rows, ok := f.config[ref]
	if !ok || table != "config" {
		return nil, fmt.Errorf("%w: %s", browse.ErrTableNotFound, table)
	}
	page := &browse.RowPage{Columns: []string{"key", "value"}}
	keys := make([]string, 0, len(rows))
	for k := range rows {
		keys = append(keys, k)
	}
	sort.Strings(keys) // a store returns rows in key order; so does this
	for _, k := range keys {
		page.Rows = append(page.Rows, []string{k, rows[k]})
	}
	page.Total = len(page.Rows)
	return page, nil
}

func (f *fakeHistory) Log(_ context.Context, _, _ string, limit int) ([]browse.CommitInfo, string, error) {
	f.logCalls++
	if limit < len(f.commits) {
		return f.commits[:limit], f.commits[limit].Hash, nil
	}
	return f.commits, "", nil
}

func (f *fakeHistory) TableHash(_ context.Context, ref, table string) (string, bool, error) {
	f.hashCalls++
	if table != "config" {
		return "", false, nil
	}
	h, ok := f.hashes[ref]
	if !ok || h == "" {
		return "", false, nil
	}
	return h, true, nil
}

// memoryHistory is the fixture the walk is asserted against: five commits, and a
// config table that changes at only two of them.
//
//	c4 (3 hours ago)   alpha=v2 beta=b gamma=g   hash h-c   ← head
//	c3 (2 days ago)    alpha=v2 beta=b gamma=g   hash h-c   wrote alpha
//	c2 (71 days ago)   alpha=v1 beta=b gamma=g   hash h-b
//	c1 (99 days ago)   alpha=v1 beta=b gamma=g   hash h-b   wrote beta
//	c0 (200 days ago)  gamma=g                   hash h-a   the root
//
// So alpha resolves to c3, beta to c1, and gamma — never changed anywhere in the
// history — to the root commit that introduced it.
func memoryHistory() *fakeHistory {
	at := func(d time.Duration) time.Time { return memoryNow.Add(-d) }
	head := map[string]string{
		"kv.memory.alpha": "second version",
		"kv.memory.beta":  "beta text",
		"kv.memory.gamma": "gamma text",
		"issue_prefix":    "demo", // not a memory: the prefix filter drops it
	}
	older := map[string]string{
		"kv.memory.alpha": "first version",
		"kv.memory.beta":  "beta text",
		"kv.memory.gamma": "gamma text",
		"issue_prefix":    "demo",
	}
	root := map[string]string{
		"kv.memory.gamma": "gamma text",
		"issue_prefix":    "demo",
	}
	return &fakeHistory{
		commits: []browse.CommitInfo{
			{Hash: "c4", Author: "bigbes", Date: at(3 * time.Hour)},
			{Hash: "c3", Author: "bigbes", Date: at(2 * 24 * time.Hour)},
			{Hash: "c2", Author: "bigbes", Date: at(71 * 24 * time.Hour)},
			{Hash: "c1", Author: "alice", Date: at(99 * 24 * time.Hour)},
			{Hash: "c0", Author: "alice", Date: at(200 * 24 * time.Hour)},
		},
		config: map[string]map[string]string{
			"main": head, "c4": head, "c3": head,
			"c2": older, "c1": older,
			"c0": root,
		},
		hashes: map[string]string{
			"main": "h-c", "c4": "h-c", "c3": "h-c",
			"c2": "h-b", "c1": "h-b",
			"c0": "h-a",
		},
	}
}

func buildMemories(t *testing.T, sess MemorySession, query string) *MemoryView {
	t.Helper()
	q, err := url.ParseQuery(query)
	require.NoError(t, err)
	v, err := BuildMemories(context.Background(), sess, "main", q, memoryNow)
	require.NoError(t, err)
	return v
}

func slugsOf(v *MemoryView) []string {
	out := make([]string, 0, len(v.Memories))
	for _, m := range v.Memories {
		out = append(out, m.Slug)
	}
	return out
}

// --- the projection ----------------------------------------------------------

// Only the kv.memory.* rows are memories, and the slug is the key without that
// prefix. The rest of config is the tracker's settings and must not appear.
func TestMemoriesPrefixFiltering(t *testing.T) {
	v := buildMemories(t, memoryHistory(), "")

	assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(v))
	assert.Equal(t, 3, v.Total, "issue_prefix is not a memory")
	assert.Equal(t, MemorySortSlug, v.Sort)
	assert.False(t, v.WalkTruncated)
}

// A value carries both newline spellings — real ones when it was written from a
// file, literal backslash-n when it was typed into a shell string — and both
// have to become the same paragraphs.
func TestMemoriesNormaliseBothNewlineSpellings(t *testing.T) {
	const escaped = `First paragraph.\n\nSecond paragraph, line one.\nLine two.`
	real := "First paragraph.\n\nSecond paragraph, line one.\nLine two."
	crlf := "First paragraph.\r\n\r\nSecond paragraph, line one.\r\nLine two."

	sess := memoryHistory()
	for _, ref := range []string{"main", "c4", "c3"} {
		sess.config[ref] = map[string]string{
			"kv.memory.escaped": escaped,
			"kv.memory.real":    real,
			"kv.memory.crlf":    crlf,
		}
	}
	v := buildMemories(t, sess, "")
	require.Len(t, v.Memories, 3)

	want := []string{"First paragraph.", "Second paragraph, line one.\nLine two."}
	for _, m := range v.Memories {
		assert.Equal(t, want, m.Paragraphs, "memory %q", m.Slug)
		assert.Equal(t, "First paragraph.\n\nSecond paragraph, line one.\nLine two.", m.Text,
			"memory %q", m.Slug)
		assert.NotContains(t, m.Text, `\n`, "the literal escape must not survive into the text")
	}
}

// ?q= is a substring of the slug or of the text, case-insensitively.
func TestMemoriesSearchFilter(t *testing.T) {
	bySlug := buildMemories(t, memoryHistory(), "q=ALPH")
	assert.Equal(t, []string{"alpha"}, slugsOf(bySlug))
	assert.Equal(t, 3, bySlug.Total, "Total counts the tracker, not the page")
	assert.Equal(t, "ALPH", bySlug.Search)

	byText := buildMemories(t, memoryHistory(), "q=second+version")
	assert.Equal(t, []string{"alpha"}, slugsOf(byText))

	none := buildMemories(t, memoryHistory(), "q=nothing+matches+this")
	assert.Empty(t, none.Memories)
	assert.Equal(t, 3, none.Total)
}

// ?key= renders one memory; an unknown slug renders none rather than everything.
func TestMemoriesSingleKey(t *testing.T) {
	one := buildMemories(t, memoryHistory(), "key=beta")
	require.Len(t, one.Memories, 1)
	assert.Equal(t, "beta", one.Memories[0].Slug)
	assert.Equal(t, "beta", one.Key)

	missing := buildMemories(t, memoryHistory(), "key=nosuch")
	assert.Empty(t, missing.Memories)
	assert.Equal(t, 3, missing.Total)
}

// Slug order is the default; age order is oldest first, which is the review
// queue. An unknown ?sort= value falls back to slug rather than to nothing.
func TestMemoriesSortOrders(t *testing.T) {
	assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(buildMemories(t, memoryHistory(), "")))
	assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(buildMemories(t, memoryHistory(), "sort=slug")))
	assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(buildMemories(t, memoryHistory(), "sort=sideways")))

	// gamma (root, 200 days) → beta (c1, 99 days) → alpha (c3, 2 days).
	byAge := buildMemories(t, memoryHistory(), "sort=age")
	assert.Equal(t, []string{"gamma", "beta", "alpha"}, slugsOf(byAge))
	assert.Equal(t, MemorySortAge, byAge.Sort)
}

// A tracker with no memories at all — and one with no config table — is an empty
// view, not an error: the tab exists wherever the beads fingerprint does.
func TestMemoriesEmptyTracker(t *testing.T) {
	noMemories := memoryHistory()
	for ref := range noMemories.config {
		noMemories.config[ref] = map[string]string{"issue_prefix": "demo"}
	}
	v := buildMemories(t, noMemories, "")
	assert.Empty(t, v.Memories)
	assert.Equal(t, 0, v.Total)
	assert.Empty(t, noMemories.reads[1:], "with nothing to date, the walk must not run")

	noTable := memoryHistory()
	noTable.config = map[string]map[string]string{}
	empty := buildMemories(t, noTable, "")
	assert.Empty(t, empty.Memories)
	assert.Equal(t, 0, empty.Total)
}

// AppliesMemories is the beads fingerprint plus a config table carrying key and
// value. It sees shapes and never rows, so an empty config still gets the tab.
func TestAppliesMemories(t *testing.T) {
	configTable := browse.TableInfo{Name: "config", Columns: []browse.ColumnInfo{
		{Name: "key", PrimaryKey: true}, {Name: "value"},
	}}
	full := append(beadsTables(), configTable)

	assert.True(t, AppliesMemories(full))
	assert.False(t, AppliesMemories(beadsTables()), "no config table")
	assert.False(t, AppliesMemories([]browse.TableInfo{configTable}), "config without the beads fingerprint")
	assert.False(t, AppliesMemories(append(beadsTables(),
		browse.TableInfo{Name: "config", Columns: []browse.ColumnInfo{{Name: "key"}}})),
		"a config table without a value column is somebody else's config")
}

// --- the revision walk -------------------------------------------------------

// The walk attributes a memory to the commit that changed its value, skips the
// commits that did not touch config without reading a row, and — when the
// history itself runs out — attributes what never changed to the root commit.
func TestMemoryRevisionWalk(t *testing.T) {
	sess := memoryHistory()
	v := buildMemories(t, sess, "")
	require.Len(t, v.Memories, 3)

	byslug := map[string]Memory{}
	for _, m := range v.Memories {
		byslug[m.Slug] = m
	}

	// alpha changed between c2 and c3, so c3 wrote it — not c4, which merely has
	// the same value, and not c2, which has the older one.
	alpha := byslug["alpha"]
	require.NotNil(t, alpha.Revision)
	assert.Equal(t, "c3", alpha.Revision.Commit)
	assert.Equal(t, memoryNow.Add(-2*24*time.Hour), alpha.Revision.Date)
	assert.Equal(t, "bigbes", alpha.Revision.Author)
	assert.False(t, alpha.Stale, "two days old")

	// beta appeared at c1 (absent at the root) and was left alone since.
	beta := byslug["beta"]
	require.NotNil(t, beta.Revision)
	assert.Equal(t, "c1", beta.Revision.Commit)
	assert.Equal(t, "alice", beta.Revision.Author)
	assert.True(t, beta.Stale, "99 days old")

	// gamma never changed anywhere in the history: the root is what wrote it.
	gamma := byslug["gamma"]
	require.NotNil(t, gamma.Revision)
	assert.Equal(t, "c0", gamma.Revision.Commit)
	assert.True(t, gamma.Stale, "200 days old")
	assert.False(t, v.WalkTruncated, "the whole history fits inside the walk")

	// The cost claim: rows were read at the head and at the two commits whose
	// config hash differs from their newer neighbour's. c3 and c1 are byte-equal
	// to the commit above them and were skipped without a read; c4 is the head
	// itself, already read once.
	assert.Equal(t, []string{"main", "c2", "c0"}, sess.reads)
	assert.Equal(t, 5, sess.hashCalls, "one O(1) table hash per commit")
	assert.Equal(t, 1, sess.logCalls, "one log call for the whole walk")
}

// A key that never changes inside the walk's budget gets no date at all rather
// than a date the walk cannot support — and the view says the walk was cut off,
// which is the only way a memory can carry no revision.
func TestMemoryRevisionUnresolvedWithinWalk(t *testing.T) {
	sess := &fakeHistory{
		config: map[string]map[string]string{"main": {"kv.memory.ancient": "unchanged"}},
		hashes: map[string]string{"main": "h"},
	}
	// One commit more than the walk examines, none of which touched config.
	for i := 0; i <= memoryWalkMax; i++ {
		h := fmt.Sprintf("k%03d", i)
		sess.commits = append(sess.commits, browse.CommitInfo{
			Hash: h, Author: "bigbes", Date: memoryNow.Add(-time.Duration(i) * time.Hour),
		})
		sess.config[h] = map[string]string{"kv.memory.ancient": "unchanged"}
		sess.hashes[h] = "h"
	}

	v := buildMemories(t, sess, "")
	require.Len(t, v.Memories, 1)
	assert.Nil(t, v.Memories[0].Revision, "no date the walk cannot support")
	assert.True(t, v.WalkTruncated)
	assert.Equal(t, memoryWalkMax, v.WalkMax)
	// Still older than the threshold? The walk's own oldest commit is 500 hours
	// back, which is short of 60 days, so the question is not asked either.
	assert.False(t, v.Memories[0].Stale)

	assert.Equal(t, []string{"main"}, sess.reads,
		"500 commits that did not touch config must cost no row read at all")
	assert.Equal(t, memoryWalkMax, sess.hashCalls)
}

// The undated arm of staleness: with no revision, the walk's oldest commit is a
// floor on the memory's age, and a floor already past the threshold supports the
// question.
func TestMemoryStaleWithoutARevision(t *testing.T) {
	sess := &fakeHistory{
		config: map[string]map[string]string{"main": {"kv.memory.ancient": "unchanged"}},
		hashes: map[string]string{"main": "h"},
	}
	for i := 0; i <= memoryWalkMax; i++ {
		h := fmt.Sprintf("k%03d", i)
		sess.commits = append(sess.commits, browse.CommitInfo{
			Hash: h, Date: memoryNow.Add(-time.Duration(i) * 24 * time.Hour),
		})
		sess.config[h] = map[string]string{"kv.memory.ancient": "unchanged"}
		sess.hashes[h] = "h"
	}

	v := buildMemories(t, sess, "")
	require.Len(t, v.Memories, 1)
	assert.Nil(t, v.Memories[0].Revision)
	assert.True(t, v.Memories[0].Stale, "the oldest commit walked is 499 days back")
}

// A commit that created the config table is attributed correctly: walking past
// it, the table is simply absent, and absent is an ordinary answer rather than
// an error.
func TestMemoryWalkPastTheTablesCreation(t *testing.T) {
	sess := &fakeHistory{
		commits: []browse.CommitInfo{
			{Hash: "b2", Author: "bigbes", Date: memoryNow.Add(-time.Hour)},
			{Hash: "b1", Author: "bigbes", Date: memoryNow.Add(-48 * time.Hour)},
			{Hash: "b0", Author: "bigbes", Date: memoryNow.Add(-72 * time.Hour)},
		},
		config: map[string]map[string]string{
			"main": {"kv.memory.first": "hello"},
			"b2":   {"kv.memory.first": "hello"},
			"b1":   {"kv.memory.first": "hello"},
			// b0 predates the config table entirely: no entry at all.
		},
		hashes: map[string]string{"main": "h1", "b2": "h1", "b1": "h1"},
	}

	v := buildMemories(t, sess, "")
	require.Len(t, v.Memories, 1)
	require.NotNil(t, v.Memories[0].Revision)
	assert.Equal(t, "b1", v.Memories[0].Revision.Commit,
		"the commit that created config is the one that wrote the memory")
	assert.False(t, v.WalkTruncated)
}

// A history that cannot be read is an error, not a page of memories with every
// date quietly missing — that page is indistinguishable from a tracker whose
// memories are all older than the walk.
func TestMemoriesFailWhenTheHistoryCannotBeRead(t *testing.T) {
	sess := &failingLog{fakeHistory: memoryHistory()}
	_, err := BuildMemories(context.Background(), sess, "main", url.Values{}, memoryNow)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "read history")
}

type failingLog struct{ *fakeHistory }

func (*failingLog) Log(context.Context, string, string, int) ([]browse.CommitInfo, string, error) {
	return nil, "", fmt.Errorf("browse: walk commits: corrupt chunk")
}