Files

113 lines
5.2 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
package memorylifecycle
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"strings"
"testing"
"data-terminal-cloud/internal/developmentembedding"
"data-terminal-cloud/internal/outbox"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestDeleteProcessorRedactsMemoryAndIsIdempotent(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if _, err := NewDeleteProcessor(nil); err == nil {
t.Fatal("nil database pool was accepted")
}
processor, err := NewDeleteProcessor(pool)
if err != nil {
t.Fatal(err)
}
memoryID, memoryPublicID := uuid.NewString(), uuid.NewString()
versionID, versionPublicID := uuid.NewString(), uuid.NewString()
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, memoryPublicID)
_, _ = pool.Exec(ctx, `DELETE FROM memory_index_entry WHERE memory_id=$1::uuid`, memoryID)
_, _ = pool.Exec(ctx, `UPDATE curated_memory SET current_version_id=NULL WHERE id=$1::uuid`, memoryID)
_, _ = pool.Exec(ctx, `DELETE FROM memory_version WHERE memory_id=$1::uuid`, memoryID)
_, _ = pool.Exec(ctx, `DELETE FROM curated_memory WHERE id=$1::uuid`, memoryID)
}()
content := "这段合成记忆必须被可靠删除。"
if _, err := pool.Exec(ctx, `
INSERT INTO curated_memory(id,public_id,tenant_id,subject_id,status,sensitivity,valid_from)
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000401','ACTIVE','normal',now())`, memoryID, memoryPublicID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO memory_version(id,public_id,memory_id,version_no,content,source_refs,confirmed_by,change_reason,content_digest)
VALUES($1::uuid,$2::uuid,$3::uuid,1,$4,'[{"type":"integration"}]','10000000-0000-0000-0000-000000000101','原始原因',$5)`,
versionID, versionPublicID, memoryID, content, strings.Repeat("c", 64)); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `UPDATE curated_memory SET current_version_id=$1::uuid WHERE id=$2::uuid`, versionID, memoryID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO memory_index_entry(tenant_id,subject_id,memory_id,memory_version_id,index_type,embedding,provider_ref,status,indexed_at)
VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000401',$1::uuid,$2::uuid,'pgvector',$3::vector,'integration','ACTIVE',now())`,
memoryID, versionID, developmentembedding.Literal(developmentembedding.Vector(content))); err != nil {
t.Fatal(err)
}
if err := processor.Handle(ctx, outbox.Event{AggregateID: uuid.NewString()}); err != nil {
t.Fatalf("missing memory deletion: %v", err)
}
event := outbox.Event{AggregateID: memoryID}
if err := processor.Handle(ctx, event); err != nil {
t.Fatalf("active memory no-op: %v", err)
}
var status string
if err := pool.QueryRow(ctx, `SELECT status FROM curated_memory WHERE id=$1::uuid`, memoryID).Scan(&status); err != nil || status != "ACTIVE" {
t.Fatalf("active memory status=%s err=%v", status, err)
}
if _, err := pool.Exec(ctx, `UPDATE curated_memory SET status='PENDING_DELETE',valid_to=now() WHERE id=$1::uuid`, memoryID); err != nil {
t.Fatal(err)
}
if err := processor.Handle(ctx, event); err != nil {
t.Fatalf("delete memory: %v", err)
}
var deletedAtSet, embeddingIsNull, providerIsNull bool
var version int64
if err := pool.QueryRow(ctx, `SELECT status,version,deleted_at IS NOT NULL FROM curated_memory WHERE id=$1::uuid`, memoryID).Scan(&status, &version, &deletedAtSet); err != nil {
t.Fatal(err)
}
if status != "DELETED" || version != 2 || !deletedAtSet {
t.Fatalf("deleted memory status=%s version=%d deleted_at=%v", status, version, deletedAtSet)
}
var redactedContent, changeReason, contentDigest, indexStatus string
if err := pool.QueryRow(ctx, `SELECT content,change_reason,content_digest FROM memory_version WHERE id=$1::uuid`, versionID).Scan(&redactedContent, &changeReason, &contentDigest); err != nil {
t.Fatal(err)
}
expectedDigest := sha256.Sum256([]byte(deletedMemoryMarker))
if redactedContent != deletedMemoryMarker || !strings.Contains(changeReason, "user requested deletion") || contentDigest != hex.EncodeToString(expectedDigest[:]) {
t.Fatalf("redacted content=%q reason=%q digest=%q", redactedContent, changeReason, contentDigest)
}
if err := pool.QueryRow(ctx, `SELECT status,embedding IS NULL,provider_ref IS NULL FROM memory_index_entry WHERE memory_id=$1::uuid`, memoryID).Scan(&indexStatus, &embeddingIsNull, &providerIsNull); err != nil {
t.Fatal(err)
}
if indexStatus != "DELETED" || !embeddingIsNull || !providerIsNull {
t.Fatalf("deleted index status=%s embedding_null=%v provider_null=%v", indexStatus, embeddingIsNull, providerIsNull)
}
if err := processor.Handle(ctx, event); err != nil {
t.Fatalf("replay deletion: %v", err)
}
var auditCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM audit_event WHERE resource_id=$1::uuid AND action='memory.delete.complete'`, memoryPublicID).Scan(&auditCount); err != nil || auditCount != 1 {
t.Fatalf("deletion audit count=%d err=%v", auditCount, err)
}
}