@@ -0,0 +1,104 @@
|
||||
package vikingmemory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"data-terminal-cloud/internal/outbox"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSessionSyncDedupAndConsent(t *testing.T) {
|
||||
db := os.Getenv("TEST_DATABASE_URL")
|
||||
if db == "" {
|
||||
t.Skip("TEST_DATABASE_URL required")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
tenant := "10000000-0000-0000-0000-000000000001"
|
||||
subject := "10000000-0000-0000-0000-000000000401"
|
||||
cs, csPublic, cx, cxPublic, conv, convPublic := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()
|
||||
exec := func(sql string, args ...any) {
|
||||
t.Helper()
|
||||
if _, err := pool.Exec(ctx, sql, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
pool.Exec(ctx, `DELETE FROM outbox_event WHERE aggregate_id=$1::uuid`, conv)
|
||||
pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, convPublic)
|
||||
pool.Exec(ctx, `DELETE FROM conversation_turn WHERE conversation_id=$1::uuid`, conv)
|
||||
pool.Exec(ctx, `DELETE FROM conversation WHERE id=$1::uuid`, conv)
|
||||
pool.Exec(ctx, `DELETE FROM context_snapshot WHERE id=$1::uuid`, cx)
|
||||
pool.Exec(ctx, `DELETE FROM consent_snapshot WHERE id=$1::uuid`, cs)
|
||||
}()
|
||||
exec(`INSERT INTO consent_snapshot(id,public_id,tenant_id,subject_id,snapshot_version,effective_consents,source_consent_ids) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,1,'{"personalized_memory":true,"transcript_storage":true,"story_extraction":true}','[]')`, cs, csPublic, tenant, subject)
|
||||
exec(`INSERT INTO context_snapshot(id,public_id,tenant_id,subject_id,profile_version,consent_snapshot_id,memory_versions,prompt_bundle_version,engine_mode,content_digest,sanitized_summary) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,1,$5::uuid,'[]','viking-test','s2s_hybrid',$6,'test')`, cx, cxPublic, tenant, subject, cs, strings.Repeat("a", 64))
|
||||
exec(`INSERT INTO conversation(id,public_id,tenant_id,household_id,subject_id,device_id,consent_snapshot_id,context_snapshot_id,provider_task_ref,engine_mode,retention_mode,status,allocation_expires_at) VALUES($1::uuid,$2::uuid,$3::uuid,'10000000-0000-0000-0000-000000000201',$4::uuid,'10000000-0000-0000-0000-000000000501',$5::uuid,$6::uuid,$7,'s2s_hybrid','transcript_only','COMPLETED',now())`, conv, convPublic, tenant, subject, cs, cx, "viking-"+conv)
|
||||
exec(`INSERT INTO conversation_turn(tenant_id,subject_id,conversation_id,turn_key,sequence_no,speaker,start_at,end_at,final_text,source_type) VALUES($1::uuid,$2::uuid,$3::uuid,$4,1,'subject',now(),now(),'我喜欢茶','provider'),($1::uuid,$2::uuid,$3::uuid,$5,2,'agent',now(),now(),'记住了','provider')`, tenant, subject, conv, "u"+conv, "a"+conv)
|
||||
calls := 0
|
||||
client := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
var payload map[string]any
|
||||
json.NewDecoder(r.Body).Decode(&payload)
|
||||
if r.URL.Path != "/api/memory/session/add" {
|
||||
t.Errorf("unexpected path %s", r.URL.Path)
|
||||
}
|
||||
metadata := payload["metadata"].(map[string]any)
|
||||
if metadata["default_user_id"] != UserID(tenant, subject) || len(payload["messages"].([]any)) != 2 {
|
||||
t.Errorf("unexpected payload %v", payload)
|
||||
}
|
||||
fmt.Fprint(w, `{"code":0}`)
|
||||
})
|
||||
service := NewService(pool, client)
|
||||
event := outbox.Event{AggregateID: conv, Payload: []byte(`{"generation_version":2}`)}
|
||||
if err := service.Enqueue(ctx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.Enqueue(ctx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var queued int
|
||||
pool.QueryRow(ctx, `SELECT count(*) FROM outbox_event WHERE aggregate_id=$1::uuid AND event_type=$2`, conv, SyncEvent).Scan(&queued)
|
||||
if queued != 1 {
|
||||
t.Fatalf("queued=%d", queued)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := service.Handle(ctx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("duplicate upstream writes=%d", calls)
|
||||
}
|
||||
// A late assistant correction must replace this session using the same ID.
|
||||
exec(`UPDATE conversation_turn SET final_text='好的,我会记住' WHERE conversation_id=$1::uuid AND speaker='agent'`, conv)
|
||||
if err := service.Handle(ctx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatal(calls)
|
||||
}
|
||||
exec(`UPDATE consent_snapshot SET effective_consents='{"transcript_storage":true}' WHERE id=$1::uuid`, cs)
|
||||
exec(`UPDATE conversation_turn SET final_text='撤回后不可写入' WHERE conversation_id=$1::uuid AND speaker='agent'`, conv)
|
||||
if err := service.Handle(ctx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatal("frozen consent ignored")
|
||||
}
|
||||
// Unknown or cross-tenant identities cannot retrieve or call the provider.
|
||||
items, err := service.Search(ctx, uuid.NewString(), subject, "茶", 2)
|
||||
if err != nil || len(items) != 0 || calls != 2 {
|
||||
t.Fatalf("scope gate items=%v err=%v calls=%d", items, err, calls)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user