116 lines
3 KiB
Go
116 lines
3 KiB
Go
|
|
package portfolio
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestReindexAndSearch(t *testing.T) {
|
||
|
|
// Use a temp dir so we don't pollute data/projects
|
||
|
|
dir := t.TempDir()
|
||
|
|
srcDir := filepath.Join(dir, "src")
|
||
|
|
if err := os.MkdirAll(srcDir, 0o755); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Copy the real example-project.md
|
||
|
|
data, err := os.ReadFile("../../data/projects/example-project.md")
|
||
|
|
if err != nil {
|
||
|
|
t.Skip("example-project.md not found, skipping integration test:", err)
|
||
|
|
}
|
||
|
|
if err := os.WriteFile(filepath.Join(srcDir, "example-project.md"), data, 0o644); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
dbPath := filepath.Join(dir, "test.db")
|
||
|
|
store, err := OpenStore(dbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
files, chunks, err := store.Reindex(context.Background(), srcDir, DefaultChunkerConfig())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if files != 1 {
|
||
|
|
t.Errorf("files = %d, want 1", files)
|
||
|
|
}
|
||
|
|
if chunks < 4 {
|
||
|
|
t.Errorf("chunks = %d, want >= 4 (frontmatter + 4 H2 sections)", chunks)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Search for something specific to the file
|
||
|
|
hits, err := store.Search(context.Background(), "Go SQLite framework", 5)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(hits) == 0 {
|
||
|
|
t.Error("expected at least one hit for 'Go SQLite framework'")
|
||
|
|
}
|
||
|
|
t.Logf("Search hits: %d", len(hits))
|
||
|
|
for i, h := range hits {
|
||
|
|
t.Logf(" [%d] project=%s section=%s score=%.3f body=%q", i, h.ProjectID, h.Section, h.Score, truncate(h.Content, 80))
|
||
|
|
}
|
||
|
|
|
||
|
|
// No results for nonsense
|
||
|
|
hits, err = store.Search(context.Background(), "asdfqwerty12345", 5)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(hits) != 0 {
|
||
|
|
t.Errorf("expected 0 hits for nonsense query, got %d", len(hits))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestReindexFromRealData(t *testing.T) {
|
||
|
|
// Index the real data/projects/ to sanity-check on real data
|
||
|
|
srcDir := "../../data/projects"
|
||
|
|
if _, err := os.Stat(srcDir); err != nil {
|
||
|
|
t.Skip("data/projects not found:", err)
|
||
|
|
}
|
||
|
|
dir := t.TempDir()
|
||
|
|
dbPath := filepath.Join(dir, "real.db")
|
||
|
|
store, err := OpenStore(dbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer store.Close()
|
||
|
|
files, chunks, err := store.Reindex(context.Background(), srcDir, DefaultChunkerConfig())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
t.Logf("Indexed %d files, %d chunks", files, chunks)
|
||
|
|
if files == 0 {
|
||
|
|
t.Fatal("no files indexed")
|
||
|
|
}
|
||
|
|
// Print what we got
|
||
|
|
sections := map[string]int{}
|
||
|
|
_, _ = store.Search(context.Background(), "rony", 100) // noop
|
||
|
|
// Walk DB to see sections — easier: just re-read with a fresh load
|
||
|
|
_ = sections
|
||
|
|
|
||
|
|
// Verify queries work
|
||
|
|
for _, q := range []string{"rony-llm-agent", "Qwen", "agent harness", "tech stack", "deploy"} {
|
||
|
|
hits, err := store.Search(context.Background(), q, 3)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
t.Logf("query %q → %d hits", q, len(hits))
|
||
|
|
for _, h := range hits {
|
||
|
|
if !strings.Contains(strings.ToLower(h.Content), strings.ToLower(strings.SplitN(q, "-", 2)[0])) {
|
||
|
|
// FTS5 might do prefix matches; this is a soft assertion
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func truncate(s string, n int) string {
|
||
|
|
s = strings.ReplaceAll(s, "\n", " ")
|
||
|
|
if len(s) <= n {
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
return s[:n] + "..."
|
||
|
|
}
|