Skip to content

Commit e9f319e

Browse files
committed
extended: stabilize cap tests and add coverage tests
- Cap tests now use mock embedder, small per-test caps via testCapBytes, smaller atoms, and defer em.Close() so race tests run quickly and TempDir cleanup succeeds reliably. - TestEvictionAllPinned now expects AddAtom to fail when pinned atoms fill the cap, matching the fail-closed behavior. - Add TestCapFailClosedWhenAllPinned. - Add coverage tests for SetEmbedder, SetEmbedderFactory, MarkDirty, Compact, UserModel, Associations, and AddAtoms batch failure path.
1 parent 24494ec commit e9f319e

1 file changed

Lines changed: 159 additions & 22 deletions

File tree

internal/memory/extended/extended_memory_test.go

Lines changed: 159 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -295,15 +295,18 @@ func TestEvictionCapEnforced(t *testing.T) {
295295
dir := t.TempDir()
296296
cfg := DefaultConfig()
297297
cfg.Enabled = boolPtr(true)
298-
cfg.MaxSizeMB = 1
299-
cfg.AtomMaxChars = 1_000_000
298+
cfg.AtomMaxChars = 100_000
300299
em := New(dir, newMockLLM(), cfg)
301-
for i := 0; i < 4; i++ {
302-
atom := MemoryAtom{Text: strings.Repeat("x", 600_000), SourceClass: SourceUserSaid}
300+
em.testCapBytes = 50 * 1024
301+
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
302+
em.index.emb = newMockEmbedder(vectorDim)
303+
defer em.Close()
304+
for i := 0; i < 10; i++ {
305+
atom := MemoryAtom{Text: strings.Repeat("x", 8_000), SourceClass: SourceUserSaid}
303306
_ = em.AddAtom(context.Background(), atom)
304307
}
305308
atoms, _ := em.List()
306-
if len(atoms) >= 4 {
309+
if len(atoms) >= 10 {
307310
t.Errorf("expected eviction to reduce atom count, got %d", len(atoms))
308311
}
309312
}
@@ -312,10 +315,13 @@ func TestEvictionPinProtected(t *testing.T) {
312315
dir := t.TempDir()
313316
cfg := DefaultConfig()
314317
cfg.Enabled = boolPtr(true)
315-
cfg.MaxSizeMB = 1
316-
cfg.AtomMaxChars = 1_000_000
318+
cfg.AtomMaxChars = 100_000
317319
em := New(dir, newMockLLM(), cfg)
318-
pinned := MemoryAtom{Text: strings.Repeat("pinned", 150_000), SourceClass: SourceUserSaid}
320+
em.testCapBytes = 50 * 1024
321+
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
322+
em.index.emb = newMockEmbedder(vectorDim)
323+
defer em.Close()
324+
pinned := MemoryAtom{Text: strings.Repeat("pinned", 5_000), SourceClass: SourceUserSaid}
319325
_ = em.AddAtom(context.Background(), pinned)
320326
atoms, _ := em.List()
321327
if len(atoms) != 1 {
@@ -324,8 +330,8 @@ func TestEvictionPinProtected(t *testing.T) {
324330
if err := em.store.Pin(atoms[0].ID, true); err != nil {
325331
t.Fatal(err)
326332
}
327-
for i := 0; i < 4; i++ {
328-
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 600_000), SourceClass: SourceUserSaid})
333+
for i := 0; i < 10; i++ {
334+
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 8_000), SourceClass: SourceUserSaid})
329335
}
330336
got, _ := em.store.Get(atoms[0].ID)
331337
if got.ID != atoms[0].ID {
@@ -377,10 +383,14 @@ func TestQuarantineCountsTowardSize(t *testing.T) {
377383
dir := t.TempDir()
378384
cfg := DefaultConfig()
379385
cfg.Enabled = boolPtr(true)
380-
cfg.MaxSizeMB = 1
386+
cfg.AtomMaxChars = 100_000
381387
em := New(dir, newMockLLM(), cfg)
382-
for i := 0; i < 4; i++ {
383-
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 600_000), SourceClass: SourceWeb})
388+
em.testCapBytes = 50 * 1024
389+
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
390+
em.index.emb = newMockEmbedder(vectorDim)
391+
defer em.Close()
392+
for i := 0; i < 10; i++ {
393+
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 8_000), SourceClass: SourceWeb})
384394
}
385395
if em.Size() == 0 {
386396
t.Error("expected non-zero size from quarantined atoms")
@@ -391,13 +401,14 @@ func TestCompactionTriggeredAfterHeavyEviction(t *testing.T) {
391401
dir := t.TempDir()
392402
cfg := DefaultConfig()
393403
cfg.Enabled = boolPtr(true)
394-
cfg.MaxSizeMB = 1
395-
cfg.AtomMaxChars = 1_000_000
404+
cfg.AtomMaxChars = 100_000
396405
em := New(dir, newMockLLM(), cfg)
406+
em.testCapBytes = 50 * 1024
397407
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
398408
em.index.emb = newMockEmbedder(vectorDim)
399-
for i := 0; i < 8; i++ {
400-
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 200_000), SourceClass: SourceUserSaid})
409+
defer em.Close()
410+
for i := 0; i < 12; i++ {
411+
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 6_000), SourceClass: SourceUserSaid})
401412
}
402413
// Heavy eviction should have triggered compaction.
403414
if em.Size() == 0 {
@@ -613,25 +624,73 @@ func TestEvictionAllPinned(t *testing.T) {
613624
dir := t.TempDir()
614625
cfg := DefaultConfig()
615626
cfg.Enabled = boolPtr(true)
616-
cfg.MaxSizeMB = 1
617-
cfg.AtomMaxChars = 1_000_000
627+
cfg.AtomMaxChars = 100_000
618628
em := New(dir, newMockLLM(), cfg)
629+
em.testCapBytes = 50 * 1024
630+
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
631+
em.index.emb = newMockEmbedder(vectorDim)
632+
defer em.Close()
619633
for i := 0; i < 3; i++ {
620-
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("p", 150_000), SourceClass: SourceUserSaid})
634+
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("p", 8_000), SourceClass: SourceUserSaid})
621635
}
622636
live, _ := em.List()
623637
for _, a := range live {
624638
_ = em.store.Pin(a.ID, true)
625639
}
626-
// Adding another atom should not evict pinned atoms; error or no-op is acceptable.
627-
_ = em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 600_000), SourceClass: SourceUserSaid})
640+
// Adding another atom should fail because no evictable atoms exist.
641+
err := em.AddAtom(context.Background(), MemoryAtom{Text: strings.Repeat("x", 30_000), SourceClass: SourceUserSaid})
642+
if err == nil {
643+
t.Error("expected AddAtom to fail when all atoms are pinned and cap would be exceeded")
644+
}
628645
for _, a := range live {
629646
if _, err := em.store.Get(a.ID); err != nil {
630647
t.Error("pinned atom was evicted")
631648
}
632649
}
633650
}
634651

