package infra import ( "testing" "time" ) // openTestVaultDB creates a fresh vault_index.db in a temp dir and returns the path. func openTestVaultDir(t *testing.T) string { t.Helper() dir := t.TempDir() db, err := VaultIndexOpen(dir) if err != nil { t.Fatalf("VaultIndexOpen: %v", err) } db.Close() return dir } // seedVaultFile inserts a row into files + files_fts. func seedVaultFile(t *testing.T, dir, relPath, mime, bucket, subBucket, contentText string, size int64) { t.Helper() db, err := VaultIndexOpen(dir) if err != nil { t.Fatalf("VaultIndexOpen seed: %v", err) } defer db.Close() now := time.Now().Unix() _, err = db.Exec(` INSERT INTO files (rel_path, size, mtime, sha256, mime, ext, bucket, sub_bucket, indexed_at) VALUES (?, ?, ?, 'aabbccdd', ?, '', ?, ?, ?)`, relPath, size, now, mime, bucket, subBucket, now, ) if err != nil { t.Fatalf("seed files: %v", err) } _, err = db.Exec(`INSERT INTO files_fts(rel_path, content_text) VALUES (?, ?)`, relPath, contentText) if err != nil { t.Fatalf("seed files_fts: %v", err) } } // --- Tests --- func TestVaultSearch_FTSMatch(t *testing.T) { t.Run("FTS match devuelve hit con snippet", func(t *testing.T) { dir := openTestVaultDir(t) seedVaultFile(t, dir, "data/raw/informe.csv", "text/csv", "data", "raw", "ventas trimestrales empresa iberica", 1024) seedVaultFile(t, dir, "data/raw/other.csv", "text/csv", "data", "raw", "productos inventario almacen", 512) hits, err := VaultSearch(dir, "ventas", 10) if err != nil { t.Fatalf("VaultSearch: %v", err) } if len(hits) != 1 { t.Fatalf("got %d hits, want 1", len(hits)) } if hits[0].RelPath != "data/raw/informe.csv" { t.Errorf("RelPath = %q, want data/raw/informe.csv", hits[0].RelPath) } if hits[0].VaultName == "" { t.Errorf("VaultName should not be empty") } }) } func TestVaultSearch_NoMatch(t *testing.T) { t.Run("query sin resultados retorna slice vacio", func(t *testing.T) { dir := openTestVaultDir(t) seedVaultFile(t, dir, "data/raw/file.csv", "text/csv", "data", "raw", "some content", 100) hits, err := VaultSearch(dir, "zzznomatch", 10) if err != nil { t.Fatalf("VaultSearch: %v", err) } if len(hits) != 0 { t.Errorf("got %d hits, want 0", len(hits)) } }) } func TestVaultSearch_LimitRespected(t *testing.T) { t.Run("limit se respeta", func(t *testing.T) { dir := openTestVaultDir(t) for i := 0; i < 10; i++ { path := "data/raw/file" + string(rune('a'+i)) + ".csv" seedVaultFile(t, dir, path, "text/csv", "data", "raw", "common keyword everywhere", 100) } hits, err := VaultSearch(dir, "common", 3) if err != nil { t.Fatalf("VaultSearch: %v", err) } if len(hits) != 3 { t.Errorf("got %d hits, want 3", len(hits)) } }) } func TestVaultSearch_BadFTSQuery_FallbackLike(t *testing.T) { t.Run("query FTS invalida activa fallback LIKE", func(t *testing.T) { dir := openTestVaultDir(t) // Insert a file whose rel_path contains "foobar" so LIKE can find it. seedVaultFile(t, dir, "data/raw/foobar_report.csv", "text/csv", "data", "raw", "", 200) // "foo:bar:" — colon after a non-column name triggers FTS5 parser error. // safeFTSQuery passes it through unchanged because it contains ":" // → FTS5 "no such column: bar" → fallback LIKE on rel_path. hits, err := VaultSearch(dir, "foo:bar:", 10) if err != nil { t.Fatalf("VaultSearch: %v", err) } if len(hits) == 0 { t.Errorf("expected fallback LIKE to find foobar_report.csv, got 0 hits") } for _, h := range hits { if h.Snippet != "" { t.Errorf("fallback hits should have empty Snippet, got %q", h.Snippet) } } }) } func TestVaultSearch_LimitZeroDefaults(t *testing.T) { t.Run("limit cero usa 50 por defecto", func(t *testing.T) { dir := openTestVaultDir(t) // Insert 55 files with the same keyword. for i := 0; i < 55; i++ { path := "data/raw/doc" + string(rune('a')) + string(rune(int('0')+i%10)) + ".csv" if i >= 10 { path = "data/raw/doc" + string(rune('b'+i/10-1)) + string(rune(int('0')+i%10)) + ".csv" } seedVaultFile(t, dir, path, "text/csv", "data", "raw", "keyword alpha beta", 100) } hits, err := VaultSearch(dir, "keyword", 0) if err != nil { t.Fatalf("VaultSearch: %v", err) } if len(hits) != 50 { t.Errorf("got %d hits, want 50 (default limit)", len(hits)) } }) }