Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions internal/search/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package search

import (
"context"
"fmt"
"time"

glean "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
)

// datasourceField is the facet filter field that both API surfaces route to
// a dedicated datasource filter instead of the generic filter mechanism.
const datasourceField = "datasource"

// BuildPlatformSearchRequest converts Options into the platform API's
// components.PlatformSearchRequest. The platform surface is deliberately
// narrower than the classic SearchRequest: flags without a platform
// equivalent are simply not mapped (cmd/search reports those to the user via
// flags.Changed, which knows what was explicitly set).
func BuildPlatformSearchRequest(opts *Options) components.PlatformSearchRequest {
req := components.PlatformSearchRequest{
Query: opts.Query,
}

if opts.PageSize > 0 {
pageSize := int64(opts.PageSize)
req.PageSize = &pageSize
}
if opts.Cursor != "" {
cursor := opts.Cursor
req.Cursor = &cursor
}

if opts.RequestOptions != nil {
for _, ff := range opts.RequestOptions.FacetFilters {
// Mirror the classic builder: datasource filtering uses the
// dedicated field, not the generic filter mechanism.
if ff.FieldName == datasourceField {
for _, v := range ff.Values {
req.Datasources = append(req.Datasources, v.Value)
}
continue
}
filter := components.PlatformFilter{Field: ff.FieldName}
for _, v := range ff.Values {
filter.Values = append(filter.Values, v.Value)
}
req.Filters = append(req.Filters, filter)
}
}

return req
}

// RunPlatformSearch executes a search against the platform API
// (POST /api/search) and returns the raw platform response. The classic
// request carries timeoutMillis in the body; the platform API does not, so
// --timeout is honored via a context deadline instead.
func RunPlatformSearch(ctx context.Context, opts *Options, sdk *glean.Glean) (*components.PlatformSearchResponse, error) {
if opts.TimeoutMillis > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(opts.TimeoutMillis)*time.Millisecond)
defer cancel()
}

result, err := sdk.Search.Query(ctx, BuildPlatformSearchRequest(opts))
if err != nil {
return nil, fmt.Errorf("search request failed: %w", err)
}
return result.PlatformSearchResponse, nil
}
88 changes: 88 additions & 0 deletions internal/search/platform_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package search

import (
"context"
"encoding/json"
"io"
"net/http"
"testing"

glean "github.com/gleanwork/api-client-go"
"github.com/gleanwork/glean-cli/internal/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBuildPlatformSearchRequest_Basic(t *testing.T) {
opts := &Options{Query: "vacation policy", PageSize: 25}
req := BuildPlatformSearchRequest(opts)

assert.Equal(t, "vacation policy", req.Query)
require.NotNil(t, req.PageSize)
assert.Equal(t, int64(25), *req.PageSize)
assert.Nil(t, req.Cursor)
assert.Empty(t, req.Datasources)
assert.Empty(t, req.Filters)
}

func TestBuildPlatformSearchRequest_ZeroPageSizeOmitted(t *testing.T) {
req := BuildPlatformSearchRequest(&Options{Query: "q"})
assert.Nil(t, req.PageSize, "unset page size defers to the API default")
}

func TestBuildPlatformSearchRequest_Cursor(t *testing.T) {
req := BuildPlatformSearchRequest(&Options{Query: "q", Cursor: "next-page-token"})
require.NotNil(t, req.Cursor)
assert.Equal(t, "next-page-token", *req.Cursor)
}

func TestBuildPlatformSearchRequest_DatasourceUsesDedicatedField(t *testing.T) {
opts := &Options{Query: "q", RequestOptions: &RequestOptions{}}
AddFacetFilter(opts, "datasource", []string{"confluence", "slack"})
AddFacetFilter(opts, "type", []string{"document"})

req := BuildPlatformSearchRequest(opts)

assert.Equal(t, []string{"confluence", "slack"}, req.Datasources)
require.Len(t, req.Filters, 1)
assert.Equal(t, "type", req.Filters[0].Field)
assert.Equal(t, []string{"document"}, req.Filters[0].Values)
}

func TestRunPlatformSearch_HitsPlatformEndpoint(t *testing.T) {
body, err := json.Marshal(map[string]any{
"results": []map[string]any{{
"url": "https://example.com/doc",
"title": "Example Doc",
"datasource": "confluence",
"snippets": []string{"an excerpt"},
}},
"has_more": false,
"request_id": "req-123",
})
require.NoError(t, err)

mock := &testutils.MockTransport{Body: body, ContentType: "application/json"}
sdk := glean.New(
glean.WithServerURL("https://test-company-be.glean.com"),
glean.WithSecurity("test-token"),
glean.WithClient(mock),
)

resp, err := RunPlatformSearch(context.Background(), &Options{Query: "example", TimeoutMillis: 5000}, sdk)
require.NoError(t, err)

require.Len(t, mock.Requests, 1)
sent := mock.Requests[0]
assert.Equal(t, "/api/search", sent.URL.Path)
assert.Equal(t, http.MethodPost, sent.Method)

sentBody, err := io.ReadAll(sent.Body)
require.NoError(t, err)
assert.Contains(t, string(sentBody), `"query":"example"`)

require.NotNil(t, resp)
require.Len(t, resp.Results, 1)
assert.Equal(t, "Example Doc", resp.Results[0].Title)
assert.Equal(t, "req-123", resp.RequestID)
}
2 changes: 1 addition & 1 deletion internal/search/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func BuildSearchRequest(opts *Options) components.SearchRequest {
// The Glean API has a dedicated DatasourcesFilter field that must
// be used for datasource filtering — the generic FacetFilters
// mechanism does not filter by datasource correctly.
if ff.FieldName == "datasource" {
if ff.FieldName == datasourceField {
for _, v := range ff.Values {
sdkOpts.DatasourcesFilter = append(sdkOpts.DatasourcesFilter, v.Value)
}
Expand Down