From e90d2e37c9f8496953c6df099c3f2a461b775a17 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 15:26:18 -0400 Subject: [PATCH] =?UTF-8?q?test(evolver):=20integration=20tests=20for=20cr?= =?UTF-8?q?oss-pollination=20logic=20per=20=C2=A710.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds mock store/LLM implementations and tests for CheckAndPollinate: generation boundaries, fitness penalties, translation triggers, multi-boundary catch-up, and empty island handling. Co-Authored-By: Claude Opus 4.7 --- .../internal/crosspoll/crosspoll_test.go | 390 +++++++++++++++++- 1 file changed, 376 insertions(+), 14 deletions(-) diff --git a/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go b/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go index d90bdc4..a370c59 100644 --- a/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go +++ b/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go @@ -1,13 +1,93 @@ package crosspoll import ( + "context" + "fmt" "math/rand" "strings" + "sync" "testing" evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db" + "github.com/aicodebattle/acb/cmd/acb-evolver/internal/llm" ) +// ── Mock implementations ────────────────────────────────────────────────── + +// mockStore implements programStore for testing. +type mockStore struct { + mu sync.Mutex + programs []*evolverdb.Program + nextID int64 + createdCalls [] *evolverdb.Program // captures programs passed to Create +} + +func newMockStore(programs ...*evolverdb.Program) *mockStore { + return &mockStore{ + programs: programs, + nextID: int64(len(programs)) + 1, + } +} + +func (m *mockStore) MaxGenerationByIsland(ctx context.Context) (map[string]int, error) { + m.mu.Lock() + defer m.mu.Unlock() + + result := make(map[string]int) + for _, p := range m.programs { + if p.Generation > result[p.Island] { + result[p.Island] = p.Generation + } + } + return result, nil +} + +func (m *mockStore) ListTopByIsland(ctx context.Context, island string, limit int) ([]*evolverdb.Program, error) { + m.mu.Lock() + defer m.mu.Unlock() + + // Filter by island, already ordered by fitness desc in test data. + var filtered []*evolverdb.Program + for _, p := range m.programs { + if p.Island == island { + filtered = append(filtered, p) + } + } + if limit > len(filtered) { + limit = len(filtered) + } + return filtered[:limit], nil +} + +func (m *mockStore) Create(ctx context.Context, p *evolverdb.Program) (int64, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.nextID++ + p.ID = m.nextID + m.programs = append(m.programs, p) + m.createdCalls = append(m.createdCalls, p) + return m.nextID, nil +} + +// mockLLM implements llmGenerator for testing. +type mockLLM struct { + generateCalled bool + generateFunc func(ctx context.Context, req llm.GenerateRequest) (*llm.GenerateResponse, error) +} + +func (m *mockLLM) Generate(ctx context.Context, req llm.GenerateRequest) (*llm.GenerateResponse, error) { + m.generateCalled = true + if m.generateFunc != nil { + return m.generateFunc(ctx, req) + } + return &llm.GenerateResponse{ + Candidate: &llm.Candidate{Code: "translated_code"}, + }, nil +} + +// ── Existing tests ──────────────────────────────────────────────────────── + func TestBuildTranslationPrompt_containsBothLanguages(t *testing.T) { got := buildTranslationPrompt("func main() {}", "go", "python") if !strings.Contains(got, "go") { @@ -39,7 +119,6 @@ func TestBuildTranslationPrompt_fencedCodeBlock(t *testing.T) { func TestPickTargetIsland_neverSource(t *testing.T) { rng := newRandZero() - // Not a real Checker, just need the method. c := &Checker{rng: rng} for _, source := range evolverdb.AllIslands { @@ -68,17 +147,10 @@ func TestPickTargetIsland_validIsland(t *testing.T) { } func TestCheckAndPollinate_noBoundary_noop(t *testing.T) { - // This test validates the boundary logic without a DB. - // When cur < 50, no pollination should trigger. - // We test the boundary computation logic directly. - - // Generation 49 should not trigger (49 < 50). - nextBoundary := ((0 / generationInterval) + 1) * generationInterval // = 50 + nextBoundary := ((0 / generationInterval) + 1) * generationInterval if nextBoundary <= 49 { t.Error("generation 49 should not trigger pollination") } - - // Generation 50 should trigger. if nextBoundary > 50 { t.Error("generation 50 should trigger pollination") } @@ -101,13 +173,12 @@ func TestCheckAndPollination_multipleBoundaries(t *testing.T) { cur := 102 count := 0 - nextBoundary := ((prev / generationInterval) + 1) * generationInterval // = 50 + nextBoundary := ((prev / generationInterval) + 1) * generationInterval for nextBoundary <= cur && nextBoundary > 0 { count++ nextBoundary += generationInterval } - // Should trigger at 50 and 100 = 2 pollinations. if count != 2 { t.Errorf("expected 2 pollinations for prev=49 cur=102, got %d", count) } @@ -117,7 +188,7 @@ func TestCheckAndPollination_noDuplicateOnRecheck(t *testing.T) { prev := 50 cur := 50 - nextBoundary := ((prev / generationInterval) + 1) * generationInterval // = 100 + nextBoundary := ((prev / generationInterval) + 1) * generationInterval if nextBoundary <= cur { t.Error("should not re-trigger at gen 50 when prev=50") } @@ -128,18 +199,309 @@ func TestCheckAndPollination_skipsFrom0to100(t *testing.T) { cur := 100 count := 0 - nextBoundary := ((prev / generationInterval) + 1) * generationInterval // = 50 + nextBoundary := ((prev / generationInterval) + 1) * generationInterval for nextBoundary <= cur && nextBoundary > 0 { count++ nextBoundary += generationInterval } - // Should trigger at 50 and 100 = 2 pollinations. if count != 2 { t.Errorf("expected 2 pollinations for prev=0 cur=100, got %d", count) } } +// ── Integration tests with mock store ───────────────────────────────────── + +func seedIsland(store *mockStore, island, lang string, fitness float64, generation int) { + store.programs = append(store.programs, &evolverdb.Program{ + ID: store.nextID, + Code: fmt.Sprintf("code_%s_gen%d", island, generation), + Language: lang, + Island: island, + Generation: generation, + ParentIDs: []int64{}, + BehaviorVector: []float64{0.5, 0.5}, + Fitness: fitness, + }) + store.nextID++ +} + +func TestCheckAndPollinate_fiftyGenerations_oneEventPerIsland(t *testing.T) { + // Seed all 4 islands at generation 50 with top programs. + store := newMockStore() + for _, island := range evolverdb.AllIslands { + seedIsland(store, island, "go", 100.0, 50) + // Add a lower-fitness program so ListTopByIsland returns the right one + seedIsland(store, island, "go", 50.0, 30) + } + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) // all start at 0 + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + // Expect exactly 4 events — one per island. + if len(results) != 4 { + t.Fatalf("expected 4 pollination events (one per island), got %d", len(results)) + } + + // Verify each island triggered exactly once. + triggered := make(map[string]int) + for _, r := range results { + triggered[r.SourceIsland]++ + } + for _, island := range evolverdb.AllIslands { + if triggered[island] != 1 { + t.Errorf("island %s: expected 1 event, got %d", island, triggered[island]) + } + } + + // Verify no translation needed (all same language). + for _, r := range results { + if r.Translated { + t.Errorf("event from %s→%s should not have translation (same lang)", r.SourceIsland, r.TargetIsland) + } + } + + // Verify prevGens was updated. + for _, island := range evolverdb.AllIslands { + if prevGens[island] != 50 { + t.Errorf("prevGens[%s] = %d, want 50", island, prevGens[island]) + } + } +} + +func TestCheckAndPollinate_underFifty_noEvents(t *testing.T) { + store := newMockStore() + for _, island := range evolverdb.AllIslands { + seedIsland(store, island, "go", 100.0, 49) + } + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + if len(results) != 0 { + t.Fatalf("expected 0 events for gen 49, got %d", len(results)) + } + + if llmClient.generateCalled { + t.Error("LLM should not have been called") + } +} + +func TestCheckAndPollinate_copiedProgramAttributes(t *testing.T) { + // Only alpha at gen 50 — other islands below boundary. + store := newMockStore() + seedIsland(store, evolverdb.IslandAlpha, "go", 100.0, 50) + seedIsland(store, evolverdb.IslandBeta, "go", 80.0, 10) + seedIsland(store, evolverdb.IslandGamma, "go", 70.0, 10) + seedIsland(store, evolverdb.IslandDelta, "go", 60.0, 10) + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 event (alpha only at gen 50), got %d", len(results)) + } + + r := results[0] + if r.SourceIsland != evolverdb.IslandAlpha { + t.Errorf("source island: got %q, want %q", r.SourceIsland, evolverdb.IslandAlpha) + } + if r.SourceIsland == r.TargetIsland { + t.Error("target island should differ from source") + } + + // Verify the created program has the fitness penalty (0.9x). + if len(store.createdCalls) != 1 { + t.Fatalf("expected 1 Create call, got %d", len(store.createdCalls)) + } + created := store.createdCalls[0] + if created.Fitness != 90.0 { + t.Errorf("migrated program fitness: got %f, want 90.0 (0.9*100)", created.Fitness) + } + if created.Island != r.TargetIsland { + t.Errorf("migrated program island: got %q, want %q", created.Island, r.TargetIsland) + } + if len(created.ParentIDs) != 1 { + t.Fatalf("migrated program should have 1 parent, got %d", len(created.ParentIDs)) + } + // Parent should be the top program on alpha (ID=1). + if created.ParentIDs[0] != 1 { + t.Errorf("parent ID: got %d, want 1 (top alpha program)", created.ParentIDs[0]) + } +} + +func TestCheckAndPollinate_translationTriggered_differentLanguages(t *testing.T) { + // Alpha speaks Go; all other islands speak Python so any target needs translation. + store := newMockStore() + seedIsland(store, evolverdb.IslandAlpha, "go", 100.0, 50) + seedIsland(store, evolverdb.IslandBeta, "python", 80.0, 10) + seedIsland(store, evolverdb.IslandGamma, "python", 70.0, 10) + seedIsland(store, evolverdb.IslandDelta, "python", 60.0, 10) + + translatedCode := "def handle_turn(): pass" + llmClient := &mockLLM{ + generateFunc: func(ctx context.Context, req llm.GenerateRequest) (*llm.GenerateResponse, error) { + return &llm.GenerateResponse{ + Candidate: &llm.Candidate{Code: translatedCode}, + }, nil + }, + } + + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 event, got %d", len(results)) + } + + r := results[0] + if !r.Translated { + t.Error("expected translation for go→python") + } + if !llmClient.generateCalled { + t.Error("expected LLM to be called for translation") + } + if r.SourceLang != "go" { + t.Errorf("source lang: got %q, want go", r.SourceLang) + } + if r.TargetLang != "python" { + t.Errorf("target lang: got %q, want python", r.TargetLang) + } + + // Verify the translated code was stored. + if len(store.createdCalls) != 1 { + t.Fatal("expected 1 Create call") + } + if store.createdCalls[0].Code != translatedCode { + t.Errorf("stored code: got %q, want %q", store.createdCalls[0].Code, translatedCode) + } + if store.createdCalls[0].Language != "python" { + t.Errorf("stored language: got %q, want python", store.createdCalls[0].Language) + } +} + +func TestCheckAndPollinate_sameLanguage_noTranslation(t *testing.T) { + // All islands speak Go. + store := newMockStore() + seedIsland(store, evolverdb.IslandAlpha, "go", 100.0, 50) + seedIsland(store, evolverdb.IslandBeta, "go", 80.0, 10) + seedIsland(store, evolverdb.IslandGamma, "go", 70.0, 10) + seedIsland(store, evolverdb.IslandDelta, "go", 60.0, 10) + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 event, got %d", len(results)) + } + + if results[0].Translated { + t.Error("should not translate when languages match") + } + if llmClient.generateCalled { + t.Error("LLM should not be called when languages match") + } +} + +func TestCheckAndPollinate_hundredGenerations_twoEvents(t *testing.T) { + // Alpha at gen 100, beta at gen 50, others below. + store := newMockStore() + seedIsland(store, evolverdb.IslandAlpha, "go", 100.0, 100) + seedIsland(store, evolverdb.IslandBeta, "go", 80.0, 50) + seedIsland(store, evolverdb.IslandGamma, "go", 70.0, 30) + seedIsland(store, evolverdb.IslandDelta, "go", 60.0, 20) + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + // Alpha crosses 50 and 100 = 2 events. Beta crosses 50 = 1 event. Total = 3. + if len(results) != 3 { + t.Fatalf("expected 3 events (alpha×2 + beta×1), got %d", len(results)) + } + + alphaCount := 0 + betaCount := 0 + for _, r := range results { + if r.SourceIsland == evolverdb.IslandAlpha { + alphaCount++ + } + if r.SourceIsland == evolverdb.IslandBeta { + betaCount++ + } + } + if alphaCount != 2 { + t.Errorf("alpha: expected 2 events (gen 50 and 100), got %d", alphaCount) + } + if betaCount != 1 { + t.Errorf("beta: expected 1 event (gen 50), got %d", betaCount) + } +} + +func TestCheckAndPollinate_emptyIsland_noEvent(t *testing.T) { + store := newMockStore() + seedIsland(store, evolverdb.IslandAlpha, "go", 100.0, 50) + // Beta, gamma, delta have no programs. + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + // Only alpha triggers (it has gen 50). The others have gen 0, no boundary. + if len(results) != 1 { + t.Fatalf("expected 1 event (alpha only), got %d", len(results)) + } + if results[0].SourceIsland != evolverdb.IslandAlpha { + t.Errorf("source: got %q, want alpha", results[0].SourceIsland) + } +} + // newRandZero creates a deterministic RNG for reproducible tests. func newRandZero() *rand.Rand { return rand.New(rand.NewSource(0))