Files
egutierrez 49eecd0c87 feat: campos documentation, notes y code en registry
Añade campos documentation, notes y code a functions y types.
El parser extrae el contenido del .md y el código fuente del archivo
referenciado en file_path. El indexer los almacena en SQLite y los
incluye en FTS5 para búsqueda sobre código y documentación.
Nueva migración 003_documentation.sql para añadir las columnas.
2026-03-28 20:32:15 +01:00

300 lines
6.2 KiB
Go

package registry
import (
"os"
"path/filepath"
"testing"
)
const functionMD = `---
name: filter_slice
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func FilterSlice[T any](xs []T, pred func(T) bool) []T"
description: "Filtra un slice aplicando un predicado sin mutar el original."
tags: [slice, functional, generic]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "functions/core/filter_slice.go"
---
## Ejemplo
` + "```go" + `
evens := FilterSlice([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
` + "```" + `
`
const typeMD = `---
name: ohlcv
lang: go
domain: finance
version: "1.0.0"
algebraic: product
definition: |
type OHLCV struct {
Open float64
High float64
Low float64
Close float64
Volume float64
}
description: "Vela de mercado con precios OHLCV."
tags: [finance, market, candle]
uses_types: []
file_path: "types/finance/ohlcv.go"
---
## Notas
Tipo producto.
`
const componentMD = `---
name: DataTable
kind: component
lang: typescript
domain: core
version: "1.0.0"
purity: impure
signature: "DataTable<T>(props: { data: T[] }): JSX.Element"
description: "Tabla de datos generica."
tags: [table, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/data_table.tsx"
props:
- name: data
type: "T[]"
required: true
description: "Array de datos"
emits: [onRowClick]
has_state: true
framework: react
variant: [default, compact]
---
`
func writeTempFile(t *testing.T, dir, name, content string) string {
t.Helper()
path := filepath.Join(dir, name)
os.MkdirAll(filepath.Dir(path), 0o755)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func TestParseFunctionMD(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "filter_slice.md", functionMD)
f, err := ParseFunctionMD(path, "")
if err != nil {
t.Fatal(err)
}
if f.ID != "filter_slice_go_core" {
t.Errorf("id: got %q", f.ID)
}
if f.Kind != KindFunction {
t.Errorf("kind: got %q", f.Kind)
}
if f.Purity != PurityPure {
t.Errorf("purity: got %q", f.Purity)
}
if len(f.Tags) != 3 {
t.Errorf("tags: got %d, want 3", len(f.Tags))
}
if f.Example == "" {
t.Error("example should be extracted from body")
}
}
func TestParseTypeMD(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "ohlcv.md", typeMD)
typ, err := ParseTypeMD(path, "")
if err != nil {
t.Fatal(err)
}
if typ.ID != "ohlcv_go_finance" {
t.Errorf("id: got %q", typ.ID)
}
if typ.Algebraic != AlgebraicProduct {
t.Errorf("algebraic: got %q", typ.Algebraic)
}
if typ.Definition == "" {
t.Error("definition should not be empty")
}
}
func TestParseComponentMD(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "DataTable.md", componentMD)
f, err := ParseFunctionMD(path, "")
if err != nil {
t.Fatal(err)
}
if f.Kind != KindComponent {
t.Errorf("kind: got %q", f.Kind)
}
if f.Framework != "react" {
t.Errorf("framework: got %q", f.Framework)
}
if len(f.Props) != 1 {
t.Errorf("props: got %d, want 1", len(f.Props))
}
if f.HasState == nil || !*f.HasState {
t.Error("has_state should be true")
}
if len(f.Emits) != 1 || f.Emits[0] != "onRowClick" {
t.Errorf("emits: got %v", f.Emits)
}
}
func TestParseMissingFrontmatter(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "bad.md", "# No frontmatter here\n")
_, err := ParseFunctionMD(path, "")
if err == nil {
t.Error("expected error for missing frontmatter")
}
}
func TestIndexFullCycle(t *testing.T) {
root := t.TempDir()
// Create function .md
writeTempFile(t, root, "functions/core/filter_slice.md", functionMD)
// Create type .md
writeTempFile(t, root, "types/finance/ohlcv.md", typeMD)
dbPath := filepath.Join(root, "registry.db")
db, err := Open(dbPath)
if err != nil {
t.Fatal(err)
}
defer db.Close()
result, err := Index(db, root)
if err != nil {
t.Fatal(err)
}
if result.Functions != 1 {
t.Errorf("functions indexed: got %d, want 1", result.Functions)
}
if result.Types != 1 {
t.Errorf("types indexed: got %d, want 1", result.Types)
}
if len(result.Errors) != 0 {
t.Errorf("unexpected errors: %v", result.Errors)
}
if len(result.ValidationErrors) != 0 {
t.Errorf("unexpected validation errors: %v", result.ValidationErrors)
}
// Verify searchable
fns, err := db.SearchFunctions("filter", "", "", "", "")
if err != nil {
t.Fatal(err)
}
if len(fns) != 1 {
t.Errorf("search 'filter': got %d, want 1", len(fns))
}
ts, err := db.SearchTypes("ohlcv", "", "")
if err != nil {
t.Fatal(err)
}
if len(ts) != 1 {
t.Errorf("search 'ohlcv': got %d, want 1", len(ts))
}
// Re-index should be idempotent
result2, err := Index(db, root)
if err != nil {
t.Fatal(err)
}
if result2.Functions != 1 || result2.Types != 1 {
t.Error("re-index should produce same counts")
}
}
const invalidPipelineMD = `---
name: bad_pipeline
kind: pipeline
lang: go
domain: core
version: "1.0.0"
purity: pure
description: "Pipeline puro sin uses_functions — debe fallar."
tags: []
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "functions/pipelines/bad.go"
---
`
func TestIndexRejectsInvalid(t *testing.T) {
root := t.TempDir()
// Valid function
writeTempFile(t, root, "functions/core/filter_slice.md", functionMD)
// Invalid pipeline (pure + empty uses_functions)
writeTempFile(t, root, "functions/pipelines/bad.md", invalidPipelineMD)
// Valid type
writeTempFile(t, root, "types/finance/ohlcv.md", typeMD)
dbPath := filepath.Join(root, "registry.db")
db, err := Open(dbPath)
if err != nil {
t.Fatal(err)
}
defer db.Close()
result, err := Index(db, root)
if err != nil {
t.Fatal(err)
}
// Valid entries should be indexed
if result.Functions != 1 {
t.Errorf("functions: got %d, want 1 (only the valid one)", result.Functions)
}
if result.Types != 1 {
t.Errorf("types: got %d, want 1", result.Types)
}
// Invalid pipeline should produce validation error
if len(result.ValidationErrors) == 0 {
t.Error("expected validation errors for invalid pipeline")
}
}