memory_test.go raw
1 package memory
2
3 import (
4 "testing"
5 "time"
6
7 "git.mleku.dev/mleku/dendrite/pkg/ratio"
8 "git.mleku.dev/mleku/dendrite/pkg/spore"
9 )
10
11 func tmpDB(t *testing.T) *DB {
12 t.Helper()
13 dir := t.TempDir()
14 db, err := Open(dir)
15 if err != nil {
16 t.Fatalf("open: %v", err)
17 }
18 t.Cleanup(func() { db.Close() })
19 return db
20 }
21
22 func TestKeyRoundTrip(t *testing.T) {
23 // TagHash determinism.
24 h1 := TagHash("func")
25 h2 := TagHash("func")
26 if h1 != h2 {
27 t.Fatal("TagHash not deterministic")
28 }
29 h3 := TagHash("type")
30 if h1 == h3 {
31 t.Fatal("different tags should produce different hashes")
32 }
33
34 // GenKey decode.
35 key := GenKey(42)
36 if DecodeGen(key) != 42 {
37 t.Fatal("GenKey round-trip")
38 }
39
40 // TypKey decode.
41 tk := TypKey(h1, 100, 7)
42 th, count, gen := DecodeTyp(tk)
43 if th != h1 || count != 100 || gen != 7 {
44 t.Fatalf("TypKey round-trip: %v %d %d", th, count, gen)
45 }
46
47 // BndKey decode.
48 bk := BndKey(h1, 5, 999)
49 bth, bgen, bsite := DecodeBnd(bk)
50 if bth != h1 || bgen != 5 || bsite != 999 {
51 t.Fatalf("BndKey round-trip: %v %d %d", bth, bgen, bsite)
52 }
53
54 // MisKey decode.
55 mk := MisKey(h3, 3, 50)
56 mth, mgen, mcount := DecodeMis(mk)
57 if mth != h3 || mgen != 3 || mcount != 50 {
58 t.Fatalf("MisKey round-trip: %v %d %d", mth, mgen, mcount)
59 }
60
61 // FitKey decode.
62 fk := FitKey(DimBehav, 10)
63 fdim, fgen := DecodeFit(fk)
64 if fdim != DimBehav || fgen != 10 {
65 t.Fatalf("FitKey round-trip: %d %d", fdim, fgen)
66 }
67
68 // HexKey decode.
69 hk := HexKey(3, 15)
70 hop, hgen := DecodeHex(hk)
71 if hop != 3 || hgen != 15 {
72 t.Fatalf("HexKey round-trip: %d %d", hop, hgen)
73 }
74 }
75
76 func TestValueRoundTrip(t *testing.T) {
77 // Fitness value.
78 v := EncodeFitValue(3, 4)
79 n, d := DecodeFitValue(v)
80 if n != 3 || d != 4 {
81 t.Fatalf("FitValue round-trip: %d/%d", n, d)
82 }
83
84 // Health value.
85 hv := EncodeHltValue(100, 256, 3, 4)
86 occ, tot, hn, hd := DecodeHltValue(hv)
87 if occ != 100 || tot != 256 || hn != 3 || hd != 4 {
88 t.Fatalf("HltValue round-trip: %d %d %d/%d", occ, tot, hn, hd)
89 }
90
91 // U32 value.
92 uv := EncodeU32Value(42)
93 if DecodeU32Value(uv) != 42 {
94 t.Fatal("U32Value round-trip")
95 }
96 }
97
98 func TestRecordAndQueryGeneration(t *testing.T) {
99 db := tmpDB(t)
100
101 if err := db.RecordGeneration(0, "", 0, time.Now()); err != nil {
102 t.Fatalf("record gen 0: %v", err)
103 }
104 if err := db.RecordGeneration(1, "abc123", 0, time.Now()); err != nil {
105 t.Fatalf("record gen 1: %v", err)
106 }
107
108 meta := db.QueryGeneration(0)
109 if meta == nil {
110 t.Fatal("gen 0 not found")
111 }
112 if meta.InstanceID != 0 {
113 t.Fatalf("expected inst 0, got %d", meta.InstanceID)
114 }
115
116 meta1 := db.QueryGeneration(1)
117 if meta1 == nil || meta1.ParentHash != "abc123" {
118 t.Fatal("gen 1 parent hash mismatch")
119 }
120
121 // Nonexistent generation.
122 if db.QueryGeneration(99) != nil {
123 t.Fatal("expected nil for nonexistent gen")
124 }
125 }
126
127 func TestRecordAndQueryTypeSig(t *testing.T) {
128 db := tmpDB(t)
129
130 // Record type signatures for 3 generations.
131 for gen := uint32(0); gen < 3; gen++ {
132 err := db.RecordTypeSig(gen, []spore.TagCount{
133 {Tag: "func", Count: int(10 + gen*5)},
134 {Tag: "type", Count: int(20 + gen*3)},
135 })
136 if err != nil {
137 t.Fatalf("record type sig gen %d: %v", gen, err)
138 }
139 }
140
141 // Query func trend.
142 trend := db.QueryTypeTrend("func", 10)
143 if len(trend) != 3 {
144 t.Fatalf("expected 3 points, got %d", len(trend))
145 }
146 // Should be ascending by generation.
147 if trend[0].Gen != 0 || trend[1].Gen != 1 || trend[2].Gen != 2 {
148 t.Fatalf("wrong gen order: %v", trend)
149 }
150 if trend[0].Count != 10 || trend[1].Count != 15 || trend[2].Count != 20 {
151 t.Fatalf("wrong counts: %v", trend)
152 }
153
154 // Query with lastN limit.
155 limited := db.QueryTypeTrend("func", 2)
156 if len(limited) != 2 {
157 t.Fatalf("expected 2 points, got %d", len(limited))
158 }
159 // Should be the 2 most recent.
160 if limited[0].Gen != 1 || limited[1].Gen != 2 {
161 t.Fatalf("wrong limited gens: %v", limited)
162 }
163 }
164
165 func TestRecordAndQueryMissing(t *testing.T) {
166 db := tmpDB(t)
167
168 for gen := uint32(0); gen < 3; gen++ {
169 err := db.RecordMissing(gen, []spore.TagCount{
170 {Tag: "import", Count: int(5 + gen)},
171 })
172 if err != nil {
173 t.Fatalf("record missing gen %d: %v", gen, err)
174 }
175 }
176
177 trend := db.QueryMissingTrend("import", 10)
178 if len(trend) != 3 {
179 t.Fatalf("expected 3 points, got %d", len(trend))
180 }
181 if trend[0].Count != 5 || trend[2].Count != 7 {
182 t.Fatalf("wrong missing counts: %v", trend)
183 }
184 }
185
186 func TestRecordAndQueryBonds(t *testing.T) {
187 db := tmpDB(t)
188
189 // Gen 0: 3 func bonds.
190 err := db.RecordBonds(0, []BondRecord{
191 {Tag: "func", SiteID: 1},
192 {Tag: "func", SiteID: 2},
193 {Tag: "func", SiteID: 3},
194 })
195 if err != nil {
196 t.Fatal(err)
197 }
198
199 // Gen 1: 5 func bonds.
200 bonds := make([]BondRecord, 5)
201 for i := range bonds {
202 bonds[i] = BondRecord{Tag: "func", SiteID: uint32(10 + i)}
203 }
204 if err := db.RecordBonds(1, bonds); err != nil {
205 t.Fatal(err)
206 }
207
208 // Query single gen count.
209 if db.QueryBondCount("func", 0) != 3 {
210 t.Fatalf("expected 3 bonds in gen 0, got %d", db.QueryBondCount("func", 0))
211 }
212 if db.QueryBondCount("func", 1) != 5 {
213 t.Fatalf("expected 5 bonds in gen 1, got %d", db.QueryBondCount("func", 1))
214 }
215
216 // Bond history.
217 history := db.QueryBondHistory("func", 10)
218 if len(history) != 2 {
219 t.Fatalf("expected 2 gens, got %d", len(history))
220 }
221 if history[0].Gen != 0 || history[0].Count != 3 {
222 t.Fatalf("gen 0: %v", history[0])
223 }
224 if history[1].Gen != 1 || history[1].Count != 5 {
225 t.Fatalf("gen 1: %v", history[1])
226 }
227 }
228
229 func TestRecordAndQueryFitness(t *testing.T) {
230 db := tmpDB(t)
231
232 for gen := uint32(0); gen < 5; gen++ {
233 score := ratio.New(int64(gen), 10)
234 err := db.RecordFitness(gen, score, score, score, score)
235 if err != nil {
236 t.Fatal(err)
237 }
238 }
239
240 // Overall trajectory.
241 traj := db.QueryFitnessTrajectory(3)
242 if len(traj) != 3 {
243 t.Fatalf("expected 3, got %d", len(traj))
244 }
245 // Should be gens 2,3,4 ascending.
246 if traj[0].Gen != 2 || traj[2].Gen != 4 {
247 t.Fatalf("wrong gens: %v", traj)
248 }
249 // Gen 4: 4/10 = 2/5.
250 if traj[2].Score.Num != 2 || traj[2].Score.Denom != 5 {
251 t.Fatalf("wrong score: %v", traj[2].Score)
252 }
253
254 // Source dimension.
255 src := db.QueryFitnessDimension(DimSource, 5)
256 if len(src) != 5 {
257 t.Fatalf("expected 5, got %d", len(src))
258 }
259 }
260
261 func TestRecordAndQueryHealth(t *testing.T) {
262 db := tmpDB(t)
263
264 for gen := uint32(0); gen < 3; gen++ {
265 err := db.RecordHealth(gen, 100+gen, 256, ratio.New(int64(gen+1), 2))
266 if err != nil {
267 t.Fatal(err)
268 }
269 }
270
271 hist := db.QueryHealthHistory(10)
272 if len(hist) != 3 {
273 t.Fatalf("expected 3, got %d", len(hist))
274 }
275 if hist[0].Occupied != 100 || hist[2].Occupied != 102 {
276 t.Fatalf("wrong occupied: %v", hist)
277 }
278 if hist[1].AvgLockIn.Num != 1 || hist[1].AvgLockIn.Denom != 1 {
279 t.Fatalf("wrong avg lock-in: %v", hist[1].AvgLockIn)
280 }
281 }
282
283 func TestRecordAndQueryHexagramOps(t *testing.T) {
284 db := tmpDB(t)
285
286 for gen := uint32(0); gen < 3; gen++ {
287 ops := map[byte]uint32{
288 0: 10 + gen, // accrete
289 1: 5 + gen*2, // dissolve
290 }
291 if err := db.RecordHexagramOps(gen, ops); err != nil {
292 t.Fatal(err)
293 }
294 }
295
296 accrete := db.QueryHexagramOps(0, 10)
297 if len(accrete) != 3 {
298 t.Fatalf("expected 3, got %d", len(accrete))
299 }
300 if accrete[0].Count != 10 || accrete[2].Count != 12 {
301 t.Fatalf("wrong accrete counts: %v", accrete)
302 }
303
304 dissolve := db.QueryHexagramOps(1, 2)
305 if len(dissolve) != 2 {
306 t.Fatalf("expected 2, got %d", len(dissolve))
307 }
308 }
309
310 func TestRecordAndQueryConnectivity(t *testing.T) {
311 db := tmpDB(t)
312
313 err := db.RecordConnectivity(0, []spore.TagRatio{
314 {Tag: "func", Value: ratio.New(3, 1)},
315 {Tag: "type", Value: ratio.New(5, 2)},
316 })
317 if err != nil {
318 t.Fatal(err)
319 }
320
321 // Connectivity is stored per-key, not queried as a trend yet.
322 // Verify by reading the raw key.
323 h := TagHash("func")
324 meta := db.QueryGeneration(0)
325 // No gen metadata recorded, so nil is expected.
326 _ = meta
327 _ = h
328 }
329
330 func TestRecordAndQueryLockIn(t *testing.T) {
331 db := tmpDB(t)
332
333 buckets := map[byte]uint32{
334 0: 5, // very low lock-in
335 128: 20, // medium
336 255: 10, // saturated
337 }
338 if err := db.RecordLockInDist(0, buckets); err != nil {
339 t.Fatal(err)
340 }
341 // Lock-in distribution is stored but not yet queried as trend.
342 // This test verifies write succeeds without error.
343 }
344
345 func TestEmptyDB(t *testing.T) {
346 db := tmpDB(t)
347
348 // All queries should return empty/nil gracefully.
349 if trend := db.QueryTypeTrend("func", 10); len(trend) != 0 {
350 t.Fatalf("expected empty, got %d", len(trend))
351 }
352 if miss := db.QueryMissingTrend("func", 10); len(miss) != 0 {
353 t.Fatalf("expected empty, got %d", len(miss))
354 }
355 if traj := db.QueryFitnessTrajectory(10); len(traj) != 0 {
356 t.Fatalf("expected empty, got %d", len(traj))
357 }
358 if hist := db.QueryHealthHistory(10); len(hist) != 0 {
359 t.Fatalf("expected empty, got %d", len(hist))
360 }
361 if bonds := db.QueryBondHistory("func", 10); len(bonds) != 0 {
362 t.Fatalf("expected empty, got %d", len(bonds))
363 }
364 if db.QueryBondCount("func", 0) != 0 {
365 t.Fatal("expected 0 bonds")
366 }
367 if db.QueryGeneration(0) != nil {
368 t.Fatal("expected nil")
369 }
370 }
371
372 func TestGraphTraversal(t *testing.T) {
373 db := tmpDB(t)
374
375 // Simulate 5 generations with varying func bond counts and fitness.
376 for gen := uint32(0); gen < 5; gen++ {
377 bondCount := int(10 + gen*3)
378 bonds := make([]BondRecord, bondCount)
379 for i := range bonds {
380 bonds[i] = BondRecord{Tag: "func", SiteID: uint32(i)}
381 }
382 if err := db.RecordBonds(gen, bonds); err != nil {
383 t.Fatal(err)
384 }
385
386 // Fitness increases with generation.
387 score := ratio.New(int64(gen+1), 5)
388 if err := db.RecordFitness(gen, score, score, score, score); err != nil {
389 t.Fatal(err)
390 }
391
392 if err := db.RecordTypeSig(gen, []spore.TagCount{
393 {Tag: "func", Count: bondCount},
394 }); err != nil {
395 t.Fatal(err)
396 }
397 }
398
399 // Graph traversal: find gens where func bonds increased, cross-ref fitness.
400 bondHist := db.QueryBondHistory("func", 5)
401 fitTraj := db.QueryFitnessTrajectory(5)
402
403 if len(bondHist) != 5 || len(fitTraj) != 5 {
404 t.Fatalf("expected 5 each, got bonds=%d fit=%d", len(bondHist), len(fitTraj))
405 }
406
407 // Verify correlation: more bonds → higher fitness (by construction).
408 for i := 1; i < len(bondHist); i++ {
409 if bondHist[i].Count <= bondHist[i-1].Count {
410 t.Fatalf("bonds should increase: gen %d=%d, gen %d=%d",
411 bondHist[i-1].Gen, bondHist[i-1].Count,
412 bondHist[i].Gen, bondHist[i].Count)
413 }
414 if !fitTraj[i-1].Score.Less(fitTraj[i].Score) {
415 t.Fatalf("fitness should increase: gen %d=%v, gen %d=%v",
416 fitTraj[i-1].Gen, fitTraj[i-1].Score,
417 fitTraj[i].Gen, fitTraj[i].Score)
418 }
419 }
420 }
421