WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
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
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
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fakeWikiPages serves the pages a knowledge base already owns, so identity
|
||||
// resolution can be exercised without a database.
|
||||
type fakeWikiPages struct {
|
||||
interfaces.WikiPageService
|
||||
bySlug map[string]*types.WikiPageLite
|
||||
}
|
||||
|
||||
func (f fakeWikiPages) ListBySlugs(ctx context.Context, kbID string, slugs []string) (map[string]*types.WikiPageLite, error) {
|
||||
out := make(map[string]*types.WikiPageLite, len(slugs))
|
||||
for _, slug := range slugs {
|
||||
if page := f.bySlug[slug]; page != nil {
|
||||
out[slug] = page
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f fakeWikiPages) FindPagesByNormalizedTitles(ctx context.Context, kbID, pageType string, identities []string) ([]*types.WikiPageLite, error) {
|
||||
wanted := make(map[string]bool, len(identities))
|
||||
for _, identity := range identities {
|
||||
wanted[identity] = true
|
||||
}
|
||||
var out []*types.WikiPageLite
|
||||
for _, page := range f.bySlug {
|
||||
if page.PageType == pageType && wanted[normalizeWikiIdentityTitle(page.Title)] {
|
||||
out = append(out, page)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func pageTitle(entityType, slug, title string) *types.WikiPageLite {
|
||||
return &types.WikiPageLite{Slug: slug, Title: title, PageType: entityType}
|
||||
}
|
||||
|
||||
// A model-written spelling variant must resolve onto the page that already owns the
|
||||
// slug instead of ending the document: window title "G 点" against page "G点".
|
||||
func TestWikiIdentityTargetsAdoptTheOwningPagesSpelling(t *testing.T) {
|
||||
service := &wikiIngestService{wikiService: fakeWikiPages{bySlug: map[string]*types.WikiPageLite{
|
||||
"entity/g-dian": pageTitle(types.WikiPageTypeEntity, "entity/g-dian", "G点"),
|
||||
}}}
|
||||
result := combinedExtraction{Entities: []extractedItem{{
|
||||
Name: "G 点", Slug: "entity/g-dian", Description: "d", Details: "x",
|
||||
}}}
|
||||
|
||||
out, dropped, err := service.resolveWikiIdentityTargets(context.Background(), "kb", result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, dropped, "a spelling-only difference is not a drop")
|
||||
require.Len(t, out.Entities, 1)
|
||||
require.Equal(t, "G点", out.Entities[0].Name, "the page keeps owning its canonical title")
|
||||
require.Equal(t, "entity/g-dian", out.Entities[0].Slug)
|
||||
}
|
||||
|
||||
// A different identity claiming an owned slug is dropped and counted, and the rest
|
||||
// of the document continues instead of failing.
|
||||
func TestWikiIdentityTargetsDropTheIntruderAndKeepTheRest(t *testing.T) {
|
||||
service := &wikiIngestService{wikiService: fakeWikiPages{bySlug: map[string]*types.WikiPageLite{
|
||||
"entity/du-lei-si": pageTitle(types.WikiPageTypeEntity, "entity/du-lei-si", "杜蕾斯"),
|
||||
}}}
|
||||
result := combinedExtraction{
|
||||
Entities: []extractedItem{
|
||||
{Name: "Durex", Slug: "entity/du-lei-si", Description: "d", Details: "x"},
|
||||
{Name: "跳蛋", Slug: "entity/tiao-dan", Description: "d", Details: "x"},
|
||||
},
|
||||
}
|
||||
|
||||
out, dropped, err := service.resolveWikiIdentityTargets(context.Background(), "kb", result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, dropped)
|
||||
require.Len(t, out.Entities, 1)
|
||||
require.Equal(t, "跳蛋", out.Entities[0].Name)
|
||||
require.Equal(t, 1, out.CandidatePruning.SlugConflicts, "the drop is counted for audit")
|
||||
}
|
||||
|
||||
// A slug owned by another page type cannot be taken over silently.
|
||||
func TestWikiIdentityTargetsRejectACrossTypeSlug(t *testing.T) {
|
||||
service := &wikiIngestService{wikiService: fakeWikiPages{bySlug: map[string]*types.WikiPageLite{
|
||||
"entity/sm": pageTitle(types.WikiPageTypeConcept, "entity/sm", "SM"),
|
||||
}}}
|
||||
result := combinedExtraction{
|
||||
Entities: []extractedItem{
|
||||
{Name: "SM", Slug: "entity/sm", Description: "d", Details: "x"},
|
||||
{Name: "跳蛋", Slug: "entity/tiao-dan", Description: "d", Details: "x"},
|
||||
},
|
||||
}
|
||||
|
||||
out, dropped, err := service.resolveWikiIdentityTargets(context.Background(), "kb", result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, dropped)
|
||||
require.Len(t, out.Entities, 1)
|
||||
require.Equal(t, "跳蛋", out.Entities[0].Name)
|
||||
}
|
||||
|
||||
// A set that loses every candidate still fails closed rather than publishing nothing
|
||||
// under the pretence of a successful document.
|
||||
func TestWikiIdentityTargetsFailClosedWhenEverythingCollides(t *testing.T) {
|
||||
service := &wikiIngestService{wikiService: fakeWikiPages{bySlug: map[string]*types.WikiPageLite{
|
||||
"entity/sm": pageTitle(types.WikiPageTypeConcept, "entity/sm", "SM"),
|
||||
}}}
|
||||
result := combinedExtraction{Entities: []extractedItem{
|
||||
{Name: "SM", Slug: "entity/sm", Description: "d", Details: "x"},
|
||||
}}
|
||||
|
||||
_, _, err := service.resolveWikiIdentityTargets(context.Background(), "kb", result)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "wiki_identity_slug_conflict")
|
||||
}
|
||||
|
||||
// Without a wiki repository the resolution is a pass-through, so Lite mode keeps
|
||||
// working and reports no drops.
|
||||
func TestWikiIdentityTargetsPassThroughWithoutRepository(t *testing.T) {
|
||||
service := &wikiIngestService{}
|
||||
result := combinedExtraction{Entities: []extractedItem{
|
||||
{Name: "跳蛋", Slug: "entity/tiao-dan", Description: "d", Details: "x"},
|
||||
}}
|
||||
|
||||
out, dropped, err := service.resolveWikiIdentityTargets(context.Background(), "kb", result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, dropped)
|
||||
require.Len(t, out.Entities, 1)
|
||||
}
|
||||
Reference in New Issue
Block a user