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