package main import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" "fn-registry/functions/infra" "fn-registry/registry" ) // fnBinDir holds the temp directory for the compiled fn binary. // It is created by TestMain and cleaned up at test end. var fnBinDir string var fnBinPath string // TestMain compiles the fn binary once before all tests. func TestMain(m *testing.M) { var err error fnBinDir, err = os.MkdirTemp("", "fn-vault-test-*") if err != nil { fmt.Fprintf(os.Stderr, "create temp dir: %v\n", err) os.Exit(1) } defer os.RemoveAll(fnBinDir) fnBinPath = filepath.Join(fnBinDir, "fn") // Find registry root by walking up from current directory. regRoot, err := findRoot() if err != nil { fmt.Fprintf(os.Stderr, "find root: %v\n", err) os.Exit(1) } cmd := exec.Command("go", "build", "-tags", "fts5", "-o", fnBinPath, ".") cmd.Dir = filepath.Join(regRoot, "cmd", "fn") if out, errB := cmd.CombinedOutput(); errB != nil { fmt.Fprintf(os.Stderr, "build fn: %v\n%s\n", errB, out) os.Exit(1) } os.Exit(m.Run()) } func findRoot() (string, error) { dir, err := os.Getwd() if err != nil { return "", err } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { return "", fmt.Errorf("could not find go.mod from %s", dir) } dir = parent } } func ensureFnBin(t *testing.T) string { t.Helper() return fnBinPath } // setupTestRegistry creates a minimal registry root with: // - registry.db (opened + migrations applied via registry.Open) // - a project with a vault declared in vault.yaml // - a vault directory with some test files // - a symlink from projects/test_proj/vaults/test_vault -> vault dir // // Returns (repoRoot, vaultDir). func setupTestRegistry(t *testing.T) (string, string) { t.Helper() repoRoot := t.TempDir() // Create vault directory with files. vaultDir := filepath.Join(t.TempDir(), "test_vault") if err := os.MkdirAll(filepath.Join(vaultDir, "data", "raw"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(vaultDir, "data", "raw", "report.csv"), []byte("name,value\nfoo,1"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(vaultDir, "data", "raw", "notes.md"), []byte("# Notes\nsome text"), 0644); err != nil { t.Fatal(err) } // Create project directory structure. projDir := filepath.Join(repoRoot, "projects", "test_proj") vaultsDir := filepath.Join(projDir, "vaults") if err := os.MkdirAll(vaultsDir, 0755); err != nil { t.Fatal(err) } // Create vault.yaml. vaultYAML := "vaults:\n - name: test_vault\n description: Test vault for unit tests\n path: " + vaultDir + "\n tags: [test]\n" if err := os.WriteFile(filepath.Join(vaultsDir, "vault.yaml"), []byte(vaultYAML), 0644); err != nil { t.Fatal(err) } // Create project.md. projMD := "---\nname: test_proj\ndescription: Test project\ntags: [test]\n---\n" if err := os.WriteFile(filepath.Join(projDir, "project.md"), []byte(projMD), 0644); err != nil { t.Fatal(err) } // Open registry.db (creates schema + runs migrations). db, err := registry.Open(filepath.Join(repoRoot, "registry.db")) if err != nil { t.Fatalf("registry.Open: %v", err) } // Index so the vault is registered in registry.db. if _, err := registry.Index(db, repoRoot); err != nil { t.Fatalf("registry.Index: %v", err) } db.Close() return repoRoot, vaultDir } // runFn runs the fn binary in repoRoot with the given args. func runFn(t *testing.T, repoRoot string, args ...string) (string, string, int) { t.Helper() bin := ensureFnBin(t) cmd := exec.Command(bin, args...) cmd.Dir = repoRoot var stdout, stderr strings.Builder cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() code := 0 if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { code = exitErr.ExitCode() } else { t.Logf("cmd error: %v", err) } } return stdout.String(), stderr.String(), code } // TestVaultList verifies that 'fn vault list' shows the indexed vault. func TestVaultList(t *testing.T) { repoRoot, _ := setupTestRegistry(t) out, stderr, code := runFn(t, repoRoot, "vault", "list") if code != 0 { t.Fatalf("fn vault list exit %d\nstderr: %s", code, stderr) } if !strings.Contains(out, "test_vault") { t.Errorf("expected 'test_vault' in output, got:\n%s", out) } } // TestVaultIndex verifies that 'fn vault index ' runs without error. func TestVaultIndex(t *testing.T) { repoRoot, _ := setupTestRegistry(t) out, stderr, code := runFn(t, repoRoot, "vault", "index", "test_vault") if code != 0 { t.Fatalf("fn vault index exit %d\nstderr: %s\nstdout: %s", code, stderr, out) } if !strings.Contains(out, "indexed") { t.Errorf("expected 'indexed' in output, got:\n%s", out) } } // TestVaultSearchJSON verifies that 'fn vault search --json' returns valid JSON array. func TestVaultSearchJSON(t *testing.T) { repoRoot, vaultDir := setupTestRegistry(t) // First index the vault so there is something to search. if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 { t.Fatal("fn vault index failed") } // Seed some content into the vault index for the search to find. db, err := infra.VaultIndexOpen(vaultDir) if err != nil { t.Fatalf("VaultIndexOpen: %v", err) } // Update content_text for FTS search. db.Exec(`DELETE FROM files_fts WHERE rel_path = 'data/raw/report.csv'`) db.Exec(`INSERT INTO files_fts(rel_path, content_text) VALUES ('data/raw/report.csv', 'foo report data')`) db.Close() out, stderr, code := runFn(t, repoRoot, "vault", "search", "report", "--json", "--vault", "test_vault") if code != 0 { t.Fatalf("fn vault search exit %d\nstderr: %s", code, stderr) } var result []map[string]interface{} if err := json.Unmarshal([]byte(out), &result); err != nil { t.Fatalf("output is not valid JSON: %v\nraw: %s", err, out) } // Should be a JSON array (possibly empty if search finds nothing, but must be valid). t.Logf("search returned %d hits", len(result)) } // TestVaultInfo verifies that 'fn vault info ' outputs vault stats. func TestVaultInfo(t *testing.T) { repoRoot, _ := setupTestRegistry(t) // Index first. if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 { t.Fatal("fn vault index failed") } out, stderr, code := runFn(t, repoRoot, "vault", "info", "test_vault") if code != 0 { t.Fatalf("fn vault info exit %d\nstderr: %s", code, stderr) } if !strings.Contains(out, "test_vault") { t.Errorf("expected vault name in output, got:\n%s", out) } if !strings.Contains(out, "Files:") { t.Errorf("expected 'Files:' in output, got:\n%s", out) } } // TestFormatBytes verifies the formatBytes helper. func TestFormatBytes(t *testing.T) { cases := []struct { input int64 expected string }{ {500, "500 B"}, {1024, "1.0 KB"}, {1536, "1.5 KB"}, {1048576, "1.0 MB"}, {1073741824, "1.0 GB"}, } for _, tc := range cases { got := formatBytes(tc.input) if got != tc.expected { t.Errorf("formatBytes(%d) = %q, want %q", tc.input, got, tc.expected) } } } // TestVaultLayoutEnsure verifies that 'fn vault layout-ensure --dry-run' works. func TestVaultLayoutEnsure(t *testing.T) { repoRoot, _ := setupTestRegistry(t) out, stderr, code := runFn(t, repoRoot, "vault", "layout-ensure", "test_vault", "--dry-run") if code != 0 { t.Fatalf("fn vault layout-ensure exit %d\nstderr: %s\nstdout: %s", code, stderr, out) } if !strings.Contains(out, "test_vault") { t.Errorf("expected vault name in output, got:\n%s", out) } } // TestVaultAggregate verifies that 'fn vault aggregate' runs without error on a clean registry. func TestVaultAggregate(t *testing.T) { repoRoot, _ := setupTestRegistry(t) // Index first so there is something to aggregate. if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 { t.Fatal("fn vault index failed") } _, stderr, code := runFn(t, repoRoot, "vault", "aggregate") if code != 0 { t.Fatalf("fn vault aggregate exit %d\nstderr: %s", code, stderr) } } // TestVaultDoctor verifies that 'fn vault doctor' runs and reports on vaults. func TestVaultDoctor(t *testing.T) { repoRoot, _ := setupTestRegistry(t) out, stderr, code := runFn(t, repoRoot, "vault", "doctor") if code != 0 { t.Fatalf("fn vault doctor exit %d\nstderr: %s", code, stderr) } if !strings.Contains(out, "test_vault") { t.Errorf("expected 'test_vault' in doctor output, got:\n%s", out) } } // TestVaultDedupe verifies that 'fn vault dedupe' runs without error after indexing. func TestVaultDedupe(t *testing.T) { repoRoot, _ := setupTestRegistry(t) if _, _, code := runFn(t, repoRoot, "vault", "index", "test_vault"); code != 0 { t.Fatal("fn vault index failed") } out, stderr, code := runFn(t, repoRoot, "vault", "dedupe", "test_vault") if code != 0 { t.Fatalf("fn vault dedupe exit %d\nstderr: %s", code, stderr) } // Should say "No duplicates" or show a table — either is fine. _ = out } // TestVaultAuditDryRun verifies that 'fn vault audit --dry-run-layout --skip-profilers' works. func TestVaultAuditDryRun(t *testing.T) { repoRoot, _ := setupTestRegistry(t) out, stderr, code := runFn(t, repoRoot, "vault", "audit", "test_vault", "--dry-run-layout", "--skip-profilers") // Exit 0 = fully ok; exit 4 = warnings (layout issues) — both acceptable here. if code != 0 && code != 4 { t.Fatalf("fn vault audit exit %d\nstderr: %s\nstdout: %s", code, stderr, out) } if !strings.Contains(out, "summary") { t.Errorf("expected 'summary' section in audit output, got:\n%s", out) } } // Suppress unused import for time. var _ = time.Now