Files
fn_registry/functions/infra/pc_locations_drift_test.go
T
egutierrez 625569485f 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

178 lines
4.4 KiB
Go

package infra
import (
"database/sql"
"os"
"path/filepath"
"testing"
_ "github.com/mattn/go-sqlite3"
)
// setupTestRegistry creates a minimal registry.db with pc_locations + apps tables
// in a temp directory and returns the root path + a cleanup func.
func setupTestRegistry(t *testing.T) (string, func()) {
t.Helper()
root := t.TempDir()
dbPath := filepath.Join(root, "registry.db")
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatalf("open test db: %v", err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE pc_locations (
id TEXT PRIMARY KEY,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
pc_id TEXT NOT NULL,
dir_path TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT,
created_at TEXT,
updated_at TEXT
);
CREATE TABLE apps (
id TEXT PRIMARY KEY,
name TEXT,
dir_path TEXT NOT NULL DEFAULT ''
);
CREATE TABLE analysis (
id TEXT PRIMARY KEY,
name TEXT,
dir_path TEXT NOT NULL DEFAULT ''
);
`)
if err != nil {
t.Fatalf("create schema: %v", err)
}
return root, func() {}
}
func insertLocation(t *testing.T, root, entityType, entityID, dirPath, status, pcID string) {
t.Helper()
db, _ := sql.Open("sqlite3", filepath.Join(root, "registry.db"))
defer db.Close()
id := entityType + "_" + entityID + "_" + pcID
_, err := db.Exec(
`INSERT INTO pc_locations(id, entity_type, entity_id, pc_id, dir_path, status) VALUES (?,?,?,?,?,?)`,
id, entityType, entityID, pcID, dirPath, status,
)
if err != nil {
t.Fatalf("insert location: %v", err)
}
}
func insertApp(t *testing.T, root, id, dirPath string) {
t.Helper()
db, _ := sql.Open("sqlite3", filepath.Join(root, "registry.db"))
defer db.Close()
_, err := db.Exec(`INSERT INTO apps(id, dir_path) VALUES (?,?)`, id, dirPath)
if err != nil {
t.Fatalf("insert app: %v", err)
}
}
func TestPcLocationsDrift_DetectsMissingFolder(t *testing.T) {
root, cleanup := setupTestRegistry(t)
defer cleanup()
// An active entry whose folder does NOT exist on disk
insertLocation(t, root, "app", "my_app", "apps/my_app", "active", "test-pc")
drifts, err := PcLocationsDrift(root, "test-pc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
found := false
for _, d := range drifts {
if d.EntityID == "my_app" && d.Issue == "missing_on_disk" {
found = true
}
}
if !found {
t.Errorf("expected missing_on_disk drift for my_app, got: %+v", drifts)
}
}
func TestPcLocationsDrift_ActiveFolderExistsNoDrift(t *testing.T) {
root, cleanup := setupTestRegistry(t)
defer cleanup()
// Create the folder
appDir := filepath.Join(root, "apps", "good_app")
if err := os.MkdirAll(appDir, 0755); err != nil {
t.Fatal(err)
}
insertLocation(t, root, "app", "good_app", "apps/good_app", "active", "test-pc")
drifts, err := PcLocationsDrift(root, "test-pc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, d := range drifts {
if d.EntityID == "good_app" {
t.Errorf("unexpected drift for good_app: %+v", d)
}
}
}
func TestPcLocationsDrift_StatusShouldBeActive(t *testing.T) {
root, cleanup := setupTestRegistry(t)
defer cleanup()
// Folder exists but status is "missing"
appDir := filepath.Join(root, "apps", "came_back")
if err := os.MkdirAll(appDir, 0755); err != nil {
t.Fatal(err)
}
insertLocation(t, root, "app", "came_back", "apps/came_back", "missing", "test-pc")
drifts, err := PcLocationsDrift(root, "test-pc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
found := false
for _, d := range drifts {
if d.EntityID == "came_back" && d.Issue == "status_should_be_active" {
found = true
}
}
if !found {
t.Errorf("expected status_should_be_active for came_back, got: %+v", drifts)
}
}
func TestPcLocationsDrift_UntrackedOnDisk(t *testing.T) {
root, cleanup := setupTestRegistry(t)
defer cleanup()
// App indexed in registry with folder on disk but no pc_locations entry
appDir := filepath.Join(root, "apps", "orphan_app")
if err := os.MkdirAll(appDir, 0755); err != nil {
t.Fatal(err)
}
insertApp(t, root, "orphan_app", "apps/orphan_app")
// No pc_locations entry for test-pc
drifts, err := PcLocationsDrift(root, "test-pc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
found := false
for _, d := range drifts {
if d.EntityID == "orphan_app" && d.Issue == "untracked_on_disk" {
found = true
}
}
if !found {
t.Errorf("expected untracked_on_disk for orphan_app, got: %+v", drifts)
}
}