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,152 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/knowledgeversion"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type versionCleanupFixture struct {
|
||||
*versionCopyFixture
|
||||
targets []string
|
||||
fail bool
|
||||
after func(string)
|
||||
}
|
||||
|
||||
func (f *versionCleanupFixture) CleanupKnowledgeVersion(ctx context.Context, target string) error {
|
||||
if !knowledgeversion.IsWorker(ctx) || types.MustTenantIDFromContext(ctx) != 1 {
|
||||
return fmt.Errorf("wrong cleanup workspace")
|
||||
}
|
||||
f.targets = append(f.targets, target)
|
||||
if f.fail {
|
||||
return fmt.Errorf("object store unavailable")
|
||||
}
|
||||
if f.after != nil {
|
||||
f.after(target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newVersionCleanupFixture(t *testing.T) (*Service, *versionCleanupFixture) {
|
||||
t.Helper()
|
||||
s, f, _ := knowledgeVersionFixture(t)
|
||||
require.NoError(t, s.db.AutoMigrate(&types.Tenant{}))
|
||||
require.NoError(t, s.db.Create(&types.Tenant{ID: 1, Name: "cleanup", StorageUsed: 100}).Error)
|
||||
c := &versionCleanupFixture{versionCopyFixture: f}
|
||||
s.SetKnowledgeVersionAdapter(c)
|
||||
return s, c
|
||||
}
|
||||
|
||||
func addCleanupStage(t *testing.T, s *Service, status string, age time.Duration, temporary bool, state string) KnowledgeVersionJob {
|
||||
t.Helper()
|
||||
id := uuid.NewString()
|
||||
require.NoError(t, s.db.Create(&types.KnowledgeBase{ID: id, TenantID: 1, CreatorID: "alice", Type: "document", IsTemporary: temporary, VersionState: state}).Error)
|
||||
require.NoError(t, s.db.Create(&types.Knowledge{ID: uuid.NewString(), TenantID: 1, KnowledgeBaseID: id, StorageSize: 25}).Error)
|
||||
job := KnowledgeVersionJob{ID: uuid.NewString(), TenantID: 1, OwnerID: "alice", TargetID: id, Status: status, Attempts: 1, AbandonedIDs: types.StringArray{}, UpdatedAt: time.Now().Add(-age)}
|
||||
require.NoError(t, s.db.Create(&job).Error)
|
||||
return job
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionCleanupDelayAndAccounting(t *testing.T) {
|
||||
s, f := newVersionCleanupFixture(t)
|
||||
recent := addCleanupStage(t, s, "failed", time.Hour, true, "copying")
|
||||
running := addCleanupStage(t, s, "running", 4*time.Hour, true, "copying")
|
||||
snapshot := addCleanupStage(t, s, "failed", 4*time.Hour, true, "snapshot")
|
||||
published := addCleanupStage(t, s, "failed", 4*time.Hour, false, "")
|
||||
old := addCleanupStage(t, s, "failed", 4*time.Hour, true, "copying")
|
||||
require.NoError(t, s.CleanupAbandonedKnowledgeVersions(context.Background()))
|
||||
require.Equal(t, []string{old.TargetID}, f.targets)
|
||||
var count int64
|
||||
require.NoError(t, s.db.Unscoped().Model(&types.Knowledge{}).Where("knowledge_base_id=?", old.TargetID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
var tenant types.Tenant
|
||||
require.NoError(t, s.db.First(&tenant, 1).Error)
|
||||
require.EqualValues(t, 75, tenant.StorageUsed)
|
||||
require.NoError(t, s.CleanupAbandonedKnowledgeVersions(context.Background()))
|
||||
require.NoError(t, s.db.First(&tenant, 1).Error)
|
||||
require.EqualValues(t, 75, tenant.StorageUsed)
|
||||
for _, job := range []KnowledgeVersionJob{recent, running, snapshot, published} {
|
||||
require.NoError(t, s.db.Model(&types.KnowledgeBase{}).Where("id=?", job.TargetID).Count(&count).Error)
|
||||
require.EqualValues(t, 1, count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionCleanupRetriesAndRetainsAccountingUntilSuccess(t *testing.T) {
|
||||
s, f := newVersionCleanupFixture(t)
|
||||
job := addCleanupStage(t, s, "failed", 4*time.Hour, true, "copying")
|
||||
f.fail = true
|
||||
require.ErrorContains(t, s.CleanupAbandonedKnowledgeVersions(context.Background()), "object store unavailable")
|
||||
var tenant types.Tenant
|
||||
require.NoError(t, s.db.First(&tenant, 1).Error)
|
||||
require.EqualValues(t, 100, tenant.StorageUsed)
|
||||
f.fail = false
|
||||
require.NoError(t, s.db.Model(&job).UpdateColumn("updated_at", time.Now().Add(-4*time.Hour)).Error)
|
||||
require.NoError(t, s.CleanupAbandonedKnowledgeVersions(context.Background()))
|
||||
require.NoError(t, s.db.First(&tenant, 1).Error)
|
||||
require.EqualValues(t, 75, tenant.StorageUsed)
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionCleanupPreservesRetryTarget(t *testing.T) {
|
||||
s, f := newVersionCleanupFixture(t)
|
||||
job := addCleanupStage(t, s, "failed", 4*time.Hour, true, "copying")
|
||||
newTarget := uuid.NewString()
|
||||
f.after = func(target string) {
|
||||
require.NoError(t, s.db.Create(&types.KnowledgeBase{ID: newTarget, TenantID: 1, IsTemporary: true, VersionState: "copying"}).Error)
|
||||
require.NoError(t, s.db.Model(&job).Updates(map[string]any{"status": "running", "target_id": newTarget, "abandoned_ids": types.StringArray{target}}).Error)
|
||||
}
|
||||
require.NoError(t, s.CleanupAbandonedKnowledgeVersions(context.Background()))
|
||||
require.NoError(t, s.db.First(&job, "id=?", job.ID).Error)
|
||||
require.Equal(t, newTarget, job.TargetID)
|
||||
require.Empty(t, job.AbandonedIDs)
|
||||
var count int64
|
||||
require.NoError(t, s.db.Model(&types.KnowledgeBase{}).Where("id=?", newTarget).Count(&count).Error)
|
||||
require.EqualValues(t, 1, count)
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionCleanupRejectsStorageMismatchAtomically(t *testing.T) {
|
||||
s, _ := newVersionCleanupFixture(t)
|
||||
job := addCleanupStage(t, s, "failed", 4*time.Hour, true, "copying")
|
||||
require.NoError(t, s.db.Model(&types.Tenant{}).Where("id=1").UpdateColumn("storage_used", 0).Error)
|
||||
require.ErrorContains(t, s.CleanupAbandonedKnowledgeVersions(context.Background()), "accounting mismatch")
|
||||
var count int64
|
||||
require.NoError(t, s.db.Model(&types.Knowledge{}).Where("knowledge_base_id=?", job.TargetID).Count(&count).Error)
|
||||
require.EqualValues(t, 1, count)
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionIdleQueueIsExpected(t *testing.T) {
|
||||
s, _, _ := knowledgeVersionFixture(t)
|
||||
require.True(t, errors.Is(s.RunKnowledgeVersionJob(context.Background()), gorm.ErrRecordNotFound))
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionGraphFingerprintAndScopedCleanup(t *testing.T) {
|
||||
s, _ := newVersionCleanupFixture(t)
|
||||
require.NoError(t, s.db.AutoMigrate(&knowledgeversion.GraphRecord{}, &knowledgeversion.GraphSource{}))
|
||||
job := addCleanupStage(t, s, "failed", 4*time.Hour, true, "copying")
|
||||
for _, kb := range []string{job.TargetID, "published-keep"} {
|
||||
require.NoError(t, s.db.Create(&knowledgeversion.GraphRecord{TenantID: 1, KnowledgeBaseID: kb, SourceTable: "knowledgeentity", LegacyID: 7, Payload: types.JSON(`{"id":7}`)}).Error)
|
||||
require.NoError(t, s.db.Create(&knowledgeversion.GraphSource{TenantID: 1, KnowledgeBaseID: kb, SourceChunkID: "old", Excerpt: "preserve", MappingState: "unmapped"}).Error)
|
||||
}
|
||||
before, err := knowledgeversion.GraphFingerprint(s.db, 1, "published-keep")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, before)
|
||||
require.NoError(t, s.db.Model(&knowledgeversion.GraphSource{}).Where("knowledge_base_id=?", "published-keep").Update("excerpt", "changed provenance").Error)
|
||||
after, err := knowledgeversion.GraphFingerprint(s.db, 1, "published-keep")
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, before, after)
|
||||
require.NoError(t, s.CleanupAbandonedKnowledgeVersions(context.Background()))
|
||||
for _, model := range []any{&knowledgeversion.GraphRecord{}, &knowledgeversion.GraphSource{}} {
|
||||
var count int64
|
||||
require.NoError(t, s.db.Model(model).Where("knowledge_base_id=?", job.TargetID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
require.NoError(t, s.db.Model(model).Where("knowledge_base_id=?", "published-keep").Count(&count).Error)
|
||||
require.EqualValues(t, 1, count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user