Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345package issues_indexer
import ( "context" "os" "strings" "testing"
"github.com/blevesearch/bleve/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "tangled.org/core/appview/models" "tangled.org/core/appview/pagination" "tangled.org/core/appview/searchquery")
func setupTestIndexer(t *testing.T) (*Indexer, func()) { t.Helper()
tmpDir, err := os.MkdirTemp("", "issue_indexer_test") require.NoError(t, err)
ix := NewIndexer(tmpDir)
mapping, err := generateIssueIndexMapping() require.NoError(t, err)
indexer, err := bleve.New(tmpDir, mapping) require.NoError(t, err) ix.indexer = indexer
cleanup := func() { ix.indexer.Close() os.RemoveAll(tmpDir) }
return ix, cleanup}
func boolPtr(b bool) *bool { return &b }
// makeLabelState creates a LabelState for testing. Each entry is either// "name" (null-type label) or "name=val" (valued label).func makeLabelState(entries ...string) models.LabelState { state := models.NewLabelState() for _, entry := range entries { if eqIdx := strings.Index(entry, "="); eqIdx > 0 { name := entry[:eqIdx] val := entry[eqIdx+1:] if state.Inner()[name] == nil { state.Inner()[name] = make(map[string]struct{}) } state.Inner()[name][val] = struct{}{} state.SetName(name, name) } else { state.Inner()[entry] = make(map[string]struct{}) state.SetName(entry, entry) } } return state}
func TestSearchFilters(t *testing.T) { ix, cleanup := setupTestIndexer(t) defer cleanup()
ctx := context.Background()
err := ix.Index(ctx, models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")}, models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature")}, models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "Fix login timeout", Body: "Login takes too long", Open: false, Did: "did:plc:alice", Labels: makeLabelState("bug")}, ) require.NoError(t, err)
opts := func() models.IssueSearchOptions { return models.IssueSearchOptions{ RepoDid: "did:plc:testrepo", IsOpen: boolPtr(true), Page: pagination.Page{Limit: 10}, } }
// Keyword in title o := opts() o.Keywords = []string{"bug"} result, err := ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))
// Keyword in body o = opts() o.Keywords = []string{"theme"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(2))
// Phrase match o = opts() o.Phrases = []string{"login bug"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))
// Author filter o = opts() o.AuthorDid = "did:plc:alice" result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))
// Label filter o = opts() o.Labels = []string{"bug"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))
// State filter (closed) o = opts() o.IsOpen = boolPtr(false) o.Labels = []string{"bug"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(3))
// Combined: keyword + author + label o = opts() o.Keywords = []string{"login"} o.AuthorDid = "did:plc:alice" o.Labels = []string{"bug"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))}
func TestSearchLabelAND(t *testing.T) { ix, cleanup := setupTestIndexer(t) defer cleanup()
ctx := context.Background()
err := ix.Index(ctx, models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Issue 1", Body: "Body", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")}, models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Issue 2", Body: "Body", Open: true, Did: "did:plc:bob", Labels: makeLabelState("bug", "urgent")}, ) require.NoError(t, err)
result, err := ix.Search(ctx, models.IssueSearchOptions{ RepoDid: "did:plc:testrepo", IsOpen: boolPtr(true), Labels: []string{"bug", "urgent"}, Page: pagination.Page{Limit: 10}, }) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(2))}
func TestSearchNegation(t *testing.T) { ix, cleanup := setupTestIndexer(t) defer cleanup()
ctx := context.Background()
err := ix.Index(ctx, models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")}, models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature")}, models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "Fix timeout bug", Body: "Timeout on save", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug", "urgent")}, ) require.NoError(t, err)
opts := func() models.IssueSearchOptions { return models.IssueSearchOptions{ RepoDid: "did:plc:testrepo", IsOpen: boolPtr(true), Page: pagination.Page{Limit: 10}, } }
// Negated label: exclude "bug", should only return issue 2 o := opts() o.NegatedLabels = []string{"bug"} result, err := ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(2))
// Negated keyword: exclude "login", should return issues 2 and 3 o = opts() o.NegatedKeywords = []string{"login"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(2), result.Total) assert.Contains(t, result.Hits, int64(2)) assert.Contains(t, result.Hits, int64(3))
// Positive label + negated label: bug but not urgent o = opts() o.Labels = []string{"bug"} o.NegatedLabels = []string{"urgent"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))
// Negated phrase o = opts() o.NegatedPhrases = []string{"dark theme"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(2), result.Total) assert.NotContains(t, result.Hits, int64(2))}
func TestSearchNegatedPhraseParsed(t *testing.T) { ix, cleanup := setupTestIndexer(t) defer cleanup()
ctx := context.Background()
err := ix.Index(ctx, models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice"}, models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob"}, models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "Fix timeout bug", Body: "Timeout on save", Open: true, Did: "did:plc:alice"}, ) require.NoError(t, err)
// Parse a query with a negated quoted phrase, as the handler would query := searchquery.Parse(`-"dark theme"`) var negatedPhrases []string for _, item := range query.Items() { if item.Kind == searchquery.KindQuoted && item.Negated { negatedPhrases = append(negatedPhrases, item.Value) } } require.Equal(t, []string{"dark theme"}, negatedPhrases)
result, err := ix.Search(ctx, models.IssueSearchOptions{ RepoDid: "did:plc:testrepo", IsOpen: boolPtr(true), NegatedPhrases: negatedPhrases, Page: pagination.Page{Limit: 10}, }) require.NoError(t, err) assert.Equal(t, uint64(2), result.Total) assert.NotContains(t, result.Hits, int64(2))}
func TestSearchNoResults(t *testing.T) { ix, cleanup := setupTestIndexer(t) defer cleanup()
ctx := context.Background()
err := ix.Index(ctx, models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Issue", Body: "Body", Open: true, Did: "did:plc:alice"}, ) require.NoError(t, err)
result, err := ix.Search(ctx, models.IssueSearchOptions{ Keywords: []string{"nonexistent"}, RepoDid: "did:plc:testrepo", IsOpen: boolPtr(true), Page: pagination.Page{Limit: 10}, }) require.NoError(t, err) assert.Equal(t, uint64(0), result.Total) assert.Empty(t, result.Hits)}
func TestSearchLabelValues(t *testing.T) { ix, cleanup := setupTestIndexer(t) defer cleanup()
ctx := context.Background()
err := ix.Index(ctx, models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "High priority bug", Body: "Urgent", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug", "priority=high")}, models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Low priority feature", Body: "Nice to have", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature", "priority=low")}, models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "High priority feature", Body: "Important", Open: true, Did: "did:plc:alice", Labels: makeLabelState("feature", "priority=high")}, ) require.NoError(t, err)
opts := func() models.IssueSearchOptions { return models.IssueSearchOptions{ RepoDid: "did:plc:testrepo", IsOpen: boolPtr(true), Page: pagination.Page{Limit: 10}, } }
o := opts() o.LabelValues = []string{"priority:high"} result, err := ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(2), result.Total) assert.Contains(t, result.Hits, int64(1)) assert.Contains(t, result.Hits, int64(3))
o = opts() o.LabelValues = []string{"priority:low"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(2))
// Combined: plain label + label value o = opts() o.Labels = []string{"feature"} o.LabelValues = []string{"priority:high"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(3))
// Negated label value o = opts() o.NegatedLabelValues = []string{"priority:low"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(2), result.Total) assert.Contains(t, result.Hits, int64(1)) assert.Contains(t, result.Hits, int64(3))
// Label value + negated plain label o = opts() o.LabelValues = []string{"priority:high"} o.NegatedLabels = []string{"feature"} result, err = ix.Search(ctx, o) require.NoError(t, err) assert.Equal(t, uint64(1), result.Total) assert.Contains(t, result.Hits, int64(1))}