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,283 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/application/repository"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type interveningWikiPageRepository struct {
|
||||
interfaces.WikiPageRepository
|
||||
beforeWrite func()
|
||||
}
|
||||
|
||||
func (r *interveningWikiPageRepository) UpdateWithRevision(ctx context.Context, page *types.WikiPage, revision *types.WikiPageRevision) error {
|
||||
r.beforeWrite()
|
||||
return r.WikiPageRepository.UpdateWithRevision(ctx, page, revision)
|
||||
}
|
||||
|
||||
func (r *interveningWikiPageRepository) UpdateMeta(ctx context.Context, page *types.WikiPage) error {
|
||||
r.beforeWrite()
|
||||
return r.WikiPageRepository.UpdateMeta(ctx, page)
|
||||
}
|
||||
|
||||
func (r *interveningWikiPageRepository) UpdateAutoLinkedContent(ctx context.Context, page *types.WikiPage) error {
|
||||
r.beforeWrite()
|
||||
return r.WikiPageRepository.UpdateAutoLinkedContent(ctx, page)
|
||||
}
|
||||
|
||||
func TestWikiGeneratedPageRejectsEditBeforeServiceRefetch(t *testing.T) {
|
||||
for _, metadataOnly := range []bool{false, true} {
|
||||
name := "content"
|
||||
if metadataOnly {
|
||||
name = "metadata"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, h, db := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
writeCtx := types.WithWikiPageWriteGuard(ctx, page)
|
||||
generated := *page
|
||||
generated.Content = "AI result based on old row"
|
||||
edit := *page
|
||||
if metadataOnly {
|
||||
edit.SortOrder = 7
|
||||
} else {
|
||||
edit.Content = "user revision"
|
||||
}
|
||||
latest, err := h.svc.UpdatePage(ctx, &edit)
|
||||
require.NoError(t, err)
|
||||
_, err = h.svc.UpdatePage(writeCtx, &generated)
|
||||
require.ErrorIs(t, err, repository.ErrWikiPageConflict)
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, latest.Content, stored.Content)
|
||||
require.Equal(t, latest.SortOrder, stored.SortOrder)
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.WikiPageRevision{}).Count(&count).Error)
|
||||
if metadataOnly {
|
||||
require.Zero(t, count)
|
||||
} else {
|
||||
require.EqualValues(t, 1, count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiGeneratedPageRejectsEditBetweenRefetchAndWrite(t *testing.T) {
|
||||
for _, contentChanged := range []bool{false, true} {
|
||||
name := "metadata_write"
|
||||
if contentChanged {
|
||||
name = "content_write"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, h, db := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
repo := &interveningWikiPageRepository{
|
||||
WikiPageRepository: repository.NewWikiPageRepository(db),
|
||||
beforeWrite: func() {
|
||||
// Metadata changes keep the content version. They must still
|
||||
// invalidate the row used by an in-flight AI replacement.
|
||||
require.NoError(t, db.Model(&types.WikiPage{}).Where("id = ?", page.ID).
|
||||
Updates(map[string]any{"sort_order": 9, "updated_at": page.UpdatedAt.Add(time.Second)}).Error)
|
||||
},
|
||||
}
|
||||
svc := NewWikiPageService(repo, nil, nil, nil, nil)
|
||||
generated := *page
|
||||
generated.SourceRefs = types.StringArray{"new-source"}
|
||||
if contentChanged {
|
||||
generated.Content = "AI replacement"
|
||||
}
|
||||
_, err = svc.UpdatePage(types.WithWikiPageWriteGuard(ctx, page), &generated)
|
||||
require.ErrorIs(t, err, repository.ErrWikiPageConflict)
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "original", stored.Content)
|
||||
require.Equal(t, 9, stored.SortOrder)
|
||||
require.Equal(t, 1, stored.Version)
|
||||
require.Empty(t, stored.SourceRefs)
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.WikiPageRevision{}).Count(&count).Error)
|
||||
require.Zero(t, count, "rejected writes must roll back their revision snapshot")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiAutoLinksRejectConcurrentEdits(t *testing.T) {
|
||||
for _, scenario := range []struct {
|
||||
name string
|
||||
beforeRefetch bool
|
||||
metadataOnly bool
|
||||
}{
|
||||
{"content_before_refetch", true, false},
|
||||
{"metadata_before_refetch", true, true},
|
||||
{"content_before_write", false, false},
|
||||
{"metadata_before_write", false, true},
|
||||
} {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
ctx, h, db := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
decorated := *page
|
||||
decorated.Content = "[[concept/other|original]]"
|
||||
var latest *types.WikiPage
|
||||
edit := func() {
|
||||
changed := *page
|
||||
changed.SourceRefs = types.StringArray{"new-source"}
|
||||
if !scenario.metadataOnly {
|
||||
changed.Content = "user revision"
|
||||
}
|
||||
latest, err = h.svc.UpdatePage(ctx, &changed)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
svc := h.svc
|
||||
if scenario.beforeRefetch {
|
||||
edit()
|
||||
} else {
|
||||
svc = NewWikiPageService(&interveningWikiPageRepository{
|
||||
WikiPageRepository: repository.NewWikiPageRepository(db), beforeWrite: edit,
|
||||
}, nil, nil, nil, nil)
|
||||
}
|
||||
require.ErrorIs(t, svc.UpdateAutoLinkedContent(ctx, &decorated), repository.ErrWikiPageConflict)
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, latest.Content, stored.Content)
|
||||
require.Equal(t, latest.SourceRefs, stored.SourceRefs)
|
||||
require.Equal(t, latest.Version, stored.Version)
|
||||
require.True(t, latest.UpdatedAt.Equal(stored.UpdatedAt))
|
||||
var revisions int64
|
||||
require.NoError(t, db.Model(&types.WikiPageRevision{}).Count(&revisions).Error)
|
||||
require.EqualValues(t, latest.Version-1, revisions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiMetadataRejectsStalePage(t *testing.T) {
|
||||
for _, archive := range []bool{false, true} {
|
||||
name := "new_source_refs"
|
||||
if archive {
|
||||
name = "archived"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, h, _ := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
changed := *page
|
||||
changed.SourceRefs = types.StringArray{"new-source"}
|
||||
changed.ChunkRefs = types.StringArray{"new-chunk"}
|
||||
if archive {
|
||||
changed.Status = types.WikiPageStatusArchived
|
||||
}
|
||||
latest, err := h.svc.UpdatePage(ctx, &changed)
|
||||
require.NoError(t, err)
|
||||
page.SortOrder = 7
|
||||
require.ErrorIs(t, h.svc.UpdatePageMeta(ctx, page), repository.ErrWikiPageConflict)
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, latest.Status, stored.Status)
|
||||
require.Equal(t, latest.SourceRefs, stored.SourceRefs)
|
||||
require.Equal(t, latest.ChunkRefs, stored.ChunkRefs)
|
||||
require.Equal(t, latest.SortOrder, stored.SortOrder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiLinkMaintenancePreservesConcurrentMetadata(t *testing.T) {
|
||||
for _, remove := range []bool{false, true} {
|
||||
name := "add"
|
||||
if remove {
|
||||
name = "remove"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, h, db := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original",
|
||||
InLinks: types.StringArray{"concept/source"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
repo := &interveningWikiPageRepository{
|
||||
WikiPageRepository: repository.NewWikiPageRepository(db),
|
||||
beforeWrite: func() {
|
||||
require.NoError(t, db.Model(&types.WikiPage{}).Where("id = ?", page.ID).
|
||||
Updates(map[string]any{
|
||||
"source_refs": types.StringArray{"new-source"}, "chunk_refs": types.StringArray{"new-chunk"},
|
||||
"status": types.WikiPageStatusArchived, "updated_at": page.UpdatedAt.Add(time.Second),
|
||||
}).Error)
|
||||
},
|
||||
}
|
||||
svc := NewWikiPageService(repo, nil, nil, nil, nil).(*wikiPageService)
|
||||
if remove {
|
||||
svc.removeInLinks(ctx, page.KnowledgeBaseID, "concept/source", types.StringArray{page.Slug})
|
||||
} else {
|
||||
svc.updateInLinks(ctx, page.KnowledgeBaseID, "concept/another", types.StringArray{page.Slug})
|
||||
}
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.WikiPageStatusArchived, stored.Status)
|
||||
require.Equal(t, types.StringArray{"new-source"}, stored.SourceRefs)
|
||||
require.Equal(t, types.StringArray{"new-chunk"}, stored.ChunkRefs)
|
||||
require.Equal(t, page.InLinks, stored.InLinks)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiPublishPreservesConcurrentArchive(t *testing.T) {
|
||||
ctx, h, db := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original", Status: types.WikiPageStatusDraft,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
repo := &interveningWikiPageRepository{
|
||||
WikiPageRepository: repository.NewWikiPageRepository(db),
|
||||
beforeWrite: func() {
|
||||
require.NoError(t, db.Model(&types.WikiPage{}).Where("id = ?", page.ID).
|
||||
Updates(map[string]any{"status": types.WikiPageStatusArchived, "updated_at": page.UpdatedAt.Add(time.Second)}).Error)
|
||||
},
|
||||
}
|
||||
svc := &wikiIngestService{wikiService: NewWikiPageService(repo, nil, nil, nil, nil)}
|
||||
failed := svc.publishDraftPages(ctx, page.KnowledgeBaseID, []string{page.Slug})
|
||||
require.ErrorIs(t, failed[page.Slug], repository.ErrWikiPageConflict)
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.WikiPageStatusArchived, stored.Status)
|
||||
}
|
||||
|
||||
func TestWikiAutoLinkWritesKeepSnapshotForCompensation(t *testing.T) {
|
||||
ctx, h, db := newWikiRevisionTestService(t)
|
||||
page, err := h.svc.CreatePage(ctx, &types.WikiPage{
|
||||
KnowledgeBaseID: "kb-guard", TenantID: 1, Slug: "concept/guard",
|
||||
Title: "Guard", PageType: types.WikiPageTypeConcept, Content: "original",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
page.Content = "decorated"
|
||||
require.NoError(t, h.svc.UpdateAutoLinkedContent(ctx, page))
|
||||
page.Content = "original"
|
||||
require.NoError(t, h.svc.UpdateAutoLinkedContent(ctx, page))
|
||||
stored, err := h.svc.GetPageBySlug(ctx, page.KnowledgeBaseID, page.Slug)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "original", stored.Content)
|
||||
require.Equal(t, 1, stored.Version)
|
||||
var revisions int64
|
||||
require.NoError(t, db.Model(&types.WikiPageRevision{}).Count(&revisions).Error)
|
||||
require.Zero(t, revisions)
|
||||
}
|
||||
Reference in New Issue
Block a user