package infra import ( "database/sql" "os" "path/filepath" "testing" _ "github.com/mattn/go-sqlite3" ) // createTestRegistryDB creates a minimal registry.db with the given apps and // a single function (random_hex_id_go_core in domain core, lang go). func createTestRegistryDB(t *testing.T, root string, apps []struct { id string lang string dirPath string usesFunctions string }) { t.Helper() dbPath := filepath.Join(root, "registry.db") db, err := sql.Open("sqlite3", dbPath) if err != nil { t.Fatal(err) } defer db.Close() _, err = db.Exec(` CREATE TABLE functions ( id TEXT PRIMARY KEY, name TEXT, domain TEXT, lang TEXT, signature TEXT, file_path TEXT ); CREATE TABLE apps ( id TEXT PRIMARY KEY, lang TEXT, dir_path TEXT, uses_functions TEXT DEFAULT '[]' ); INSERT INTO functions (id, name, domain, lang, file_path) VALUES ('random_hex_id_go_core','random_hex_id','core','go','functions/core/random_hex_id.go'); `) if err != nil { t.Fatal(err) } for _, a := range apps { _, err = db.Exec( `INSERT INTO apps (id, lang, dir_path, uses_functions) VALUES (?,?,?,?)`, a.id, a.lang, a.dirPath, a.usesFunctions, ) if err != nil { t.Fatalf("insert app %s: %v", a.id, err) } } } // TestAuditUsesFunctions_DetectsMissing verifies that a Go app that calls // RandomHexID in its source but declares empty uses_functions gets // random_hex_id_go_core reported as missing. func TestAuditUsesFunctions_DetectsMissing(t *testing.T) { t.Run("missing function detected for Go app", func(t *testing.T) { root := t.TempDir() createTestRegistryDB(t, root, []struct { id, lang, dirPath, usesFunctions string }{ {"testapp_go_tools", "go", "apps/testapp", `[]`}, }) appDir := filepath.Join(root, "apps", "testapp") if err := os.MkdirAll(appDir, 0755); err != nil { t.Fatal(err) } goSrc := `package main import ( "fmt" "fn-registry/functions/core" ) func main() { id := core.RandomHexID(8) fmt.Println(id) } ` if err := os.WriteFile(filepath.Join(appDir, "main.go"), []byte(goSrc), 0644); err != nil { t.Fatal(err) } results, err := AuditUsesFunctions(root) if err != nil { t.Fatalf("AuditUsesFunctions: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } got := results[0] if len(got.Missing) != 1 || got.Missing[0] != "random_hex_id_go_core" { t.Errorf("Missing = %v, want [random_hex_id_go_core]", got.Missing) } if len(got.Unused) != 0 { t.Errorf("Unused = %v, want []", got.Unused) } }) } // TestAuditUsesFunctions_DetectsUnused verifies that a function declared in // uses_functions but not called in source is reported as unused. func TestAuditUsesFunctions_DetectsUnused(t *testing.T) { t.Run("unused function detected for Go app", func(t *testing.T) { root := t.TempDir() createTestRegistryDB(t, root, []struct { id, lang, dirPath, usesFunctions string }{ {"testapp2_go_tools", "go", "apps/testapp2", `["random_hex_id_go_core"]`}, }) appDir := filepath.Join(root, "apps", "testapp2") if err := os.MkdirAll(appDir, 0755); err != nil { t.Fatal(err) } goSrc := `package main import "fmt" func main() { fmt.Println("hello") } ` if err := os.WriteFile(filepath.Join(appDir, "main.go"), []byte(goSrc), 0644); err != nil { t.Fatal(err) } results, err := AuditUsesFunctions(root) if err != nil { t.Fatalf("AuditUsesFunctions: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } got := results[0] if len(got.Unused) != 1 || got.Unused[0] != "random_hex_id_go_core" { t.Errorf("Unused = %v, want [random_hex_id_go_core]", got.Unused) } if len(got.Missing) != 0 { t.Errorf("Missing = %v, want []", got.Missing) } }) } // TestSnakeToPascal_HandlesAbbreviations verifies the commonAbbrevs expansion // (issue 0057, Fase 1). Each "want" is the exported Go symbol the registry // actually uses for that snake_case name. It also pins the deliberate // non-mappings (cdp, pdf): the registry's own convention is mixed-case there, // so the abbreviation must NOT fire. func TestSnakeToPascal_HandlesAbbreviations(t *testing.T) { cases := []struct{ in, want string }{ // New abbreviations added by issue 0057 (verified against real func names). {"fetch_ohlcv", "FetchOHLCV"}, {"normalize_ohlcv", "NormalizeOHLCV"}, {"duckdb_open", "DuckDBOpen"}, {"load_ohlcv_from_duckdb", "LoadOHLCVFromDuckDB"}, {"clickhouse_open", "ClickHouseOpen"}, {"nordvpn_container_run", "NordVPNContainerRun"}, {"parse_nordvpn_status", "ParseNordVPNStatus"}, {"hash_sha256", "HashSHA256"}, {"hash_md5", "HashMD5"}, {"strip_ansi", "StripANSI"}, {"parse_ip_cidr", "ParseIPCIDR"}, {"open_aead", "OpenAEAD"}, {"seal_aead", "SealAEAD"}, {"pty_capture_stream", "PTYCaptureStream"}, {"setup_vps_app", "SetupVPSApp"}, {"vps_setup_app", "VPSSetupApp"}, {"wg_keygen", "WGKeygen"}, {"wg_peer_add", "WGPeerAdd"}, {"vt_render", "VTRender"}, {"fft", "FFT"}, {"ema", "EMA"}, {"rsi", "RSI"}, {"sma", "SMA"}, {"vwap", "VWAP"}, {"cdp_get_ax_outline", "CdpGetAXOutline"}, {"audit_e2e_coverage", "AuditE2ECoverage"}, {"e2e_run_checks", "E2ERunChecks"}, {"extract_urls", "ExtractURLs"}, // Pre-existing abbreviations (regression guard — must keep working). {"http_json_response", "HTTPJSONResponse"}, {"sqlite_open", "SQLiteOpen"}, {"random_hex_id", "RandomHexID"}, // Deliberate non-mappings: registry uses mixed-case (Cdp, Pdf) here, so // the snake_case→Pascal conversion must leave them mixed-case. These are // the cases the .go fallback (Fase 2) and the signature path cover. {"cdp_get_html", "CdpGetHTML"}, {"cdp_navigate", "CdpNavigate"}, {"pdf_simple_report", "PdfSimpleReport"}, } for _, c := range cases { if got := snakeToPascal(c.in); got != c.want { t.Errorf("snakeToPascal(%q) = %q, want %q", c.in, got, c.want) } } } // goFallbackEnv builds a minimal registry.db + app on disk for the .go fallback // test. The registry function gen_wails_crud_go_infra mimics wails_bind_crud: // its signature omits the `func` keyword (so the signature regex misses) and its // PascalCase("gen_wails_crud")="GenWailsCRUD" differs from the real exported // symbol "GenerateWailsCRUD". The app calls the real symbol. When writeFnFile is // true, the registry .go file exists and the fallback can recover the symbol. func goFallbackEnv(t *testing.T, fnFilePath string, writeFnFile bool) UsesFunctionsAudit { t.Helper() root := t.TempDir() dbPath := filepath.Join(root, "registry.db") db, err := sql.Open("sqlite3", dbPath) if err != nil { t.Fatal(err) } _, err = db.Exec(` CREATE TABLE functions (id TEXT PRIMARY KEY, name TEXT, domain TEXT, lang TEXT, signature TEXT, file_path TEXT); CREATE TABLE apps (id TEXT PRIMARY KEY, lang TEXT, dir_path TEXT, uses_functions TEXT DEFAULT '[]'); `) if err != nil { t.Fatal(err) } _, err = db.Exec( `INSERT INTO functions (id,name,domain,lang,signature,file_path) VALUES (?,?,?,?,?,?)`, "gen_wails_crud_go_infra", "gen_wails_crud", "infra", "go", "GenerateWailsCRUD(spec WailsCRUDSpec) string", fnFilePath, ) if err != nil { t.Fatal(err) } _, err = db.Exec( `INSERT INTO apps (id,lang,dir_path,uses_functions) VALUES (?,?,?,?)`, "myapp_go_infra", "go", "apps/myapp", `["gen_wails_crud_go_infra"]`, ) if err != nil { t.Fatal(err) } db.Close() if writeFnFile { fnAbsDir := filepath.Join(root, filepath.Dir(fnFilePath)) if err := os.MkdirAll(fnAbsDir, 0755); err != nil { t.Fatal(err) } src := "package infra\n\ntype WailsCRUDSpec struct{}\n\nfunc GenerateWailsCRUD(spec WailsCRUDSpec) string { return \"\" }\n" if err := os.WriteFile(filepath.Join(root, fnFilePath), []byte(src), 0644); err != nil { t.Fatal(err) } } appDir := filepath.Join(root, "apps", "myapp") if err := os.MkdirAll(appDir, 0755); err != nil { t.Fatal(err) } appSrc := "package main\n\nimport (\n\t\"fmt\"\n\t\"fn-registry/functions/infra\"\n)\n\nfunc main() {\n\tfmt.Println(infra.GenerateWailsCRUD(infra.WailsCRUDSpec{}))\n}\n" if err := os.WriteFile(filepath.Join(appDir, "main.go"), []byte(appSrc), 0644); err != nil { t.Fatal(err) } results, err := AuditUsesFunctions(root) if err != nil { t.Fatalf("AuditUsesFunctions: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } return results[0] } // TestAuditUsesFunctions_GoFileFallback verifies the .go fallback (issue 0057, // Fase 2): when neither the registry signature nor PascalCase(name) yields the // real exported symbol, the auditor reads the function's .go file to recover it, // so a genuinely-used function is not a false "unused". The error sub-case (file // absent) shows the fallback degrades gracefully and the function is then // correctly reported unused — proving the fallback is load-bearing. func TestAuditUsesFunctions_GoFileFallback(t *testing.T) { t.Run("golden: .go fallback recovers real symbol -> not unused", func(t *testing.T) { got := goFallbackEnv(t, "functions/infra/gen_wails_crud.go", true) if len(got.Unused) != 0 { t.Errorf("Unused = %v, want [] (fallback should find GenerateWailsCRUD)", got.Unused) } if len(got.Missing) != 0 { t.Errorf("Missing = %v, want []", got.Missing) } }) t.Run("error: missing .go file -> flagged unused, no crash", func(t *testing.T) { got := goFallbackEnv(t, "functions/infra/gen_wails_crud.go", false) if len(got.Unused) != 1 || got.Unused[0] != "gen_wails_crud_go_infra" { t.Errorf("Unused = %v, want [gen_wails_crud_go_infra] (no fallback file to read)", got.Unused) } }) } // TestAuditUsesFunctions_SharedGoFileNotMisattributed pins the regression caught // during issue 0057 verification: several registry functions can share one .go // file (the "TU adicional" pattern, e.g. cdp_new_tab living in cdp_list_tabs.go). // Because they have valid signatures, the .go fallback must stay GATED OFF for // them — otherwise the file's first exported func (here ListTabs) would be // mis-attributed to a sibling (NewTab) and suppress a genuine "unused" finding. // The app below uses only ListTabs; NewTab must remain flagged unused. func TestAuditUsesFunctions_SharedGoFileNotMisattributed(t *testing.T) { root := t.TempDir() dbPath := filepath.Join(root, "registry.db") db, err := sql.Open("sqlite3", dbPath) if err != nil { t.Fatal(err) } _, err = db.Exec(` CREATE TABLE functions (id TEXT PRIMARY KEY, name TEXT, domain TEXT, lang TEXT, signature TEXT, file_path TEXT); CREATE TABLE apps (id TEXT PRIMARY KEY, lang TEXT, dir_path TEXT, uses_functions TEXT DEFAULT '[]'); INSERT INTO functions (id,name,domain,lang,signature,file_path) VALUES ('list_tabs_go_browser','list_tabs','browser','go','func ListTabs() error','functions/browser/tabs.go'), ('new_tab_go_browser','new_tab','browser','go','func NewTab() error','functions/browser/tabs.go'); INSERT INTO apps (id,lang,dir_path,uses_functions) VALUES ('tabsapp_go_browser','go','apps/tabsapp','["list_tabs_go_browser","new_tab_go_browser"]'); `) if err != nil { t.Fatal(err) } db.Close() // Shared registry .go file: ListTabs is the FIRST exported func. fnDir := filepath.Join(root, "functions", "browser") if err := os.MkdirAll(fnDir, 0755); err != nil { t.Fatal(err) } tabsSrc := "package browser\n\nfunc ListTabs() error { return nil }\n\nfunc NewTab() error { return nil }\n" if err := os.WriteFile(filepath.Join(fnDir, "tabs.go"), []byte(tabsSrc), 0644); err != nil { t.Fatal(err) } // App calls only ListTabs, but declares both. appDir := filepath.Join(root, "apps", "tabsapp") if err := os.MkdirAll(appDir, 0755); err != nil { t.Fatal(err) } appSrc := "package main\n\nimport (\n\t\"fmt\"\n\t\"fn-registry/functions/browser\"\n)\n\nfunc main() {\n\tfmt.Println(browser.ListTabs())\n}\n" if err := os.WriteFile(filepath.Join(appDir, "main.go"), []byte(appSrc), 0644); err != nil { t.Fatal(err) } results, err := AuditUsesFunctions(root) if err != nil { t.Fatalf("AuditUsesFunctions: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } got := results[0] if len(got.Unused) != 1 || got.Unused[0] != "new_tab_go_browser" { t.Errorf("Unused = %v, want [new_tab_go_browser] (sibling must NOT rescue via shared file)", got.Unused) } } // TestGoRealExportedName verifies the .go symbol extractor: top-level exported // funcs (plain and generic) are recovered, method receivers are skipped, the // result is cached, and unreadable/empty paths return "" without error. func TestGoRealExportedName(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "functions", "infra"), 0755); err != nil { t.Fatal(err) } // File whose first exported func is preceded by an unexported func + a method. src := "package infra\n\n" + "import \"fmt\"\n\n" + "func helper() {}\n\n" + "type T struct{}\n\n" + "func (t *T) Save() {}\n\n" + "func GenerateWailsCRUD(spec int) string { fmt.Println(spec); return \"\" }\n\n" + "func WailsStreamData[X any](xs []X) {}\n" rel := "functions/infra/sample.go" if err := os.WriteFile(filepath.Join(root, rel), []byte(src), 0644); err != nil { t.Fatal(err) } cache := map[string]string{} t.Run("golden: first top-level exported func (skips helper + method)", func(t *testing.T) { if got := goRealExportedName(root, rel, cache); got != "GenerateWailsCRUD" { t.Errorf("got %q, want GenerateWailsCRUD", got) } if cache[rel] != "GenerateWailsCRUD" { t.Errorf("cache[%q] = %q, want GenerateWailsCRUD", rel, cache[rel]) } }) t.Run("edge: generic func form func Name[T any](", func(t *testing.T) { genRel := "functions/infra/gen.go" genSrc := "package infra\n\nfunc WailsStreamData[X any](xs []X) {}\n" if err := os.WriteFile(filepath.Join(root, genRel), []byte(genSrc), 0644); err != nil { t.Fatal(err) } if got := goRealExportedName(root, genRel, cache); got != "WailsStreamData" { t.Errorf("got %q, want WailsStreamData", got) } }) t.Run("error: missing file -> empty string, cached", func(t *testing.T) { missRel := "functions/infra/does_not_exist.go" if got := goRealExportedName(root, missRel, cache); got != "" { t.Errorf("got %q, want empty for missing file", got) } if v, ok := cache[missRel]; !ok || v != "" { t.Errorf("missing file should be cached as empty, got ok=%v v=%q", ok, v) } }) t.Run("error: empty file_path -> empty string", func(t *testing.T) { if got := goRealExportedName(root, "", cache); got != "" { t.Errorf("got %q, want empty for empty path", got) } }) } // TestAuditUsesFunctions_MissingDir verifies that apps whose dir_path does not // exist on disk get an entry with nil Missing/Unused slices (cannot inspect). func TestAuditUsesFunctions_MissingDir(t *testing.T) { t.Run("missing dir returns entry with nil slices", func(t *testing.T) { root := t.TempDir() createTestRegistryDB(t, root, []struct { id, lang, dirPath, usesFunctions string }{ {"testapp3_go_tools", "go", "apps/testapp3", `[]`}, }) // intentionally do NOT create apps/testapp3 on disk results, err := AuditUsesFunctions(root) if err != nil { t.Fatalf("AuditUsesFunctions: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } got := results[0] if got.Missing != nil { t.Errorf("Missing should be nil for missing dir, got %v", got.Missing) } if got.Unused != nil { t.Errorf("Unused should be nil for missing dir, got %v", got.Unused) } }) }