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>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user