kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
194 行
6.0 KiB
Go
194 行
6.0 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
"go.kenn.io/agentsview/internal/dbtest"
|
|
"go.kenn.io/agentsview/internal/service"
|
|
)
|
|
|
|
// seedSearchSession creates a non-one-shot session with a user message
|
|
// whose content is FTS-indexed, so Search can find it by a term.
|
|
func seedSearchSession(t *testing.T, d *db.DB, id, project, content string) {
|
|
t.Helper()
|
|
dbtest.SeedSession(t, d, id, project, func(s *db.Session) {
|
|
s.MessageCount = 3
|
|
s.UserMessageCount = 2
|
|
})
|
|
msgs := []db.Message{
|
|
dbtest.UserMsg(id, 0, content),
|
|
dbtest.AsstMsg(id, 1, "understood"),
|
|
}
|
|
require.NoError(t, d.InsertMessages(msgs))
|
|
}
|
|
|
|
func TestDirectBackend_Search_Roundtrip(t *testing.T) {
|
|
t.Parallel()
|
|
d := dbtest.OpenTestDB(t)
|
|
if !d.HasFTS() {
|
|
t.Skip("FTS not available in this build")
|
|
}
|
|
seedSearchSession(t, d, "s1", "proj-a", "the quick brown fox jumped")
|
|
seedSearchSession(t, d, "s2", "proj-b", "lazy dogs sleep")
|
|
be := service.NewDirectBackend(d, nil)
|
|
|
|
res, err := be.Search(context.Background(), service.SearchRequest{
|
|
Query: "fox",
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Len(t, res.Results, 1)
|
|
assert.Equal(t, "s1", res.Results[0].SessionID)
|
|
assert.Equal(t, "proj-a", res.Results[0].Project)
|
|
|
|
// Project filter restricts results.
|
|
none, err := be.Search(context.Background(), service.SearchRequest{
|
|
Query: "fox",
|
|
Project: "proj-b",
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, none.Results)
|
|
}
|
|
|
|
func TestDirectBackend_Search_EmptyQuery(t *testing.T) {
|
|
t.Parallel()
|
|
d := dbtest.OpenTestDB(t)
|
|
be := service.NewDirectBackend(d, nil)
|
|
|
|
_, err := be.Search(context.Background(), service.SearchRequest{Query: " "})
|
|
require.Error(t, err)
|
|
var inputErr *db.SearchInputError
|
|
assert.True(t, errors.As(err, &inputErr),
|
|
"empty query should be a SearchInputError, got %T", err)
|
|
}
|
|
|
|
// A query containing FTS operator characters (a hyphen, a colon) must be
|
|
// treated as literal terms rather than 500ing the store, because
|
|
// db.PrepareFTSQuery quotes each term.
|
|
func TestDirectBackend_Search_PunctuationIsLiteral(t *testing.T) {
|
|
t.Parallel()
|
|
d := dbtest.OpenTestDB(t)
|
|
if !d.HasFTS() {
|
|
t.Skip("FTS not available in this build")
|
|
}
|
|
seedSearchSession(t, d, "s1", "proj", "deploying agentsview-mcp today")
|
|
be := service.NewDirectBackend(d, nil)
|
|
|
|
res, err := be.Search(context.Background(), service.SearchRequest{
|
|
Query: "agentsview-mcp",
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Results, 1)
|
|
assert.Equal(t, "s1", res.Results[0].SessionID)
|
|
}
|
|
|
|
func TestHTTPBackend_Search_Roundtrip(t *testing.T) {
|
|
t.Parallel()
|
|
env := newHTTPBackendEnv(t)
|
|
d := env.DB
|
|
if !d.HasFTS() {
|
|
t.Skip("FTS not available in this build")
|
|
}
|
|
seedSearchSession(t, d, "s1", "proj-a", "the quick brown fox jumped")
|
|
svc := env.Backend("", false)
|
|
|
|
res, err := svc.Search(context.Background(), service.SearchRequest{
|
|
Query: "fox",
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Len(t, res.Results, 1)
|
|
assert.Equal(t, "s1", res.Results[0].SessionID)
|
|
}
|
|
|
|
// The HTTP backend must forward all search params to the daemon's
|
|
// /api/v1/search endpoint with the expected query-key names.
|
|
func TestHTTPBackend_Search_SendsParams(t *testing.T) {
|
|
t.Parallel()
|
|
var got url.Values
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
got = r.URL.Query()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"results":[],"next":0}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
svc := service.NewHTTPBackend(srv.URL, "", false)
|
|
|
|
_, err := svc.Search(context.Background(), service.SearchRequest{
|
|
Query: "needle", Project: "proj", Sort: "recency", Cursor: 7, Limit: 5,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "needle", got.Get("q"))
|
|
assert.Equal(t, "proj", got.Get("project"))
|
|
assert.Equal(t, "recency", got.Get("sort"))
|
|
assert.Equal(t, "7", got.Get("cursor"))
|
|
assert.Equal(t, "5", got.Get("limit"))
|
|
}
|
|
|
|
// A daemon without an FTS index responds 501; the HTTP backend maps that
|
|
// to the shared ErrSearchUnavailable sentinel.
|
|
func TestHTTPBackend_Search_Unavailable(t *testing.T) {
|
|
t.Parallel()
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNotImplemented)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
svc := service.NewHTTPBackend(srv.URL, "", true)
|
|
|
|
_, err := svc.Search(context.Background(), service.SearchRequest{Query: "fox"})
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, service.ErrSearchUnavailable),
|
|
"501 should map to ErrSearchUnavailable, got %v", err)
|
|
}
|
|
|
|
// The direct backend has no VectorSearcher wired into the test DB, so
|
|
// semantic content search must surface db.ErrSemanticUnavailable unwrapped
|
|
// (errors.Is must still see through it -- no extra wrapping in between).
|
|
func TestDirectBackend_SearchContent_SemanticUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
d := dbtest.OpenTestDB(t)
|
|
be := service.NewDirectBackend(d, nil)
|
|
|
|
_, err := be.SearchContent(context.Background(), service.ContentSearchRequest{
|
|
Pattern: "fox", Mode: "semantic",
|
|
})
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, service.ErrSemanticUnavailable),
|
|
"expected ErrSemanticUnavailable, got %v", err)
|
|
}
|
|
|
|
// A daemon without a VectorSearcher responds 501 to a semantic content
|
|
// search; the HTTP backend maps that to the shared ErrSemanticUnavailable
|
|
// sentinel, mirroring the ErrSearchUnavailable mapping above.
|
|
func TestHTTPBackend_SearchContent_SemanticUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNotImplemented)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
svc := service.NewHTTPBackend(srv.URL, "", true)
|
|
|
|
_, err := svc.SearchContent(context.Background(), service.ContentSearchRequest{
|
|
Pattern: "fox", Mode: "semantic",
|
|
})
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, service.ErrSemanticUnavailable),
|
|
"501 should map to ErrSemanticUnavailable, got %v", err)
|
|
}
|