Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
154 lines
4.4 KiB
Go
154 lines
4.4 KiB
Go
package opensearch
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
)
|
|
|
|
// spySink records audit events for assertions.
|
|
type spySink struct {
|
|
mu sync.Mutex
|
|
indexCreated []indexCreatedEvent
|
|
reindex []reindexEvent
|
|
}
|
|
|
|
type indexCreatedEvent struct {
|
|
alias string
|
|
dim int
|
|
}
|
|
type reindexEvent struct {
|
|
src, dst string
|
|
docs int64
|
|
}
|
|
|
|
func (s *spySink) EmitIndexCreated(_ context.Context, alias string, dim int) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.indexCreated = append(s.indexCreated, indexCreatedEvent{alias, dim})
|
|
}
|
|
|
|
func (s *spySink) EmitReindexExecuted(_ context.Context, src, dst string, docs int64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.reindex = append(s.reindex, reindexEvent{src, dst, docs})
|
|
}
|
|
|
|
// TestBuildInternalCfg_ReadsHNSWFields verifies the HNSW IndexConfig fields
|
|
// flow into internalCfg, falling back to defaults when unset.
|
|
func TestBuildInternalCfg_ReadsHNSWFields(t *testing.T) {
|
|
t.Run("reads set fields", func(t *testing.T) {
|
|
cfg, err := buildInternalCfg(&types.IndexConfig{
|
|
HNSWM: 24,
|
|
HNSWEFConstruction: 200,
|
|
HNSWEFSearch: 128,
|
|
KNNEngine: "faiss",
|
|
NumberOfShards: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.hnswM != 24 || cfg.hnswEFConstruction != 200 || cfg.efSearch != 128 || cfg.knnEngine != "faiss" {
|
|
t.Errorf("HNSW not wired: %+v", cfg)
|
|
}
|
|
if cfg.shards != 2 {
|
|
t.Errorf("shards: want 2, got %d", cfg.shards)
|
|
}
|
|
})
|
|
|
|
t.Run("defaults when unset", func(t *testing.T) {
|
|
cfg, err := buildInternalCfg(&types.IndexConfig{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.hnswM != 16 || cfg.hnswEFConstruction != 100 || cfg.efSearch != 100 || cfg.knnEngine != "lucene" {
|
|
t.Errorf("defaults not applied: %+v", cfg)
|
|
}
|
|
})
|
|
|
|
t.Run("nil config is all defaults", func(t *testing.T) {
|
|
cfg, err := buildInternalCfg(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.knnEngine != "lucene" || cfg.hnswM != 16 {
|
|
t.Errorf("nil defaults wrong: %+v", cfg)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestBuildIndexMapping_ReflectsHNSWConfig verifies the end-to-end path
|
|
// IndexConfig → buildInternalCfg → buildIndexMapping carries the operator's
|
|
// HNSW values into the cluster mapping JSON (regression guard for the wire-
|
|
// through, since defaults coincide with common values).
|
|
func TestBuildIndexMapping_ReflectsHNSWConfig(t *testing.T) {
|
|
cfg, err := buildInternalCfg(&types.IndexConfig{
|
|
HNSWM: 24,
|
|
HNSWEFConstruction: 200,
|
|
HNSWEFSearch: 128,
|
|
KNNEngine: "faiss",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, err := buildIndexMapping(cfg, 768)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(body)
|
|
for _, want := range []string{`"m":24`, `"ef_construction":200`, `"engine":"faiss"`, `"knn.algo_param.ef_search":128`} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("mapping JSON missing %q\n%s", want, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWithAuditSink_SetsAndNilSafe verifies the functional option.
|
|
func TestWithAuditSink_SetsAndNilSafe(t *testing.T) {
|
|
spy := &spySink{}
|
|
r := &Repository{}
|
|
WithAuditSink(spy)(r)
|
|
if r.sink != spy {
|
|
t.Fatal("WithAuditSink did not set the sink")
|
|
}
|
|
// nil must not clobber an already-set sink
|
|
WithAuditSink(nil)(r)
|
|
if r.sink != spy {
|
|
t.Fatal("WithAuditSink(nil) clobbered the sink")
|
|
}
|
|
}
|
|
|
|
// TestAuditSink_NopByDefault verifies a Repository with no sink does not panic
|
|
// when the audit accessor is used (nopSink fallback).
|
|
func TestAuditSink_NopByDefault(t *testing.T) {
|
|
var _ AuditSink = nopSink{} // compile-time assertion
|
|
r := &Repository{} // sink left nil
|
|
// Must not panic.
|
|
r.auditSink().EmitIndexCreated(context.Background(), "weknora_768", 768)
|
|
r.auditSink().EmitReindexExecuted(context.Background(), "a", "b", 3)
|
|
}
|
|
|
|
// TestAuditSink_EmitIndexCreated_OnEnsureReady verifies createIndexAndAlias
|
|
// emits exactly one index-created event with the per-dim alias when a new
|
|
// index is provisioned.
|
|
func TestAuditSink_EmitIndexCreated_OnEnsureReady(t *testing.T) {
|
|
repo, ts := newTestRepo(t, (&indexLifecycleHandler{}).ServeHTTP)
|
|
defer ts.Close()
|
|
spy := &spySink{}
|
|
repo.sink = spy
|
|
|
|
if err := repo.ensureReady(context.Background(), 768); err != nil {
|
|
t.Fatalf("ensureReady: %v", err)
|
|
}
|
|
if len(spy.indexCreated) != 1 {
|
|
t.Fatalf("want 1 index_created event, got %d", len(spy.indexCreated))
|
|
}
|
|
got := spy.indexCreated[0]
|
|
if got.alias != "weknora_test_768" || got.dim != 768 {
|
|
t.Errorf("event mismatch: %+v", got)
|
|
}
|
|
}
|