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) }