go-help/catalog_test.go
Snider 62c5d65374
All checks were successful
Security Scan / security (push) Successful in 14s
Test / test (push) Successful in 1m44s
feat: modernise to Go 1.26 iterators and stdlib helpers
Add Catalog.All(), Catalog.SearchResults(), AllSections(), Tokens()
iterators. Use slices.SortFunc, slices.Clone, maps.Values, built-in
min replacing min3, strings.SplitSeq for streaming, range-over-int
in levenshtein and benchmarks.

Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:32:21 +00:00

212 lines
5.2 KiB
Go

package help
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefaultCatalog_Good(t *testing.T) {
c := DefaultCatalog()
require.NotNil(t, c)
require.NotNil(t, c.topics)
require.NotNil(t, c.index)
t.Run("contains built-in topics", func(t *testing.T) {
topics := c.List()
assert.GreaterOrEqual(t, len(topics), 2, "should have at least 2 default topics")
})
t.Run("getting-started topic exists", func(t *testing.T) {
topic, err := c.Get("getting-started")
require.NoError(t, err)
assert.Equal(t, "Getting Started", topic.Title)
assert.Contains(t, topic.Content, "Common Commands")
})
t.Run("config topic exists", func(t *testing.T) {
topic, err := c.Get("config")
require.NoError(t, err)
assert.Equal(t, "Configuration", topic.Title)
assert.Contains(t, topic.Content, "Environment Variables")
})
}
func TestCatalog_Add_Good(t *testing.T) {
c := &Catalog{
topics: make(map[string]*Topic),
index: newSearchIndex(),
}
topic := &Topic{
ID: "test-topic",
Title: "Test Topic",
Content: "This is a test topic for unit testing.",
Tags: []string{"test", "unit"},
}
c.Add(topic)
t.Run("topic is retrievable after add", func(t *testing.T) {
got, err := c.Get("test-topic")
require.NoError(t, err)
assert.Equal(t, topic, got)
})
t.Run("topic is searchable after add", func(t *testing.T) {
results := c.Search("test")
assert.NotEmpty(t, results)
})
t.Run("overwrite existing topic", func(t *testing.T) {
replacement := &Topic{
ID: "test-topic",
Title: "Replaced Topic",
Content: "Replacement content.",
}
c.Add(replacement)
got, err := c.Get("test-topic")
require.NoError(t, err)
assert.Equal(t, "Replaced Topic", got.Title)
})
}
func TestCatalog_List_Good(t *testing.T) {
c := &Catalog{
topics: make(map[string]*Topic),
index: newSearchIndex(),
}
t.Run("empty catalog returns empty list", func(t *testing.T) {
list := c.List()
assert.Empty(t, list)
})
t.Run("returns all added topics", func(t *testing.T) {
c.Add(&Topic{ID: "alpha", Title: "Alpha"})
c.Add(&Topic{ID: "beta", Title: "Beta"})
c.Add(&Topic{ID: "gamma", Title: "Gamma"})
list := c.List()
assert.Len(t, list, 3)
// Collect IDs (order is not guaranteed from map)
ids := make(map[string]bool)
for _, t := range list {
ids[t.ID] = true
}
assert.True(t, ids["alpha"])
assert.True(t, ids["beta"])
assert.True(t, ids["gamma"])
})
}
func TestCatalog_Search_Good(t *testing.T) {
c := DefaultCatalog()
t.Run("finds default topics", func(t *testing.T) {
results := c.Search("configuration")
assert.NotEmpty(t, results)
})
t.Run("empty query returns nil", func(t *testing.T) {
results := c.Search("")
assert.Nil(t, results)
})
t.Run("no match returns empty", func(t *testing.T) {
results := c.Search("zzzyyyxxx")
assert.Empty(t, results)
})
}
func TestCatalog_Get_Good(t *testing.T) {
c := &Catalog{
topics: make(map[string]*Topic),
index: newSearchIndex(),
}
c.Add(&Topic{ID: "exists", Title: "Existing Topic"})
t.Run("existing topic", func(t *testing.T) {
topic, err := c.Get("exists")
require.NoError(t, err)
assert.Equal(t, "Existing Topic", topic.Title)
})
t.Run("missing topic returns error", func(t *testing.T) {
topic, err := c.Get("does-not-exist")
assert.Nil(t, topic)
assert.Error(t, err)
assert.Contains(t, err.Error(), "topic not found")
assert.Contains(t, err.Error(), "does-not-exist")
})
}
func TestCatalog_Search_Good_ScoreTiebreaking(t *testing.T) {
// Tests the alphabetical tie-breaking in search result sorting (search.go:165).
c := &Catalog{
topics: make(map[string]*Topic),
index: newSearchIndex(),
}
// Add topics with identical content so they receive the same score.
c.Add(&Topic{
ID: "zebra-topic",
Title: "Zebra",
Content: "Unique keyword zephyr.",
})
c.Add(&Topic{
ID: "alpha-topic",
Title: "Alpha",
Content: "Unique keyword zephyr.",
})
results := c.Search("zephyr")
require.Len(t, results, 2)
// With equal scores, results should be sorted alphabetically by title.
assert.Equal(t, "Alpha", results[0].Topic.Title)
assert.Equal(t, "Zebra", results[1].Topic.Title)
assert.Equal(t, results[0].Score, results[1].Score,
"scores should be equal for tie-breaking to apply")
}
func BenchmarkSearch(b *testing.B) {
// Build a catalog with 100+ topics for benchmarking.
c := &Catalog{
topics: make(map[string]*Topic),
index: newSearchIndex(),
}
for i := range 150 {
c.Add(&Topic{
ID: fmt.Sprintf("topic-%d", i),
Title: fmt.Sprintf("Topic Number %d About Various Subjects", i),
Content: fmt.Sprintf("This is the content of topic %d. It covers installation, configuration, deployment, and testing of the system.", i),
Tags: []string{"generated", fmt.Sprintf("tag%d", i%10)},
Sections: []Section{
{
ID: fmt.Sprintf("section-%d-a", i),
Title: "Overview",
Content: "An overview of the topic and its purpose.",
},
{
ID: fmt.Sprintf("section-%d-b", i),
Title: "Details",
Content: "Detailed information about the topic including examples and usage.",
},
},
})
}
b.ResetTimer()
b.ReportAllocs()
for range b.N {
c.Search("installation configuration")
}
}