Files
fn_registry/functions/infra/find_unused_functions_test.go
T
egutierrez 2a3d780347 feat(doctor): add fn doctor CLI + 14 functions for system management
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>
2026-05-07 01:42:10 +02:00

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")
}
})
}