Files
xinmi/internal/application/repository/retriever/opensearch/copy_bulk_test.go
T
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

194 lines
6.4 KiB
Go

package opensearch
import (
"context"
"io"
"net/http"
"strings"
"sync"
"testing"
)
// TestTransformSourceID covers the generated-question / regular / fallback
// branches that CopyIndices uses to remap source_id.
func TestTransformSourceID(t *testing.T) {
t.Run("regular chunk uses target chunk id", func(t *testing.T) {
if got := transformSourceID("chunk1", "chunk1", "tgt1"); got != "tgt1" {
t.Errorf("want tgt1, got %s", got)
}
})
t.Run("generated question preserves question id", func(t *testing.T) {
if got := transformSourceID("chunk1-q7", "chunk1", "tgt1"); got != "tgt1-q7" {
t.Errorf("want tgt1-q7, got %s", got)
}
})
t.Run("unrelated source id gets fresh uuid", func(t *testing.T) {
got := transformSourceID("totally-different", "chunk1", "tgt1")
if got == "totally-different" || got == "tgt1" || len(got) != 36 {
t.Errorf("want fresh uuid, got %q", got)
}
})
}
// TestCopyIndices_EmptyMapping_NoOp verifies an empty chunk map short-circuits
// before any HTTP call.
func TestCopyIndices_EmptyMapping_NoOp(t *testing.T) {
repo, ts := newTestRepo(t, func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected HTTP call: %s %s", r.Method, r.URL.Path)
})
defer ts.Close()
err := repo.CopyIndices(context.Background(), "kbSrc", map[string]string{}, map[string]string{}, "kbDst", 768, "manual")
if err != nil {
t.Fatalf("want nil, got %v", err)
}
}
// TestCopyIndices_ScanThenBatchSave verifies the search→BatchSave path:
// remaps IDs, keys the embedding by the *target source id* (OpenSearch
// BatchSave's lookup key), and emits one reindex audit event.
func TestCopyIndices_ScanThenBatchSave(t *testing.T) {
var (
mu sync.Mutex
bulkBody string
searchCnt int
)
handler := func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodHead:
w.WriteHeader(http.StatusOK) // alias exists → ensureReady no-op
case strings.Contains(r.URL.Path, "_search"):
mu.Lock()
searchCnt++
first := searchCnt == 1
mu.Unlock()
if first {
_, _ = w.Write([]byte(`{"hits":{"hits":[
{"_source":{"content":"c","source_id":"srcChunk","source_type":1,"chunk_id":"srcChunk","knowledge_id":"srcKnow","knowledge_base_id":"kbSrc","tag_id":"t","is_enabled":true,"is_recommended":false,"embedding":[0.1,0.2,0.3]}}
]}}`))
} else {
_, _ = w.Write([]byte(`{"hits":{"hits":[]}}`))
}
case strings.HasSuffix(r.URL.Path, "/_bulk"):
b, _ := io.ReadAll(r.Body)
mu.Lock()
bulkBody = string(b)
mu.Unlock()
_, _ = w.Write([]byte(`{"errors":false,"items":[]}`))
default:
_, _ = w.Write([]byte(`{}`))
}
}
repo, ts := newTestRepo(t, handler)
defer ts.Close()
spy := &spySink{}
repo.sink = spy
err := repo.CopyIndices(context.Background(), "kbSrc",
map[string]string{"srcKnow": "tgtKnow"}, // knowledge_id remap (sourceToTargetKBIDMap is keyed by knowledge_id, mirroring ES)
map[string]string{"srcChunk": "tgtChunk"},
"kbDst", 768, "manual")
if err != nil {
t.Fatalf("CopyIndices: %v", err)
}
mu.Lock()
defer mu.Unlock()
if bulkBody == "" {
t.Fatal("no bulk request captured")
}
// Target IDs present, source IDs gone from the written doc.
for _, want := range []string{`"chunk_id":"tgtChunk"`, `"knowledge_id":"tgtKnow"`, `"knowledge_base_id":"kbDst"`, `"source_id":"tgtChunk"`} {
if !strings.Contains(bulkBody, want) {
t.Errorf("bulk body missing %q\n%s", want, bulkBody)
}
}
if strings.Contains(bulkBody, `"knowledge_base_id":"kbSrc"`) {
t.Errorf("bulk body leaked source KB id\n%s", bulkBody)
}
// Embedding written (keyed by target source id internally).
if !strings.Contains(bulkBody, "0.1") {
t.Errorf("embedding not written\n%s", bulkBody)
}
if len(spy.reindex) != 1 || spy.reindex[0].docs != 1 {
t.Errorf("want 1 reindex event with docs=1, got %+v", spy.reindex)
}
}
// TestBatchUpdateChunkEnabledStatus_GroupedUpdateByQuery verifies the status
// map is grouped by value into one _update_by_query per distinct value, each
// passing chunk ids via terms + the new value via bound script params.
func TestBatchUpdateChunkEnabledStatus_GroupedUpdateByQuery(t *testing.T) {
var (
mu sync.Mutex
bodies []string
)
handler := func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "_update_by_query") {
b, _ := io.ReadAll(r.Body)
mu.Lock()
bodies = append(bodies, string(b))
mu.Unlock()
_, _ = w.Write([]byte(`{"updated":1,"version_conflicts":0,"failures":[]}`))
return
}
_, _ = w.Write([]byte(`{}`))
}
repo, ts := newTestRepo(t, handler)
defer ts.Close()
err := repo.BatchUpdateChunkEnabledStatus(context.Background(), map[string]bool{
"c1": true, "c2": false, "c3": true,
})
if err != nil {
t.Fatalf("BatchUpdateChunkEnabledStatus: %v", err)
}
mu.Lock()
defer mu.Unlock()
if len(bodies) != 2 {
t.Fatalf("want 2 grouped update_by_query calls (true/false), got %d: %v", len(bodies), bodies)
}
joined := strings.Join(bodies, "\n")
for _, want := range []string{"c1", "c2", "c3", "is_enabled", "params"} {
if !strings.Contains(joined, want) {
t.Errorf("update_by_query bodies missing %q\n%s", want, joined)
}
}
}
func TestBatchUpdateChunkEnabledStatus_Empty_NoOp(t *testing.T) {
repo, ts := newTestRepo(t, func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected HTTP call: %s %s", r.Method, r.URL.Path)
})
defer ts.Close()
if err := repo.BatchUpdateChunkEnabledStatus(context.Background(), map[string]bool{}); err != nil {
t.Fatalf("want nil, got %v", err)
}
}
// TestInspectByQueryResponse covers the success path and the failure path,
// asserting cluster-side reason text is NOT surfaced in the returned error.
func TestInspectByQueryResponse(t *testing.T) {
t.Run("clean response", func(t *testing.T) {
body := strings.NewReader(`{"updated":5,"version_conflicts":0,"failures":[]}`)
if err := inspectByQueryResponse(body); err != nil {
t.Fatalf("want nil, got %v", err)
}
})
t.Run("failures do not leak reason", func(t *testing.T) {
body := strings.NewReader(`{"updated":1,"version_conflicts":0,"failures":[
{"id":"c9","cause":{"type":"version_conflict_engine_exception","reason":"SECRET document body leaked here"}}
]}`)
err := inspectByQueryResponse(body)
if err == nil {
t.Fatal("want error for failures, got nil")
}
if strings.Contains(err.Error(), "SECRET") {
t.Errorf("error leaked cluster reason: %v", err)
}
if !strings.Contains(err.Error(), "version_conflict_engine_exception") {
t.Errorf("error should surface bounded type: %v", err)
}
})
}