8d98faccd9
Añade sistema de proposals al registry: modelos (ProposalKind, ProposalStatus), CRUD completo (Insert/Get/Update/Delete/List/Search con FTS), validación, migración 002_proposals.sql y subcomando CLI fn proposal (add/list/show/update). Motor de migraciones con embed.FS reemplaza schema estático. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
301 lines
7.7 KiB
Go
301 lines
7.7 KiB
Go
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")
|
|
}
|
|
}
|