2a3d780347
Adds `fn doctor` read-only diagnostic command with subcommands artefacts, services, sync, uses-functions, unused, and --json flag for agents. Each subcommand wraps a registry function in functions/infra/. New functions: - artefact_doctor, services_status, pc_locations_drift, audit_uses_functions, find_unused_functions (Go diagnostics) - backup_sqlite_db, rotate_backups, wait_for_http, wait_for_port, port_kill, tail_journal, pre_commit_hook_install (bash utilities) - notify_telegram (Go HTTP) - backup_all pipeline (tag launcher) Plus prior session leftovers (scan_secrets_in_dirty, append_diary_entry, git utilities, http_session_cookie_middleware, compile/full-git pipelines). Fixes pc_locations_drift filepath.Join bug with absolute dir_path. Documents fn doctor in CLAUDE.md, .claude/rules/fn_doctor.md (rule 23), docs/architecture.md, CHANGELOG.md (2026-05-07), and diary entry. First fn doctor uses-functions run found drift in 7/12 apps (deuda para sincronizar app.md con imports reales). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
3.9 KiB
Go
150 lines
3.9 KiB
Go
package infra
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"database/sql"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
func seedTestRegistry(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "registry.db")
|
|
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open temp db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.Exec(`
|
|
CREATE TABLE functions (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
lang TEXT NOT NULL,
|
|
domain TEXT NOT NULL,
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
kind TEXT NOT NULL DEFAULT 'function',
|
|
updated_at TEXT NOT NULL,
|
|
uses_functions TEXT NOT NULL DEFAULT '[]'
|
|
);
|
|
CREATE TABLE apps (
|
|
id TEXT PRIMARY KEY,
|
|
uses_functions TEXT NOT NULL DEFAULT '[]'
|
|
);
|
|
CREATE TABLE analysis (
|
|
id TEXT PRIMARY KEY,
|
|
uses_functions TEXT NOT NULL DEFAULT '[]'
|
|
);
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("create schema: %v", err)
|
|
}
|
|
|
|
// fn_a is used by fn_b
|
|
// fn_b is used by an app
|
|
// fn_c is the orphan — nobody uses it
|
|
_, err = db.Exec(`
|
|
INSERT INTO functions VALUES
|
|
('fn_a', 'fn_a', 'go', 'core', '[]', 'function', '2026-01-01T00:00:00Z', '[]'),
|
|
('fn_b', 'fn_b', 'go', 'core', '[]', 'function', '2026-01-15T00:00:00Z', '["fn_a"]'),
|
|
('fn_c', 'fn_c', 'go', 'core', '[]', 'function', '2025-06-01T00:00:00Z', '[]');
|
|
INSERT INTO apps VALUES
|
|
('app_x', '["fn_b"]');
|
|
INSERT INTO analysis VALUES
|
|
('an_y', '[]');
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("seed data: %v", err)
|
|
}
|
|
|
|
return dir
|
|
}
|
|
|
|
func TestFindUnusedFunctions_DetectsOrphan(t *testing.T) {
|
|
t.Run("solo fn_c queda huerfana con 2 funciones consumidas", func(t *testing.T) {
|
|
dir := seedTestRegistry(t)
|
|
got, err := FindUnusedFunctions(dir)
|
|
if err != nil {
|
|
t.Fatalf("FindUnusedFunctions error: %v", err)
|
|
}
|
|
|
|
if len(got) != 1 {
|
|
ids := make([]string, len(got))
|
|
for i, u := range got {
|
|
ids[i] = u.ID
|
|
}
|
|
t.Fatalf("expected 1 unused function, got %d: %v", len(got), ids)
|
|
}
|
|
if got[0].ID != "fn_c" {
|
|
t.Errorf("expected orphan ID fn_c, got %s", got[0].ID)
|
|
}
|
|
if got[0].AgeDays <= 0 {
|
|
t.Errorf("expected positive AgeDays, got %d", got[0].AgeDays)
|
|
}
|
|
})
|
|
|
|
t.Run("launcher pipeline se excluye aunque nadie la use", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "registry.db")
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.Exec(`
|
|
CREATE TABLE functions (
|
|
id TEXT PRIMARY KEY, name TEXT, lang TEXT, domain TEXT,
|
|
tags TEXT DEFAULT '[]', kind TEXT DEFAULT 'function',
|
|
updated_at TEXT, uses_functions TEXT DEFAULT '[]'
|
|
);
|
|
CREATE TABLE apps (id TEXT PRIMARY KEY, uses_functions TEXT DEFAULT '[]');
|
|
CREATE TABLE analysis (id TEXT PRIMARY KEY, uses_functions TEXT DEFAULT '[]');
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("schema: %v", err)
|
|
}
|
|
db.Exec(`
|
|
INSERT INTO functions VALUES
|
|
('pipe_launch', 'pipe_launch', 'bash', 'pipelines', '["launcher"]', 'pipeline', '2026-01-01T00:00:00Z', '[]'),
|
|
('pipe_nolabel', 'pipe_nolabel', 'go', 'pipelines', '[]', 'pipeline', '2026-01-01T00:00:00Z', '[]'),
|
|
('fn_orphan', 'fn_orphan', 'go', 'core', '[]', 'function', '2026-01-01T00:00:00Z', '[]');
|
|
`)
|
|
|
|
got, err := FindUnusedFunctions(dir)
|
|
if err != nil {
|
|
t.Fatalf("error: %v", err)
|
|
}
|
|
|
|
ids := map[string]bool{}
|
|
for _, u := range got {
|
|
ids[u.ID] = true
|
|
}
|
|
if ids["pipe_launch"] {
|
|
t.Error("launcher pipeline should be excluded from unused")
|
|
}
|
|
if !ids["pipe_nolabel"] {
|
|
t.Error("pipeline without launcher tag should appear as unused")
|
|
}
|
|
if !ids["fn_orphan"] {
|
|
t.Error("orphan function should appear as unused")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFindUnusedFunctions_MissingDB(t *testing.T) {
|
|
t.Run("error si registry.db no existe", func(t *testing.T) {
|
|
dir, _ := os.MkdirTemp("", "nodb")
|
|
defer os.RemoveAll(dir)
|
|
_, err := FindUnusedFunctions(dir)
|
|
if err == nil {
|
|
t.Error("expected error for missing db, got nil")
|
|
}
|
|
})
|
|
}
|