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
125 lines
3.7 KiB
Go
125 lines
3.7 KiB
Go
//go:build pgtest
|
|
|
|
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
)
|
|
|
|
// classifySearchErr buckets a SearchContent failure into the two error
|
|
// classes the HTTP layer maps to distinct status codes: "input" is a
|
|
// *db.SearchInputError (400) and "unavailable" is db.ErrSemanticUnavailable
|
|
// (501). An input error must never also satisfy errors.Is ErrSemanticUnavailable,
|
|
// otherwise a 400 could be reported as a 501.
|
|
func classifySearchErr(t *testing.T, err error) string {
|
|
t.Helper()
|
|
require.Error(t, err)
|
|
var inputErr *db.SearchInputError
|
|
if errors.As(err, &inputErr) {
|
|
assert.False(t, errors.Is(err, db.ErrSemanticUnavailable),
|
|
"input error must not also be ErrSemanticUnavailable: %v", err)
|
|
return "input"
|
|
}
|
|
if errors.Is(err, db.ErrSemanticUnavailable) {
|
|
return "unavailable"
|
|
}
|
|
require.Failf(t, "unexpected error class", "%T: %v", err, err)
|
|
return ""
|
|
}
|
|
|
|
// TestSearchContentSemanticErrorParity pins that db.DB (SQLite) and
|
|
// postgres.Store classify the same semantic/hybrid requests into the same
|
|
// error class. Neither store has a VectorSearcher wired, so invalid inputs
|
|
// must be rejected as *db.SearchInputError (400) before the capability gate,
|
|
// while an otherwise-valid semantic/hybrid request falls through to
|
|
// db.ErrSemanticUnavailable (501). Running one input table through both
|
|
// backends is the cross-backend check the per-backend suites don't make.
|
|
func TestSearchContentSemanticErrorParity(t *testing.T) {
|
|
sqlite, err := db.Open(filepath.Join(t.TempDir(), "parity.db"))
|
|
require.NoError(t, err, "open sqlite")
|
|
t.Cleanup(func() { sqlite.Close() })
|
|
|
|
pg := setupContentSearch(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
filter db.ContentSearchFilter
|
|
want string
|
|
}{
|
|
{
|
|
name: "semantic cursor rejected",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "semantic", Cursor: 1},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "hybrid cursor rejected",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "hybrid", Cursor: 1},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "semantic non-messages source rejected",
|
|
filter: db.ContentSearchFilter{
|
|
Pattern: "x", Mode: "semantic", Sources: []string{"tool_input"},
|
|
},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "hybrid non-messages source rejected",
|
|
filter: db.ContentSearchFilter{
|
|
Pattern: "x", Mode: "hybrid", Sources: []string{"tool_result"},
|
|
},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "semantic bad scope rejected",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "semantic", Scope: "bogus"},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "hybrid bad scope rejected",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "hybrid", Scope: "bogus"},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "unknown mode rejected",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "bogus"},
|
|
want: "input",
|
|
},
|
|
{
|
|
name: "valid semantic hits capability gate",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "semantic"},
|
|
want: "unavailable",
|
|
},
|
|
{
|
|
name: "valid hybrid hits capability gate",
|
|
filter: db.ContentSearchFilter{Pattern: "x", Mode: "hybrid"},
|
|
want: "unavailable",
|
|
},
|
|
{
|
|
name: "valid semantic messages-only hits capability gate",
|
|
filter: db.ContentSearchFilter{
|
|
Pattern: "x", Mode: "semantic", Sources: []string{"messages"},
|
|
},
|
|
want: "unavailable",
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, sqliteErr := sqlite.SearchContent(ctx, tc.filter)
|
|
_, pgErr := pg.SearchContent(ctx, tc.filter)
|
|
assert.Equal(t, tc.want, classifySearchErr(t, sqliteErr), "sqlite class")
|
|
assert.Equal(t, tc.want, classifySearchErr(t, pgErr), "postgres class")
|
|
})
|
|
}
|
|
}
|