Files
xinmi/internal/application/service/wiki_routing_metadata_test.go
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

144 lines
6.1 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/Tencent/WeKnora/internal/models/chat"
"github.com/Tencent/WeKnora/internal/models/embedding"
"github.com/Tencent/WeKnora/internal/types"
"github.com/stretchr/testify/require"
)
func routingMetadataFixture() combinedExtraction {
return combinedExtraction{
Entities: []extractedItem{{Name: "Alpha", Slug: "entity/alpha", Aliases: []string{"A"},
Description: "UNSUPPORTED_DESCRIPTION_ALPHA", Details: "UNSUPPORTED_DETAILS_ALPHA", SourceChunks: []string{"source-a"}}},
Concepts: []extractedItem{{Name: "Beta", Slug: "concept/beta", Aliases: []string{"B"},
Description: "UNSUPPORTED_DESCRIPTION_BETA", Details: "UNSUPPORTED_DETAILS_BETA", SourceChunks: []string{"source-b"}}},
}
}
func requireNoGeneratedRoutingProse(t *testing.T, text string) {
t.Helper()
require.NotContains(t, text, "UNSUPPORTED_DESCRIPTION_")
require.NotContains(t, text, "UNSUPPORTED_DETAILS_")
}
func TestWikiRoutingMetadataCitationPreservesIdentitiesAndOriginalEvidence(t *testing.T) {
ctx := context.Background()
fixture := routingMetadataFixture()
original, err := json.Marshal(fixture)
require.NoError(t, err)
catalog := renderCandidateSlugsXML(fixture.Entities, fixture.Concepts)
for _, item := range append(append([]extractedItem(nil), fixture.Entities...), fixture.Concepts...) {
require.Contains(t, catalog, item.Slug)
require.Contains(t, catalog, fmt.Sprintf("name: %q", item.Name))
require.Contains(t, catalog, fmt.Sprintf("aliases=%q", strings.Join(item.Aliases, ", ")))
}
requireNoGeneratedRoutingProse(t, catalog)
chunks := []*types.Chunk{
{ID: "source-a", Content: "Alpha: first source reports 65/35; condition X is mandatory.\n", ChunkIndex: 0},
{ID: "source-b", Content: "Beta: another source reports 65/30; no correction of OCR value ?.\n", ChunkIndex: 1},
}
batches := splitChunksIntoCitationBatches(chunks)
require.Len(t, batches, 1)
calls := 0
model := &wikiScriptedChat{id: t.Name(), call: func(_ context.Context, messages []chat.Message) (*types.ChatResponse, error) {
calls++
var prompt strings.Builder
for _, message := range messages {
prompt.WriteString(message.Content)
}
requireNoGeneratedRoutingProse(t, prompt.String())
for _, chunk := range chunks {
require.Contains(t, prompt.String(), chunk.Content, "source text is not replaced by generated metadata")
}
return &types.ChatResponse{Content: `{"citations":{"entity/alpha":["c000"],"concept/beta":["c001"]}}`}, nil
}}
svc := &wikiIngestService{checkpointDB: openWikiCheckpointDB(t, filepath.Join(t.TempDir(), "routing.sqlite"))}
result, err := svc.classifyCitationBatch(ctx, model, catalog, batches[0], "English", false)
require.NoError(t, err)
require.Equal(t, 1, calls)
require.Equal(t, []string{"source-a"}, result.Citations["entity/alpha"])
require.Equal(t, []string{"source-b"}, result.Citations["concept/beta"])
unchanged, err := json.Marshal(fixture)
require.NoError(t, err)
require.JSONEq(t, string(original), string(unchanged))
scope := &wikiCheckpointScope{7, "kb", "doc", "routing-audit"}
require.NoError(t, svc.saveWikiCheckpoint(ctx, scope, "candidates", "complete", fixture))
var restored combinedExtraction
hit, err := svc.loadWikiCheckpoint(ctx, scope, "candidates", "complete", &restored)
require.NoError(t, err)
require.True(t, hit)
require.Equal(t, fixture, restored, "audit descriptions, aliases and all source references remain persisted")
}
func TestWikiRoutingMetadataTaxonomyReceivesOnlyIdentityInputs(t *testing.T) {
fixture := routingMetadataFixture()
updates := map[string][]SlugUpdate{
"entity/alpha": {{Type: types.WikiPageTypeEntity, Item: fixture.Entities[0]}},
"concept/beta": {{Type: types.WikiPageTypeConcept, Item: fixture.Concepts[0]}},
}
original, err := json.Marshal(updates)
require.NoError(t, err)
calls := 0
model := &wikiScriptedChat{id: t.Name(), call: func(_ context.Context, messages []chat.Message) (*types.ChatResponse, error) {
calls++
for _, message := range messages {
requireNoGeneratedRoutingProse(t, message.Content)
}
require.Contains(t, messages[0].Content, "entity/alpha | title: Alpha | type: entity")
require.Contains(t, messages[0].Content, "concept/beta | title: Beta | type: concept")
return &types.ChatResponse{Content: `{"assignments":[{"slug":"entity/alpha","path":["People"]},{"slug":"concept/beta","path":["Topics"]}]}`}, nil
}}
svc := &wikiIngestService{}
got, err := svc.planBatchTaxonomyChecked(context.Background(), model, &types.KnowledgeBase{ID: "kb"}, updates, "English")
require.NoError(t, err)
require.Equal(t, 1, calls)
require.Equal(t, []string{"People"}, got["entity/alpha"])
require.Equal(t, []string{"Topics"}, got["concept/beta"])
unchanged, err := json.Marshal(updates)
require.NoError(t, err)
require.JSONEq(t, string(original), string(unchanged))
}
type routingMetadataEmbedder struct {
embedding.Embedder
inputs [][]string
}
func (e *routingMetadataEmbedder) BatchEmbed(_ context.Context, inputs []string) ([][]float32, error) {
e.inputs = append(e.inputs, append([]string(nil), inputs...))
result := make([][]float32, len(inputs))
for i := range result {
result[i] = []float32{1, 0}
}
return result, nil
}
func TestWikiRoutingMetadataTaxonomyEmbeddingExcludesGeneratedProse(t *testing.T) {
fixture := routingMetadataFixture()
items := collectTaxonomyItems(map[string][]SlugUpdate{
"entity/alpha": {{Type: types.WikiPageTypeEntity, Item: fixture.Entities[0]}},
"concept/beta": {{Type: types.WikiPageTypeConcept, Item: fixture.Concepts[0]}},
})
pool := make([][]string, wikiTaxonomyFeedAllMaxFolders+1)
for i := range pool {
pool[i] = []string{"Existing", fmt.Sprintf("Folder %d", i)}
}
embedder := &routingMetadataEmbedder{}
svc := &wikiIngestService{modelService: parentChildModelService{embedder: embedder}}
_, err := svc.selectRelevantFoldersChecked(context.Background(), &types.KnowledgeBase{EmbeddingModelID: "embedding"}, items, pool)
require.NoError(t, err)
require.Len(t, embedder.inputs, 2)
require.Equal(t, []string{"Beta", "Alpha"}, embedder.inputs[1])
for _, batch := range embedder.inputs {
requireNoGeneratedRoutingProse(t, strings.Join(batch, "\n"))
}
}