package registry import ( "os" "path/filepath" "testing" ) func tempDB(t *testing.T) *DB { t.Helper() path := filepath.Join(t.TempDir(), "test.db") db, err := Open(path) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return db } func TestInsertAndGetFunction(t *testing.T) { db := tempDB(t) f := &Function{ Name: "filter_slice", Kind: KindFunction, Lang: "go", Domain: "core", Version: "1.0.0", Purity: PurityPure, Signature: "func FilterSlice[T any](xs []T, pred func(T) bool) []T", Description: "Filtra un slice con un predicado sin mutar el original", Tags: []string{"slice", "functional", "generic"}, FilePath: "functions/core/filter_slice.go", } if err := db.InsertFunction(f); err != nil { t.Fatal(err) } if f.ID != "filter_slice_go_core" { t.Fatalf("expected ID filter_slice_go_core, got %s", f.ID) } got, err := db.GetFunction("filter_slice_go_core") if err != nil { t.Fatal(err) } if got.Name != "filter_slice" { t.Errorf("name: got %q, want %q", got.Name, "filter_slice") } if got.Purity != PurityPure { t.Errorf("purity: got %q, want %q", got.Purity, PurityPure) } if len(got.Tags) != 3 { t.Errorf("tags: got %d, want 3", len(got.Tags)) } } func TestInsertAndGetType(t *testing.T) { db := tempDB(t) typ := &Type{ Name: "ohlcv", Lang: "go", Domain: "finance", Version: "1.0.0", Algebraic: AlgebraicProduct, Definition: `type OHLCV struct { Open, High, Low, Close, Volume float64 }`, Description: "Vela de mercado con precios OHLCV", Tags: []string{"finance", "market", "candle"}, FilePath: "types/finance/ohlcv.go", } if err := db.InsertType(typ); err != nil { t.Fatal(err) } got, err := db.GetType("ohlcv_go_finance") if err != nil { t.Fatal(err) } if got.Algebraic != AlgebraicProduct { t.Errorf("algebraic: got %q, want %q", got.Algebraic, AlgebraicProduct) } } func TestSearchFunctionsFTS(t *testing.T) { db := tempDB(t) fns := []*Function{ {Name: "filter_slice", Kind: KindFunction, Lang: "go", Domain: "core", Purity: PurityPure, Description: "Filtra un slice con un predicado", Version: "1.0.0"}, {Name: "map_slice", Kind: KindFunction, Lang: "go", Domain: "core", Purity: PurityPure, Description: "Transforma cada elemento de un slice", Version: "1.0.0"}, {Name: "fetch_ticks", Kind: KindFunction, Lang: "go", Domain: "io", Purity: PurityImpure, Description: "Obtiene ticks de un exchange", ErrorType: "error_go_core", Version: "1.0.0"}, } for _, f := range fns { if err := db.InsertFunction(f); err != nil { t.Fatal(err) } } // FTS search results, err := db.SearchFunctions("slice", "", "", "", "") if err != nil { t.Fatal(err) } if len(results) != 2 { t.Errorf("FTS 'slice': got %d results, want 2", len(results)) } // Filter by purity results, err = db.SearchFunctions("", "", PurityImpure, "", "") if err != nil { t.Fatal(err) } if len(results) != 1 || results[0].Name != "fetch_ticks" { t.Errorf("filter impure: unexpected results %v", results) } // Filter by domain results, err = db.SearchFunctions("", "", "", "", "core") if err != nil { t.Fatal(err) } if len(results) != 2 { t.Errorf("filter domain=core: got %d, want 2", len(results)) } } func TestPurge(t *testing.T) { db := tempDB(t) db.InsertFunction(&Function{Name: "test_fn", Kind: KindFunction, Lang: "go", Domain: "core", Purity: PurityPure, Description: "test", Version: "1.0.0"}) db.InsertType(&Type{Name: "test_type", Lang: "go", Domain: "core", Algebraic: AlgebraicProduct, Description: "test", Version: "1.0.0"}) if err := db.Purge(); err != nil { t.Fatal(err) } fns, _ := db.SearchFunctions("", "", "", "", "") if len(fns) != 0 { t.Errorf("after purge: got %d functions, want 0", len(fns)) } ts, _ := db.SearchTypes("", "", "") if len(ts) != 0 { t.Errorf("after purge: got %d types, want 0", len(ts)) } } func TestProposalCRUD(t *testing.T) { db := tempDB(t) p := &Proposal{ ID: "proposal_test_1", Kind: ProposalNewFunction, Title: "Add retry with backoff", Description: "Exponential backoff for HTTP clients", Evidence: map[string]any{"assertion_ids": []any{"a1", "a2"}}, CreatedBy: "agent", } if err := db.InsertProposal(p); err != nil { t.Fatalf("insert: %v", err) } if p.Status != ProposalPending { t.Errorf("default status = %q, want pending", p.Status) } got, err := db.GetProposal("proposal_test_1") if err != nil { t.Fatalf("get: %v", err) } if got.Title != "Add retry with backoff" { t.Errorf("title = %q, want %q", got.Title, "Add retry with backoff") } if got.Evidence["assertion_ids"] == nil { t.Error("evidence should contain assertion_ids") } // Update got.Status = ProposalApproved got.ReviewedBy = "lucas" if err := db.UpdateProposal(got); err != nil { t.Fatalf("update: %v", err) } updated, _ := db.GetProposal("proposal_test_1") if updated.Status != ProposalApproved { t.Errorf("status = %q, want approved", updated.Status) } if updated.ReviewedBy != "lucas" { t.Errorf("reviewed_by = %q, want lucas", updated.ReviewedBy) } // List with filter db.InsertProposal(&Proposal{ ID: "proposal_test_2", Kind: ProposalImproveType, TargetID: "ohlcv_go_finance", Title: "Improve OHLCV", CreatedBy: "agent", }) all, err := db.ListProposals("", "") if err != nil { t.Fatalf("list all: %v", err) } if len(all) != 2 { t.Errorf("list all = %d, want 2", len(all)) } byKind, _ := db.ListProposals(ProposalNewFunction, "") if len(byKind) != 1 { t.Errorf("list by kind = %d, want 1", len(byKind)) } byStatus, _ := db.ListProposals("", ProposalPending) if len(byStatus) != 1 { t.Errorf("list by status pending = %d, want 1", len(byStatus)) } // Search FTS found, err := db.SearchProposals("backoff", "", "") if err != nil { t.Fatalf("search: %v", err) } if len(found) != 1 { t.Errorf("search 'backoff' = %d, want 1", len(found)) } // Delete if err := db.DeleteProposal("proposal_test_1"); err != nil { t.Fatalf("delete: %v", err) } _, err = db.GetProposal("proposal_test_1") if err == nil { t.Error("expected error after delete") } } func TestValidateProposal(t *testing.T) { tests := []struct { name string p Proposal wantErr bool }{ {"valid new_function", Proposal{ID: "p1", Kind: ProposalNewFunction, Title: "test"}, false}, {"valid improve with target", Proposal{ID: "p2", Kind: ProposalImproveFunction, Title: "test", TargetID: "fn_go_core"}, false}, {"missing title", Proposal{ID: "p3", Kind: ProposalNewFunction}, true}, {"missing kind", Proposal{ID: "p4", Title: "test"}, true}, {"improve without target", Proposal{ID: "p5", Kind: ProposalImproveType, Title: "test"}, true}, {"invalid kind", Proposal{ID: "p6", Kind: "invalid", Title: "test"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateProposal(&tt.p) if (err != nil) != tt.wantErr { t.Errorf("ValidateProposal() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestMigrations(t *testing.T) { db := tempDB(t) // Verify schema_migrations table exists and has entries var count int err := db.conn.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count) if err != nil { t.Fatalf("query schema_migrations: %v", err) } if count < 2 { t.Errorf("expected at least 2 migrations, got %d", count) } // Verify proposals table exists _, err = db.conn.Exec("SELECT 1 FROM proposals LIMIT 1") if err != nil { t.Fatalf("proposals table should exist: %v", err) } } func TestDrop(t *testing.T) { path := filepath.Join(t.TempDir(), "drop.db") db, err := Open(path) if err != nil { t.Fatal(err) } if err := db.Drop(); err != nil { t.Fatal(err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Error("db file should not exist after Drop") } }