chore: auto-commit (95 archivos)
- cmd/fn/doctor.go - cmd/fn/main.go - cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt - cpp/apps/primitives_gallery/playground/tables/data_table.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.h - cpp/apps/primitives_gallery/playground/tables/self_test.cpp - cpp/apps/primitives_gallery/playground/tables/tql.cpp - cpp/apps/primitives_gallery/playground/tables/viz.cpp - cpp/apps/primitives_gallery/playground/tables/viz.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1059
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fn-registry/functions/infra"
|
||||
"fn-registry/registry"
|
||||
)
|
||||
|
||||
// fnBinDir holds the temp directory for the compiled fn binary.
|
||||
// It is created by TestMain and cleaned up at test end.
|
||||
var fnBinDir string
|
||||
var fnBinPath string
|
||||
|
||||
// TestMain compiles the fn binary once before all tests.
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
fnBinDir, err = os.MkdirTemp("", "fn-vault-test-*")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "create temp dir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.RemoveAll(fnBinDir)
|
||||
|
||||
fnBinPath = filepath.Join(fnBinDir, "fn")
|
||||
// Find registry root by walking up from current directory.
|
||||
regRoot, err := findRoot()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "find root: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-tags", "fts5", "-o", fnBinPath, ".")
|
||||
cmd.Dir = filepath.Join(regRoot, "cmd", "fn")
|
||||
if out, errB := cmd.CombinedOutput(); errB != nil {
|
||||
fmt.Fprintf(os.Stderr, "build fn: %v\n%s\n", errB, out)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func findRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("could not find go.mod from %s", dir)
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func ensureFnBin(t *testing.T) string {
|
||||
t.Helper()
|
||||
return fnBinPath
|
||||
}
|
||||
|
||||
// setupTestRegistry creates a minimal registry root with:
|
||||
// - registry.db (opened + migrations applied via registry.Open)
|
||||
// - a project with a vault declared in vault.yaml
|
||||
// - a vault directory with some test files
|
||||
// - a symlink from projects/test_proj/vaults/test_vault -> vault dir
|
||||
//
|
||||
// Returns (repoRoot, vaultDir).
|
||||
func setupTestRegistry(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
repoRoot := t.TempDir()
|
||||
|
||||
// Create vault directory with files.
|
||||
vaultDir := filepath.Join(t.TempDir(), "test_vault")
|
||||
if err := os.MkdirAll(filepath.Join(vaultDir, "data", "raw"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(vaultDir, "data", "raw", "report.csv"),
|
||||
[]byte("name,value\nfoo,1"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(vaultDir, "data", "raw", "notes.md"),
|
||||
[]byte("# Notes\nsome text"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create project directory structure.
|
||||
projDir := filepath.Join(repoRoot, "projects", "test_proj")
|
||||
vaultsDir := filepath.Join(projDir, "vaults")
|
||||
if err := os.MkdirAll(vaultsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create vault.yaml.
|
||||
vaultYAML := "vaults:\n - name: test_vault\n description: Test vault for unit tests\n path: " + vaultDir + "\n tags: [test]\n"
|
||||
if err := os.WriteFile(filepath.Join(vaultsDir, "vault.yaml"), []byte(vaultYAML), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create project.md.
|
||||
projMD := "---\nname: test_proj\ndescription: Test project\ntags: [test]\n---\n"
|
||||
if err := os.WriteFile(filepath.Join(projDir, "project.md"), []byte(projMD), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Open registry.db (creates schema + runs migrations).
|
||||
db, err := registry.Open(filepath.Join(repoRoot, "registry.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("registry.Open: %v", err)
|
||||
}
|
||||
|
||||
// Index so the vault is registered in registry.db.
|
||||
if _, err := registry.Index(db, repoRoot); err != nil {
|
||||
t.Fatalf("registry.Index: %v", err)
|
||||
}
|
||||
db.Close()
|
||||
|
||||
return repoRoot, vaultDir
|
||||
}
|
||||
|
||||
// runFn runs the fn binary in repoRoot with the given args.
|
||||
func runFn(t *testing.T, repoRoot string, args ...string) (string, string, int) {
|
||||
t.Helper()
|
||||
bin := ensureFnBin(t)
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Dir = repoRoot
|
||||
var stdout, stderr strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
code := 0
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
code = exitErr.ExitCode()
|
||||
} else {
|
||||
t.Logf("cmd error: %v", err)
|
||||
}
|
||||
}
|
||||
return stdout.String(), stderr.String(), code
|
||||
}
|
||||
|
||||
// TestVaultList verifies that 'fn vault list' shows the indexed vault.
|
||||
func TestVaultList(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "list")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault list exit %d\nstderr: %s", code, stderr)
|
||||
}
|
||||
if !strings.Contains(out, "test_vault") {
|
||||
t.Errorf("expected 'test_vault' in output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultIndex verifies that 'fn vault index <name>' runs without error.
|
||||
func TestVaultIndex(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "index", "test_vault")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault index exit %d\nstderr: %s\nstdout: %s", code, stderr, out)
|
||||
}
|
||||
if !strings.Contains(out, "indexed") {
|
||||
t.Errorf("expected 'indexed' in output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultSearchJSON verifies that 'fn vault search --json' returns valid JSON array.
|
||||
func TestVaultSearchJSON(t *testing.T) {
|
||||
repoRoot, vaultDir := setupTestRegistry(t)
|
||||
|
||||
// First index the vault so there is something to search.
|
||||
if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
|
||||
t.Fatal("fn vault index failed")
|
||||
}
|
||||
|
||||
// Seed some content into the vault index for the search to find.
|
||||
db, err := infra.VaultIndexOpen(vaultDir)
|
||||
if err != nil {
|
||||
t.Fatalf("VaultIndexOpen: %v", err)
|
||||
}
|
||||
// Update content_text for FTS search.
|
||||
db.Exec(`DELETE FROM files_fts WHERE rel_path = 'data/raw/report.csv'`)
|
||||
db.Exec(`INSERT INTO files_fts(rel_path, content_text) VALUES ('data/raw/report.csv', 'foo report data')`)
|
||||
db.Close()
|
||||
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "search", "report", "--json", "--vault", "test_vault")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault search exit %d\nstderr: %s", code, stderr)
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(out), &result); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v\nraw: %s", err, out)
|
||||
}
|
||||
// Should be a JSON array (possibly empty if search finds nothing, but must be valid).
|
||||
t.Logf("search returned %d hits", len(result))
|
||||
}
|
||||
|
||||
// TestVaultInfo verifies that 'fn vault info <name>' outputs vault stats.
|
||||
func TestVaultInfo(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
|
||||
// Index first.
|
||||
if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
|
||||
t.Fatal("fn vault index failed")
|
||||
}
|
||||
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "info", "test_vault")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault info exit %d\nstderr: %s", code, stderr)
|
||||
}
|
||||
if !strings.Contains(out, "test_vault") {
|
||||
t.Errorf("expected vault name in output, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Files:") {
|
||||
t.Errorf("expected 'Files:' in output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatBytes verifies the formatBytes helper.
|
||||
func TestFormatBytes(t *testing.T) {
|
||||
cases := []struct {
|
||||
input int64
|
||||
expected string
|
||||
}{
|
||||
{500, "500 B"},
|
||||
{1024, "1.0 KB"},
|
||||
{1536, "1.5 KB"},
|
||||
{1048576, "1.0 MB"},
|
||||
{1073741824, "1.0 GB"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := formatBytes(tc.input)
|
||||
if got != tc.expected {
|
||||
t.Errorf("formatBytes(%d) = %q, want %q", tc.input, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultLayoutEnsure verifies that 'fn vault layout-ensure --dry-run' works.
|
||||
func TestVaultLayoutEnsure(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "layout-ensure", "test_vault", "--dry-run")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault layout-ensure exit %d\nstderr: %s\nstdout: %s", code, stderr, out)
|
||||
}
|
||||
if !strings.Contains(out, "test_vault") {
|
||||
t.Errorf("expected vault name in output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultAggregate verifies that 'fn vault aggregate' runs without error on a clean registry.
|
||||
func TestVaultAggregate(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
|
||||
// Index first so there is something to aggregate.
|
||||
if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
|
||||
t.Fatal("fn vault index failed")
|
||||
}
|
||||
|
||||
_, stderr, code := runFn(t, repoRoot, "vault", "aggregate")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault aggregate exit %d\nstderr: %s", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultDoctor verifies that 'fn vault doctor' runs and reports on vaults.
|
||||
func TestVaultDoctor(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "doctor")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault doctor exit %d\nstderr: %s", code, stderr)
|
||||
}
|
||||
if !strings.Contains(out, "test_vault") {
|
||||
t.Errorf("expected 'test_vault' in doctor output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVaultDedupe verifies that 'fn vault dedupe' runs without error after indexing.
|
||||
func TestVaultDedupe(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
|
||||
if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 {
|
||||
t.Fatal("fn vault index failed")
|
||||
}
|
||||
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "dedupe", "test_vault")
|
||||
if code != 0 {
|
||||
t.Fatalf("fn vault dedupe exit %d\nstderr: %s", code, stderr)
|
||||
}
|
||||
// Should say "No duplicates" or show a table — either is fine.
|
||||
_ = out
|
||||
}
|
||||
|
||||
// TestVaultAuditDryRun verifies that 'fn vault audit --dry-run-layout --skip-profilers' works.
|
||||
func TestVaultAuditDryRun(t *testing.T) {
|
||||
repoRoot, _ := setupTestRegistry(t)
|
||||
out, stderr, code := runFn(t, repoRoot, "vault", "audit", "test_vault",
|
||||
"--dry-run-layout", "--skip-profilers")
|
||||
// Exit 0 = fully ok; exit 4 = warnings (layout issues) — both acceptable here.
|
||||
if code != 0 && code != 4 {
|
||||
t.Fatalf("fn vault audit exit %d\nstderr: %s\nstdout: %s", code, stderr, out)
|
||||
}
|
||||
if !strings.Contains(out, "summary") {
|
||||
t.Errorf("expected 'summary' section in audit output, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress unused import for time.
|
||||
var _ = time.Now
|
||||
@@ -44,6 +44,10 @@ func cmdDoctor(args []string) {
|
||||
doctorUnused(r, jsonOut)
|
||||
case "cpp-apps":
|
||||
doctorCppApps(r, jsonOut)
|
||||
case "ml":
|
||||
doctorML(r, jsonOut)
|
||||
case "vaults":
|
||||
doctorVaults(r, jsonOut)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
|
||||
doctorUsage()
|
||||
@@ -65,6 +69,8 @@ Subcommands:
|
||||
uses-functions Audit imports reales vs uses_functions del app.md
|
||||
unused Funciones del registry sin consumidores
|
||||
cpp-apps Conformidad de apps C++ con cpp/PATTERNS.md (cfg.about, dockspace, menubar)
|
||||
ml Entorno ML: GPUs NVIDIA, CUDA toolkit, venv Python, paquetes torch/diffusers, CLIs y vault
|
||||
vaults Salud de vaults: directorio, layout, índice, staleness, drift
|
||||
|
||||
Flags:
|
||||
--json Salida JSON (para scripting/agentes)`)
|
||||
@@ -103,6 +109,16 @@ func doctorAll(root string, jsonOut bool) {
|
||||
} else {
|
||||
all["cpp_apps_error"] = err.Error()
|
||||
}
|
||||
if v, err := infra.AuditMlEnv(root); err == nil {
|
||||
all["ml"] = v
|
||||
} else {
|
||||
all["ml_error"] = err.Error()
|
||||
}
|
||||
if v, err := infra.VaultDoctor(root); err == nil {
|
||||
all["vaults"] = v
|
||||
} else {
|
||||
all["vaults_error"] = err.Error()
|
||||
}
|
||||
emit(all)
|
||||
return
|
||||
}
|
||||
@@ -119,6 +135,10 @@ func doctorAll(root string, jsonOut bool) {
|
||||
doctorUnused(root, false)
|
||||
fmt.Println("\n=== C++ apps standard conformance ===")
|
||||
doctorCppApps(root, false)
|
||||
fmt.Println("\n=== ML environment ===")
|
||||
doctorML(root, false)
|
||||
fmt.Println("\n=== Vaults ===")
|
||||
doctorVaults(root, false)
|
||||
}
|
||||
|
||||
func doctorCppApps(root string, jsonOut bool) {
|
||||
@@ -280,6 +300,81 @@ func doctorUnused(root string, jsonOut bool) {
|
||||
fmt.Printf("\n%d unused functions (candidates to remove).\n", len(unused))
|
||||
}
|
||||
|
||||
func doctorVaults(root string, jsonOut bool) {
|
||||
entries, err := infra.VaultDoctor(root)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if jsonOut {
|
||||
emit(entries)
|
||||
return
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
fmt.Println("No vaults declared (no projects/*/vaults/vault.yaml found).")
|
||||
return
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "NAME\tSTATUS\tFILES\tINDEXED\tISSUES")
|
||||
ok := 0
|
||||
for _, e := range entries {
|
||||
issues := "-"
|
||||
if len(e.Issues) > 0 {
|
||||
issues = strings.Join(e.Issues, "; ")
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%s\n",
|
||||
e.VaultName, e.Status, e.DiskFiles, e.IndexedFiles, issues)
|
||||
if e.Status == "ok" {
|
||||
ok++
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Printf("\n%d/%d vaults healthy.\n", ok, len(entries))
|
||||
}
|
||||
|
||||
func doctorML(root string, jsonOut bool) {
|
||||
report, err := infra.AuditMlEnv(root)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if jsonOut {
|
||||
emit(report)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("GPUs detected: %d\n", len(report.Gpus))
|
||||
for _, g := range report.Gpus {
|
||||
fmt.Printf(" [%d] %s VRAM: %d/%d MiB Driver: %s CUDA: %s\n",
|
||||
g.Index, g.Name, g.VramFreeMb, g.VramTotalMb, g.DriverVersion, g.CudaVersion)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "CHECK\tSTATUS\tVERSION\tDETAIL")
|
||||
for _, c := range report.Checks {
|
||||
version := c.Version
|
||||
if version == "" {
|
||||
version = "-"
|
||||
}
|
||||
detail := c.Detail
|
||||
if len(detail) > 60 {
|
||||
detail = detail[:60] + "..."
|
||||
}
|
||||
if detail == "" {
|
||||
detail = "-"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Name, c.Status, version, detail)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
overall := "OK"
|
||||
if !report.OverallOK {
|
||||
overall = "INCOMPLETE"
|
||||
}
|
||||
fmt.Printf("\nOverall ML environment: %s\n", overall)
|
||||
}
|
||||
|
||||
func emit(v any) {
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -45,6 +45,8 @@ func main() {
|
||||
cmdAnalysis(os.Args[2:])
|
||||
case "sync":
|
||||
cmdSync(os.Args[2:])
|
||||
case "vault":
|
||||
cmdVault(os.Args[2:])
|
||||
case "doctor":
|
||||
cmdDoctor(os.Args[2:])
|
||||
case "help", "-h", "--help":
|
||||
@@ -73,6 +75,7 @@ Usage:
|
||||
fn app <list|clone|pull> Gestiona apps externas (Gitea)
|
||||
fn analysis <list|clone|pull> Gestiona analyses externas (Gitea)
|
||||
fn sync [status|locations] Sincroniza con servidor central
|
||||
fn vault <list|search|index|info> Gestiona y busca en data vaults
|
||||
fn doctor [artefacts|services|sync|uses-functions|unused] [--json]
|
||||
Diagnostico read-only del registry`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user