feat(infra): auto-commit con 6 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:16:36 +02:00
parent ea6a3ec8a5
commit 6aec0413bb
6 changed files with 541 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
package infra
import (
"fmt"
"os"
"path/filepath"
)
// ResolveRegistryRoot returns the absolute path of the fn_registry root
// (the directory that contains registry.db), or an error if not found.
//
// Resolution order:
// 1. FN_REGISTRY_ROOT env var — if set and <dir>/registry.db exists, return it.
// 2. Walk up from the executable's directory — up to 6 levels; first dir
// that contains registry.db wins. Covers binaries at <root>/fn and at
// <root>/apps/<x>/<bin>.
// 3. $HOME/fn_registry — if <$HOME>/fn_registry/registry.db exists.
// 4. Error — no method found a valid registry root.
func ResolveRegistryRoot() (string, error) {
// 1. Env var
if envDir := os.Getenv("FN_REGISTRY_ROOT"); envDir != "" {
if nonEmptyFile(filepath.Join(envDir, "registry.db")) {
return filepath.Clean(envDir), nil
}
}
// 2. Walk up from executable
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
for i := 0; i < 6; i++ {
if nonEmptyFile(filepath.Join(dir, "registry.db")) {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
// Reached filesystem root
break
}
dir = parent
}
}
// 3. $HOME/fn_registry
if home, err := os.UserHomeDir(); err == nil {
candidate := filepath.Join(home, "fn_registry")
if nonEmptyFile(filepath.Join(candidate, "registry.db")) {
return candidate, nil
}
}
return "", fmt.Errorf("resolve_registry_root: no se encontro registry.db (set FN_REGISTRY_ROOT)")
}
// nonEmptyFile reports whether path is an existing, non-empty regular file.
// A zero-byte registry.db — which sql.Open silently creates when handed a
// mis-resolved path — must NOT count as a valid registry root, otherwise such
// a shadow file would hijack the walk-up and mask the real database.
func nonEmptyFile(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsRegular() && fi.Size() > 0
}
+51
View File
@@ -0,0 +1,51 @@
---
name: resolve_registry_root
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func ResolveRegistryRoot() (string, error)"
description: "Devuelve la ruta absoluta de la raíz del fn_registry (el directorio que contiene registry.db). Resuelve en orden: env var FN_REGISTRY_ROOT, walk-up desde el ejecutable (hasta 6 niveles), y $HOME/fn_registry. Retorna error si ningún método localiza registry.db. Centraliza la resolución dinámica para que ningún binario del ecosistema necesite un path hardcodeado."
tags: [registry, infra, root, path, env]
uses_functions: []
uses_types: []
params:
- name: (ninguno)
desc: "La función no recibe argumentos."
output: "Ruta absoluta del directorio raíz del fn_registry (contiene registry.db), o error descriptivo si no se encontró."
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["fmt", "os", "path/filepath"]
tested: true
tests:
- "env var apunta a tmpdir con registry.db"
- "HOME apunta a tmpdir con fn_registry/registry.db"
- "nada existe devuelve error"
test_file_path: "functions/infra/resolve_registry_root_test.go"
file_path: "functions/infra/resolve_registry_root.go"
---
## Ejemplo
```go
root, err := infra.ResolveRegistryRoot()
if err != nil {
log.Fatalf("no se pudo localizar fn_registry: %v", err)
}
// Construir ruta al binario fn
fnBin := filepath.Join(root, "fn")
cmd := exec.Command(fnBin, "run", "mi_funcion")
```
## Cuando usarla
Cuando un binario Go del ecosistema (dag_engine, deploy_server, registry_mcp, etc.) necesita localizar la raíz del registry para construir rutas a `fn run`, abrir `registry.db`, o encontrar `python/.venv` — y `FN_REGISTRY_ROOT` puede no estar seteado en el entorno del usuario.
## Gotchas
- **Depende del entorno en tiempo de ejecución** (env vars + filesystem): no es determinista. Dos llamadas sucesivas desde entornos distintos pueden devolver rutas distintas.
- **El paso 2 (walk-up desde ejecutable) asume que el binario vive dentro del árbol del registry.** Si el binario se copia a `/usr/local/bin` o a un directorio externo, este paso no encontrará nada — los pasos 1 y 3 sirven como fallback.
- **En `go test` el exe es un binario temporal** generado por el framework de tests, que vive en `/tmp/`. Por eso el paso 2 no encontrará `registry.db` durante los tests unitarios. Los tests de esta función usan `t.Setenv` para cubrir los pasos 1 y 3 sin depender del entorno real.
- **El paso 3 deriva `$HOME` dinámicamente** con `os.UserHomeDir()` — nunca hay paths de usuario hardcodeados.
@@ -0,0 +1,60 @@
package infra
import (
"os"
"path/filepath"
"testing"
)
func TestResolveRegistryRoot(t *testing.T) {
t.Run("env var apunta a tmpdir con registry.db", func(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "registry.db")
if err := os.WriteFile(dbPath, []byte("SQLite format 3\x00"), 0644); err != nil {
t.Fatal(err)
}
t.Setenv("FN_REGISTRY_ROOT", dir)
got, err := ResolveRegistryRoot()
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if got != filepath.Clean(dir) {
t.Errorf("got %q, want %q", got, filepath.Clean(dir))
}
})
t.Run("HOME apunta a tmpdir con fn_registry/registry.db", func(t *testing.T) {
homeDir := t.TempDir()
registryDir := filepath.Join(homeDir, "fn_registry")
if err := os.MkdirAll(registryDir, 0755); err != nil {
t.Fatal(err)
}
dbPath := filepath.Join(registryDir, "registry.db")
if err := os.WriteFile(dbPath, []byte("SQLite format 3\x00"), 0644); err != nil {
t.Fatal(err)
}
// Clear env var so we fall through to the HOME check.
t.Setenv("FN_REGISTRY_ROOT", "")
t.Setenv("HOME", homeDir)
got, err := ResolveRegistryRoot()
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if got != registryDir {
t.Errorf("got %q, want %q", got, registryDir)
}
})
t.Run("nada existe devuelve error", func(t *testing.T) {
emptyHome := t.TempDir()
t.Setenv("FN_REGISTRY_ROOT", "")
t.Setenv("HOME", emptyHome)
_, err := ResolveRegistryRoot()
if err == nil {
t.Error("expected error when nothing exists, got nil")
}
})
}