652+
func TestCapFailClosedWhenAllPinned(t *testing.T) {
653+
dir := t.TempDir()
654+
cfg := DefaultConfig()
655+
cfg.Enabled = boolPtr(true)
656+
cfg.AtomMaxChars = 100_000
657+
em := New(dir, newMockLLM(), cfg)
658+
em.testCapBytes = 50 * 1024
659+
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
660+
em.index.emb = newMockEmbedder(vectorDim)
661+
defer em.Close()
662+
663+
// Fill the store with pinned atoms that consume nearly the whole cap.
664+
for i := 0; i < 5; i++ {
665+
a := MemoryAtom{Text: strings.Repeat("p", 8_000), SourceClass: SourceUserSaid}
666+
if err := em.AddAtom(context.Background(), a); err != nil {
667+
break
668+
}
669+
}
670+
live, _ := em.List()
671+
if len(live) == 0 {
672+
t.Fatal("expected at least one live atom")
673+
}
674+
for _, a := range live {
675+
_ = em.store.Pin(a.ID, true)
676+
}
677+
678+
before, _ := em.store.List()
679+
sz, _ := em.store.Size()
680+
if sz <= 0 {
681+
t.Fatal("expected positive store size")
682+
}
683+
// Attempt to add another atom; it must be rejected.
684+
incoming := MemoryAtom{Text: strings.Repeat("x", 30_000), SourceClass: SourceUserSaid}
685+
if err := em.AddAtom(context.Background(), incoming); err == nil {
686+
t.Error("expected AddAtom to fail when cap exceeded and all atoms pinned")
687+
}
688+
after, _ := em.store.List()
689+
if len(after) != len(before) {
690+
t.Errorf("expected pinned atom count unchanged, got %d before %d after", len(before), len(after))
691+
}
692+
}
693+
635694
func TestCapDisabled(t *testing.T) {
636695
dir := t.TempDir()
637696
cfg := DefaultConfig()
@@ -702,3 +761,81 @@ func TestQuarantineScanBeforeStore(t *testing.T) {
702761
t.Errorf("expected 0 quarantined atoms after scan rejection, got %d", len(quarantined))
703762
}
704763
}
764+
765+
func TestSetEmbedderAndFactory(t *testing.T) {
766+
dir := t.TempDir()
767+
cfg := DefaultConfig()
768+
cfg.Enabled = boolPtr(true)
769+
em := New(dir, newMockLLM(), cfg)
770+
defer em.Close()
771+
772+
emb := newMockEmbedder(vectorDim)
773+
em.SetEmbedder(emb)
774+
if em.index.emb != emb {
775+
t.Error("SetEmbedder did not set active embedder")
776+
}
777+
778+
called := false
779+
em.SetEmbedderFactory(func() embedding.TextEmbedder {
780+
called = true
781+
return newMockEmbedder(vectorDim)
782+
})
783+
_ = em.index.newEmb()
784+
if !called {
785+
t.Error("SetEmbedderFactory did not replace factory")
786+
}
787+
}
788+
789+
func TestMarkDirtyAndCompact(t *testing.T) {
790+
dir := t.TempDir()
791+
cfg := DefaultConfig()
792+
cfg.Enabled = boolPtr(true)
793+
em := New(dir, newMockLLM(), cfg)
794+
em.index.newEmb = func() embedding.TextEmbedder { return newMockEmbedder(vectorDim) }
795+
em.index.emb = newMockEmbedder(vectorDim)
796+
em.MarkDirty()
797+
if !em.index.dirty {
798+
t.Error("MarkDirty did not mark index dirty")
799+
}
800+
em.Compact()
801+
em.Close()
802+
// Compaction should complete without panic; background goroutine drains.
803+
}
804+
805+
func TestUserModelAndAssociationsStubs(t *testing.T) {
806+
um := NewUserModel()
807+
um.Update(MemoryAtom{Text: "x"}) // should not panic
808+
if got := um.Summary(); got != "" {
809+
t.Errorf("expected empty summary, got %q", got)
810+
}
811+
812+
assoc := NewAssociations()
813+
assoc.Link("a", "b") // should not panic
814+
if got := assoc.Related("a"); got != nil {
815+
t.Errorf("expected nil related atoms, got %v", got)
816+
}
817+
}
818+
819+
func TestAddAtomsBatchFailurePath(t *testing.T) {
820+
dir := t.TempDir()
821+
cfg := DefaultConfig()
822+
cfg.Enabled = boolPtr(true)
823+
em := New(dir, newMockLLM(), cfg)
824+
defer em.Close()
825+
826+
// Inject an invalid atom that the store will reject to exercise the error
827+
// logging path; valid atoms should still be added.
828+
atoms := []MemoryAtom{
829+
{ID: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", Text: "valid one", SourceClass: SourceUserSaid},
830+
{ID: "", Text: "", SourceClass: SourceUserSaid}, // empty text will be rejected
831+
{ID: "b1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", Text: "valid two", SourceClass: SourceUserSaid},
832+
}
833+
if err := em.AddAtoms(context.Background(), atoms); err != nil {
834+
t.Fatalf("AddAtoms returned unexpected error: %v", err)
835+
}
836+
live, _ := em.List()
837+
if len(live) != 2 {
838+
t.Errorf("expected 2 live atoms, got %d", len(live))
839+
}
840+
}
841+

0 commit comments

Comments
 (0)