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>
91 lines
2.2 KiB
Go
91 lines
2.2 KiB
Go
package infra
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// NOTE: readFnPC is defined in pc_locations_drift.go with signature (string, error).
|
|
|
|
// createMinimalRegistry creates a temporary registry.db with the apps table
|
|
// but no rows, for testing ServicesStatus with an empty dataset.
|
|
func createMinimalRegistry(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)
|
|
}
|
|
_, err = db.Exec(`CREATE TABLE apps (id TEXT, name TEXT, tags TEXT, notes TEXT, description TEXT)`)
|
|
if err != nil {
|
|
t.Fatalf("create table: %v", err)
|
|
}
|
|
db.Close()
|
|
return dir
|
|
}
|
|
|
|
func TestServicesStatus_NoApps(t *testing.T) {
|
|
root := createMinimalRegistry(t)
|
|
got, err := ServicesStatus(root)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("expected empty slice, got %d entries", len(got))
|
|
}
|
|
}
|
|
|
|
func TestServicesStatus_BadDB(t *testing.T) {
|
|
_, err := ServicesStatus("/nonexistent/path")
|
|
if err == nil {
|
|
t.Error("expected error for missing db, got nil")
|
|
}
|
|
}
|
|
|
|
func TestServicesStatus_ParseFirstPort(t *testing.T) {
|
|
cases := []struct {
|
|
text string
|
|
want int
|
|
}{
|
|
{"listens on port 9090 for requests", 9090},
|
|
{"API available at :8080", 8080},
|
|
{"no port mentioned", 0},
|
|
{"low port 80 and high 9000", 9000}, // 80 < 1024, skip; 9000 ok
|
|
{"port 65535 max", 65535},
|
|
}
|
|
for _, c := range cases {
|
|
got := parseFirstPort(c.text)
|
|
if got != c.want {
|
|
t.Errorf("parseFirstPort(%q) = %d, want %d", c.text, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServicesStatus_ReadFnPC(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pcFile := filepath.Join(dir, ".fn_pc")
|
|
|
|
// readFnPC returns the first non-empty line (no comment stripping)
|
|
if err := os.WriteFile(pcFile, []byte("home-wsl\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Temporarily override HOME so readFnPC picks up our file
|
|
old := os.Getenv("HOME")
|
|
t.Cleanup(func() { os.Setenv("HOME", old) })
|
|
os.Setenv("HOME", dir)
|
|
|
|
got, err2 := readFnPC()
|
|
if err2 != nil {
|
|
t.Fatalf("readFnPC error: %v", err2)
|
|
}
|
|
if got != "home-wsl" {
|
|
t.Errorf("readFnPC() = %q, want %q", got, "home-wsl")
|
|
}
|
|
}
|