a802f59f55
- cmd/fn/doctor.go - cmd/fn/main.go - cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt - cpp/apps/primitives_gallery/playground/tables/data_table.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.h - cpp/apps/primitives_gallery/playground/tables/self_test.cpp - cpp/apps/primitives_gallery/playground/tables/tql.cpp - cpp/apps/primitives_gallery/playground/tables/viz.cpp - cpp/apps/primitives_gallery/playground/tables/viz.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
319 lines
9.5 KiB
Go
319 lines
9.5 KiB
Go
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 <name>' 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 <name>' 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
|