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>
104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
package infra
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// setupTestDB creates a minimal registry.db with apps and analysis tables.
|
|
func setupTestDB(t *testing.T, root string) {
|
|
t.Helper()
|
|
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 apps (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
dir_path TEXT NOT NULL DEFAULT ''
|
|
);
|
|
CREATE TABLE analysis (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
dir_path TEXT NOT NULL DEFAULT ''
|
|
);
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("create tables: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestArtefactDoctor_DetectsMissingDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
setupTestDB(t, root)
|
|
|
|
db, _ := sql.Open("sqlite3", filepath.Join(root, "registry.db"))
|
|
defer db.Close()
|
|
db.Exec("INSERT INTO apps (id, name, dir_path) VALUES ('ghost_app', 'ghost', 'apps/ghost_app')")
|
|
|
|
checks, err := ArtefactDoctor(root)
|
|
if err != nil {
|
|
t.Fatalf("ArtefactDoctor error: %v", err)
|
|
}
|
|
if len(checks) != 1 {
|
|
t.Fatalf("expected 1 check, got %d", len(checks))
|
|
}
|
|
c := checks[0]
|
|
if c.OK {
|
|
t.Errorf("expected not OK for missing dir, got OK")
|
|
}
|
|
found := false
|
|
for _, iss := range c.Issues {
|
|
if iss == "directory_missing" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected 'directory_missing' issue, got %v", c.Issues)
|
|
}
|
|
}
|
|
|
|
func TestArtefactDoctor_OKArtefact(t *testing.T) {
|
|
root := t.TempDir()
|
|
setupTestDB(t, root)
|
|
|
|
// Create a minimal app dir with .git and app.md
|
|
appDir := filepath.Join(root, "apps", "my_app")
|
|
if err := os.MkdirAll(filepath.Join(appDir, ".git"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appMd := "---\nname: my_app\ndescription: test app\n---\n"
|
|
if err := os.WriteFile(filepath.Join(appDir, "app.md"), []byte(appMd), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Simulate a git repo with upstream by creating a packed-refs with HEAD
|
|
// We won't actually init git, so no_upstream_branch will fire — that's fine.
|
|
// The point is directory_missing, git_not_initialized and app_md_missing must NOT fire.
|
|
|
|
db, _ := sql.Open("sqlite3", filepath.Join(root, "registry.db"))
|
|
defer db.Close()
|
|
db.Exec("INSERT INTO apps (id, name, dir_path) VALUES ('my_app_go_tools', 'my_app', 'apps/my_app')")
|
|
|
|
checks, err := ArtefactDoctor(root)
|
|
if err != nil {
|
|
t.Fatalf("ArtefactDoctor error: %v", err)
|
|
}
|
|
if len(checks) != 1 {
|
|
t.Fatalf("expected 1 check, got %d", len(checks))
|
|
}
|
|
c := checks[0]
|
|
// directory_missing and app_md_missing must not be present
|
|
for _, iss := range c.Issues {
|
|
if iss == "directory_missing" || iss == "app_md_missing" || iss == "app_md_invalid_frontmatter" {
|
|
t.Errorf("unexpected issue %q in %v", iss, c.Issues)
|
|
}
|
|
}
|
|
}
|