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,244 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func conflictingMemoryProposal(t *testing.T, svc *Service, ctx context.Context) (*types.MemoryItem, *types.MemoryItem) {
|
||||
t.Helper()
|
||||
accepted, err := svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "database", Content: "My database is MySQL",
|
||||
Origin: types.MemoryOriginExplicit, Importance: 4,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
proposal, err := svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "database", Content: "My database is PostgreSQL",
|
||||
Origin: types.MemoryOriginExtracted, Inferred: true, Importance: 4,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusPending, proposal.Status)
|
||||
return accepted, proposal
|
||||
}
|
||||
|
||||
func TestPendingConflictPreservesAcceptedMemoryWhenRejected(t *testing.T) {
|
||||
svc, _, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
accepted, proposal := conflictingMemoryProposal(t, svc, ctx)
|
||||
scope := scopeFor(t, ctx)
|
||||
|
||||
prior, err := svc.repo.GetItem(ctx, scope, accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
require.Nil(t, prior.InvalidAt)
|
||||
require.Empty(t, prior.SupersededBy)
|
||||
recall := svc.Recall(ctx, "database")
|
||||
require.Contains(t, recall.Prompt, accepted.Content)
|
||||
require.NotContains(t, recall.Prompt, proposal.Content)
|
||||
require.Contains(t, svc.RetrievalContextFor(ctx).Background, accepted.Content)
|
||||
require.NotContains(t, svc.RetrievalContextFor(ctx).Background, proposal.Content)
|
||||
|
||||
require.NoError(t, svc.RejectItem(ctx, proposal.ID))
|
||||
deleted, err := svc.repo.GetItem(ctx, scope, proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, deleted)
|
||||
prior, err = svc.repo.GetItem(ctx, scope, accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
require.Nil(t, prior.InvalidAt)
|
||||
require.Empty(t, prior.SupersededBy)
|
||||
require.Contains(t, svc.Recall(ctx, "database").Prompt, accepted.Content)
|
||||
|
||||
_, err = svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: proposal.Kind, Topic: proposal.Topic, Content: proposal.Content,
|
||||
Origin: types.MemoryOriginExtracted, Inferred: true,
|
||||
})
|
||||
require.ErrorIs(t, err, ErrPreviouslyForgotten)
|
||||
}
|
||||
|
||||
func TestConfirmPendingConflictSupersedesOnlyAcceptedConflict(t *testing.T) {
|
||||
svc, _, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
accepted, proposal := conflictingMemoryProposal(t, svc, ctx)
|
||||
unrelated, err := svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "role", Content: "I work as a data engineer",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
confirmed, err := svc.ConfirmItem(ctx, proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, confirmed.Status)
|
||||
scope := scopeFor(t, ctx)
|
||||
prior, err := svc.repo.GetItem(ctx, scope, accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusSuperseded, prior.Status)
|
||||
require.Equal(t, confirmed.ID, prior.SupersededBy)
|
||||
require.NotNil(t, prior.InvalidAt)
|
||||
require.Equal(t, accepted.Content, prior.Content)
|
||||
other, err := svc.repo.GetItem(ctx, scope, unrelated.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, other.Status)
|
||||
recall := svc.Recall(ctx, "database")
|
||||
require.Contains(t, recall.Prompt, proposal.Content)
|
||||
require.NotContains(t, recall.Prompt, accepted.Content)
|
||||
|
||||
_, err = svc.ConfirmItem(ctx, accepted.ID)
|
||||
require.ErrorIs(t, err, ErrItemNotPending, "historical memory must never be reactivated by confirmation")
|
||||
_, err = svc.ConfirmItem(ctx, confirmed.ID)
|
||||
require.ErrorIs(t, err, ErrItemNotPending)
|
||||
require.ErrorIs(t, svc.RejectItem(ctx, confirmed.ID), ErrItemNotPending,
|
||||
"a stale reject action must not delete a confirmed item")
|
||||
stillConfirmed, err := svc.repo.GetItem(ctx, scope, confirmed.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, stillConfirmed.Status)
|
||||
}
|
||||
|
||||
func TestConfirmPendingExtensionSupersedesContainedAcceptedMemory(t *testing.T) {
|
||||
svc, _, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
accepted, err := svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "role", Content: "I maintain data pipelines",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
proposal, err := svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "engineering", Content: "I maintain data pipelines in Go",
|
||||
Origin: types.MemoryOriginExtracted, Inferred: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
prior, err := svc.repo.GetItem(ctx, scopeFor(t, ctx), accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
_, err = svc.ConfirmItem(ctx, proposal.ID)
|
||||
require.NoError(t, err)
|
||||
prior, err = svc.repo.GetItem(ctx, scopeFor(t, ctx), accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusSuperseded, prior.Status)
|
||||
require.Equal(t, proposal.ID, prior.SupersededBy)
|
||||
}
|
||||
|
||||
func TestMemoryReviewRejectsWrongStatusAndScope(t *testing.T) {
|
||||
svc, db, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
_, proposal := conflictingMemoryProposal(t, svc, ctx)
|
||||
for _, foreignCtx := range []context.Context{
|
||||
enabledCtx(t, tenants, 1, "bob"),
|
||||
enabledCtx(t, tenants, 2, "alice"),
|
||||
} {
|
||||
foreignAccepted, foreignProposal := conflictingMemoryProposal(t, svc, foreignCtx)
|
||||
_, err := svc.ConfirmItem(foreignCtx, proposal.ID)
|
||||
require.ErrorIs(t, err, ErrItemNotFound)
|
||||
require.ErrorIs(t, svc.RejectItem(foreignCtx, proposal.ID), ErrItemNotFound)
|
||||
_, err = svc.ConfirmItem(foreignCtx, foreignProposal.ID)
|
||||
require.NoError(t, err)
|
||||
prior, err := svc.repo.GetItem(foreignCtx, scopeFor(t, foreignCtx), foreignAccepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusSuperseded, prior.Status)
|
||||
}
|
||||
stillPending, err := svc.repo.GetItem(ctx, scopeFor(t, ctx), proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusPending, stillPending.Status)
|
||||
|
||||
for _, status := range []string{types.MemoryStatusActive, types.MemoryStatusArchived, types.MemoryStatusSuperseded} {
|
||||
require.NoError(t, db.Model(&types.MemoryItem{}).Where("id = ?", proposal.ID).Update("status", status).Error)
|
||||
_, err := svc.ConfirmItem(ctx, proposal.ID)
|
||||
require.ErrorIs(t, err, ErrItemNotPending)
|
||||
require.ErrorIs(t, svc.RejectItem(ctx, proposal.ID), ErrItemNotPending)
|
||||
unchanged, err := svc.repo.GetItem(ctx, scopeFor(t, ctx), proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, status, unchanged.Status)
|
||||
}
|
||||
_, err = svc.ConfirmItem(ctx, "missing")
|
||||
require.ErrorIs(t, err, ErrItemNotFound)
|
||||
}
|
||||
|
||||
func TestPendingConfirmationRollsBackOnWriteFailureAndRetries(t *testing.T) {
|
||||
for _, failingStatus := range []string{types.MemoryStatusSuperseded, types.MemoryStatusActive} {
|
||||
t.Run(failingStatus, func(t *testing.T) {
|
||||
svc, db, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
accepted, proposal := conflictingMemoryProposal(t, svc, ctx)
|
||||
require.NoError(t, db.Exec(fmt.Sprintf(`CREATE TRIGGER fail_memory_review
|
||||
BEFORE UPDATE OF status ON memory_items WHEN NEW.status = '%s'
|
||||
BEGIN SELECT RAISE(ABORT, 'simulated review write failure'); END`, failingStatus)).Error)
|
||||
confirmed, err := svc.ConfirmItem(ctx, proposal.ID)
|
||||
require.ErrorContains(t, err, "simulated review write failure")
|
||||
require.Nil(t, confirmed)
|
||||
scope := scopeFor(t, ctx)
|
||||
prior, err := svc.repo.GetItem(ctx, scope, accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
require.Nil(t, prior.InvalidAt)
|
||||
require.Empty(t, prior.SupersededBy)
|
||||
pending, err := svc.repo.GetItem(ctx, scope, proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusPending, pending.Status)
|
||||
require.NoError(t, db.Exec("DROP TRIGGER fail_memory_review").Error)
|
||||
|
||||
confirmed, err = svc.ConfirmItem(ctx, proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, confirmed.Status)
|
||||
prior, err = svc.repo.GetItem(ctx, scope, accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusSuperseded, prior.Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingRejectionRollsBackTombstoneAndRetries(t *testing.T) {
|
||||
svc, db, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
accepted, proposal := conflictingMemoryProposal(t, svc, ctx)
|
||||
require.NoError(t, db.Exec(`CREATE TRIGGER fail_memory_rejection
|
||||
BEFORE DELETE ON memory_items WHEN OLD.status = 'pending'
|
||||
BEGIN SELECT RAISE(ABORT, 'simulated review delete failure'); END`).Error)
|
||||
require.ErrorContains(t, svc.RejectItem(ctx, proposal.ID), "simulated review delete failure")
|
||||
scope := scopeFor(t, ctx)
|
||||
forgotten, err := svc.repo.HasTombstone(ctx, scope, types.MemoryFingerprint(proposal.Content))
|
||||
require.NoError(t, err)
|
||||
require.False(t, forgotten, "failed rejection must not prevent a later review")
|
||||
pending, err := svc.repo.GetItem(ctx, scope, proposal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusPending, pending.Status)
|
||||
require.NoError(t, db.Exec("DROP TRIGGER fail_memory_rejection").Error)
|
||||
require.NoError(t, svc.RejectItem(ctx, proposal.ID))
|
||||
forgotten, err = svc.repo.HasTombstone(ctx, scope, types.MemoryFingerprint(proposal.Content))
|
||||
require.NoError(t, err)
|
||||
require.True(t, forgotten)
|
||||
prior, err := svc.repo.GetItem(ctx, scope, accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
}
|
||||
|
||||
func TestPendingWriteFailurePreservesAcceptedMemoryAndRetries(t *testing.T) {
|
||||
svc, db, tenants := newMemoryHarness(t)
|
||||
ctx := enabledCtx(t, tenants, 1, "alice")
|
||||
accepted, err := svc.Remember(ctx, types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "database", Content: "My database is MySQL",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
proposal := types.MemoryItem{
|
||||
Kind: types.MemoryKindProfile, Topic: "database", Content: "My database is PostgreSQL",
|
||||
Origin: types.MemoryOriginExtracted, Inferred: true,
|
||||
}
|
||||
require.NoError(t, db.Exec(`CREATE TRIGGER fail_memory_proposal
|
||||
BEFORE INSERT ON memory_items WHEN NEW.status = 'pending'
|
||||
BEGIN SELECT RAISE(ABORT, 'simulated proposal insert failure'); END`).Error)
|
||||
_, err = svc.Remember(ctx, proposal)
|
||||
require.ErrorContains(t, err, "simulated proposal insert failure")
|
||||
prior, err := svc.repo.GetItem(ctx, scopeFor(t, ctx), accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
require.NoError(t, db.Exec("DROP TRIGGER fail_memory_proposal").Error)
|
||||
stored, err := svc.Remember(ctx, proposal)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusPending, stored.Status)
|
||||
_, pendingCount, err := svc.ListItems(ctx, types.MemoryStatusPending, 10, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), pendingCount)
|
||||
prior, err = svc.repo.GetItem(ctx, scopeFor(t, ctx), accepted.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.MemoryStatusActive, prior.Status)
|
||||
}
|
||||
Reference in New Issue
Block a user