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

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
@@ -0,0 +1,227 @@
package memory
import (
"context"
"fmt"
"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"
)
func TestRecoveryMaintenanceRetainsPolicyAndLeaseChecks(t *testing.T) {
for _, maintenance := range []string{"topics", "consolidation"} {
for _, phase := range []string{"before", "after"} {
t.Run(maintenance+"/"+phase, func(t *testing.T) {
svc, db, ctx, messages, models, queue := recoveryHarness(t)
scope, err := ResolveScope(ctx)
require.NoError(t, err)
svc.ScheduleExtraction(ctx, "s", "m", "model")
if maintenance == "topics" {
_, err := svc.repo.BumpTopic(ctx, scope, "database tuning", types.NormalizeTopicKey("database tuning"), "database tuning")
require.NoError(t, err)
models.response = `{"memories":[],"topics":["deployment automation"]}`
models.responseFor = map[string]string{topicAdjudicationPrompt: `{"resolutions":[{"index":0,"same_as":0}]}`}
} else {
for i := range 6 {
require.NoError(t, svc.repo.CreateItem(ctx, &types.MemoryItem{TenantID: 1, SubjectID: scope.SubjectID,
Kind: types.MemoryKindFact, Topic: "deployment automation", Content: fmt.Sprintf("deployment automation uses deterministic verification %d", i)}))
}
models.responseFor = map[string]string{consolidationSystemPrompt: `{"statement":"deployment automation uses deterministic verification"}`}
}
var beforeItems int64
require.NoError(t, db.Model(&types.MemoryItem{}).Count(&beforeItems).Error)
before, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
mutate := func() { messages.owners = map[string]interfaces.MemoryScope{} }
models.onChat = func() {
if phase == "before" {
models.onGetChatModel = mutate
} else {
models.onChat = mutate
}
}
require.NoError(t, svc.Handle(context.Background(), queue.pop()))
calls := 1
if phase == "after" {
calls = 2
}
require.Equal(t, calls, models.callCount())
var afterItems int64
require.NoError(t, db.Model(&types.MemoryItem{}).Count(&afterItems).Error)
require.Equal(t, beforeItems, afterItems)
after, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
require.Equal(t, before.SessionCursors, after.SessionCursors)
require.Equal(t, before.PendingSessions, after.PendingSessions)
require.Zero(t, after.ExtractFailures)
if maintenance == "topics" {
topics, err := svc.repo.TopTopics(ctx, scope, 10)
require.NoError(t, err)
require.Len(t, topics, 1)
require.Equal(t, 1, topics[0].Hits, "revoked maintenance cannot fall back to counting the rejected model output")
}
})
}
}
}
func TestRecoveryWithNoReadableTranscriptSkipsAutomaticMaintenance(t *testing.T) {
svc, _, ctx, messages, models, queue := recoveryHarness(t)
svc.ScheduleExtraction(ctx, "s", "m", "model")
scope, err := ResolveScope(ctx)
require.NoError(t, err)
for i := range 6 {
require.NoError(t, svc.repo.CreateItem(ctx, &types.MemoryItem{TenantID: 1, SubjectID: scope.SubjectID,
Kind: types.MemoryKindFact, Content: fmt.Sprintf("deployment automation uses deterministic verification %d", i)}))
}
messages.owners = map[string]interfaces.MemoryScope{}
require.NoError(t, svc.Handle(context.Background(), queue.pop()))
require.Zero(t, models.callCount())
subject, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
require.Nil(t, subject.ConsolidatedAt)
require.Nil(t, subject.ExtractCursor)
}
func TestRecoveryRechecksPolicyAndReservationAroundProvider(t *testing.T) {
for _, phase := range []string{"before", "after", "truncated"} {
for _, change := range []string{"deleted subject", "disabled subject", "disabled workspace", "revoked owner", "disabled Agent", "revoked context Agent", "replaced attempt", "expired lease"} {
t.Run(phase+"/"+change, func(t *testing.T) {
svc, db, ctx, messages, models, queue := recoveryHarness(t)
scope, err := ResolveScope(ctx)
require.NoError(t, err)
base := time.Now().Add(-time.Hour)
prior := userMessage("s", "earlier preference", base)
input := userMessage("s", "current preference", base.Add(time.Minute))
messages.set("s", []*types.Message{prior, input})
svc.ScheduleExtraction(ctx, "s", "m", "model")
cursor := base.Add(time.Second)
require.NoError(t, db.Model(&types.MemorySubject{}).Where("tenant_id = ?", 1).
Updates(map[string]interface{}{"session_cursors": types.MemorySessionCursors{"s": cursor}, "extract_cursor": cursor}).Error)
before, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
models.response = `{"memories":[{"action":"add","kind":"preference","topic":"working style","content":"prefers clear examples"}],"topics":["software engineering"]}`
mutate := func() {
switch change {
case "deleted subject":
require.NoError(t, db.Where("tenant_id = ?", 1).Delete(&types.MemorySubject{}).Error)
case "disabled subject":
require.NoError(t, svc.repo.UpdateSubjectEnabled(ctx, scope, false))
case "disabled workspace":
svc.tenantRepo.(*stubTenantRepo).set(1, &types.MemoryConfig{Enabled: false})
case "revoked owner":
messages.owners = map[string]interfaces.MemoryScope{}
case "disabled Agent":
messages.deniedMessages = map[string]bool{input.ID: true}
case "revoked context Agent":
messages.deniedMessages = map[string]bool{prior.ID: true}
case "replaced attempt":
require.NoError(t, db.Model(&types.MemorySubject{}).Where("tenant_id = ?", 1).Update("extract_attempt_id", "replacement").Error)
case "expired lease":
require.NoError(t, db.Model(&types.MemorySubject{}).Where("tenant_id = ?", 1).Update("extract_lease_until", time.Now().Add(-time.Second)).Error)
}
}
if phase == "before" {
models.onGetChatModel = mutate
} else {
models.onChat = mutate
if phase == "truncated" {
models.truncateUntilCall = 1
}
}
err = svc.Handle(context.Background(), queue.pop())
stale := change == "replaced attempt" || change == "expired lease"
if stale {
require.ErrorIs(t, err, errExtractionAttemptLost)
} else {
require.NoError(t, err)
}
wantCalls := 1
if phase == "before" {
wantCalls = 0
}
require.Equal(t, wantCalls, models.callCount(), "revocation must also block the truncation retry and maintenance")
var count int64
require.NoError(t, db.Model(&types.MemoryItem{}).Count(&count).Error)
require.Zero(t, count, "discard provider output after revocation")
after, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
if change == "deleted subject" {
require.Nil(t, after, "worker cannot recreate a deleted subject")
return
}
require.Equal(t, before.PendingSessions, after.PendingSessions)
require.Equal(t, before.SessionCursors, after.SessionCursors)
require.True(t, before.ExtractCursor.Equal(*after.ExtractCursor))
if !stale {
require.Zero(t, after.ExtractFailures, "policy changes pause without charging the provider retry budget")
}
if change == "replaced attempt" {
require.Equal(t, "replacement", after.ExtractAttemptID)
}
})
}
}
}
func TestRecoveryRechecksWebAccountLifecycle(t *testing.T) {
for _, phase := range []string{"queued", "before", "after"} {
for _, change := range []string{"UPDATE users SET is_active = false", "UPDATE users SET deleted_at = CURRENT_TIMESTAMP", "UPDATE tenant_members SET status = 'suspended'"} {
t.Run(phase+"/"+change, func(t *testing.T) {
svc, db, ctx, _, models, queue := recoveryHarness(t)
require.NoError(t, db.AutoMigrate(&types.Session{}, &types.Message{}))
for _, sql := range []string{
`CREATE TABLE users (id TEXT PRIMARY KEY, is_active BOOLEAN, is_system_admin BOOLEAN, deleted_at DATETIME)`,
`CREATE TABLE tenant_members (user_id TEXT, tenant_id INTEGER, status TEXT, deleted_at DATETIME)`,
`INSERT INTO users (id, is_active, is_system_admin) VALUES ('alice', true, false)`,
`INSERT INTO tenant_members (user_id, tenant_id, status) VALUES ('alice', 1, 'active')`,
} {
require.NoError(t, db.Exec(sql).Error)
}
base := time.Now().Add(-time.Hour).UTC().Truncate(time.Microsecond)
session := &types.Session{TenantID: 1, UserID: "alice"}
require.NoError(t, db.Create(session).Error)
input := &types.Message{SessionID: session.ID, RequestID: "r", Role: "user", Content: "prefers simple examples", CreatedAt: base}
answer := &types.Message{SessionID: session.ID, RequestID: "r", Role: "assistant", Content: "okay", IsCompleted: true, CreatedAt: base.Add(time.Second)}
require.NoError(t, db.Create(input).Error)
require.NoError(t, db.Create(answer).Error)
svc.messageRepo = repository.NewMessageRepository(db)
svc.ScheduleExtraction(ctx, session.ID, answer.ID, "model")
task := queue.pop()
require.NotNil(t, task)
scope, err := ResolveScope(ctx)
require.NoError(t, err)
before, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
mutate := func() { require.NoError(t, db.Exec(change).Error) }
models.response = `{"memories":[{"action":"add","content":"prefers simple examples"}],"topics":["examples"]}`
switch phase {
case "queued":
mutate()
case "before":
models.onGetChatModel = mutate
case "after":
models.onChat = mutate
}
require.NoError(t, svc.Handle(context.Background(), task))
wantCalls := 0
if phase == "after" {
wantCalls = 1
}
require.Equal(t, wantCalls, models.callCount())
after, err := svc.repo.GetSubject(ctx, scope)
require.NoError(t, err)
require.Equal(t, before.SessionCursors, after.SessionCursors, "revoked owners never replay or advance historical cursors")
require.Nil(t, after.ExtractCursor)
require.Zero(t, after.ExtractFailures)
var count int64
require.NoError(t, db.Model(&types.MemoryItem{}).Count(&count).Error)
require.Zero(t, count)
})
}
}
}