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