feat: proposals en registry

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>
This commit is contained in:
2026-03-28 17:13:24 +01:00
parent 093367107e
commit 8d98faccd9
9 changed files with 902 additions and 109 deletions
+132
View File
@@ -151,6 +151,138 @@ func TestPurge(t *testing.T) {
}
}
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)