2026-09-06 17:04:11 +08:00
package vikingmemory
import (
"context"
"data-terminal-cloud/internal/outbox"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
)
func testClient ( t * testing . T , handler http . HandlerFunc ) * Client {
t . Helper ()
server := httptest . NewTLSServer ( handler )
t . Cleanup ( server . Close )
c , err := NewClient ( Options { BaseURL : server . URL , ResourceID : "mem-test" , APIKey : "test-secret" , EventType : "event_v1" , ProfileType : "profile_v1" , Timeout : time . Second })
if err != nil {
t . Fatal ( err )
}
c . http = server . Client ()
return c
}
func TestSearchBindsUserAndRejectsForeignResults ( t * testing . T ) {
calls := 0
c := testClient ( t , func ( w http . ResponseWriter , r * http . Request ) {
calls ++
var input map [ string ] any
json . NewDecoder ( r . Body ). Decode ( & input )
filter := input [ "filter" ].( map [ string ] any )
if filter [ "user_id" ].([] any )[ 0 ] != "alice" || filter [ "assistant_id" ].([] any )[ 0 ] != AssistantID || input [ "resource_id" ] != "mem-test" {
t . Errorf ( "incorrect scope: %v" , input )
}
if r . Header . Get ( "Authorization" ) != "Bearer test-secret" {
t . Error ( "missing auth" )
}
fmt . Fprint ( w , `{"code":0,"data":{"result_list":[{"id":"foreign","user_id":["bob"],"assistant_id":["platform_companion_v1"],"memory_info":{"summary":"must not return"}},{"id":"ours","user_id":["alice"],"assistant_id":["platform_companion_v1"],"memory_info":{"summary":"喜欢茶"}}]}}` )
})
items , err := c . Search ( context . Background (), "alice" , "茶" , 2 )
if err != nil || len ( items ) != 2 || items [ 0 ]. ID != "ours" || calls != 2 {
t . Fatalf ( "items=%v calls=%d err=%v" , items , calls , err )
}
if _ , err = c . Search ( context . Background (), "" , "" , 1 ); err == nil {
t . Fatal ( "empty identity accepted" )
}
}
func TestVikingErrorsDoNotLeakServerBodyOrKey ( t * testing . T ) {
for _ , status := range [] int { 200 , 401 , 429 , 503 } {
t . Run ( fmt . Sprint ( status ), func ( t * testing . T ) {
c := testClient ( t , func ( w http . ResponseWriter , r * http . Request ) {
w . WriteHeader ( status )
fmt . Fprint ( w , `{"code":900,"message":"test-secret private transcript"}` )
})
err := c . AddSession ( context . Background (), "c_test" , "alice" , [] Message {{ Role : "user" , Content : "test" }}, 1 )
if err == nil || strings . Contains ( err . Error (), "secret" ) || strings . Contains ( err . Error (), "transcript" ) {
t . Fatalf ( "unsafe error %v" , err )
}
2026-10-08 10:06:44 +08:00
if outbox . IsRetryable ( err ) != ( status == 429 || status >= 500 ) {
2026-09-06 17:04:11 +08:00
t . Fatalf ( "retry classification %v" , err )
}
})
}
}
func TestStableNamespacedIdentityAndSessionIDs ( t * testing . T ) {
if UserID ( "a" , "x" ) == UserID ( "b" , "x" ) || UserID ( "a" , "x" ) == UserID ( "a" , "y" ) {
t . Fatal ( "identity collision" )
}
id := SessionID ( "12345678-1234-1234-1234-123456789abc" , 1 )
if ! regexp . MustCompile ( `^[a-zA-Z][a-zA-Z0-9_]{0,127}$` ). MatchString ( id ) || id == SessionID ( "12345678-1234-1234-1234-123456789abc" , 0 ) {
t . Fatal ( id )
}
}
func TestDeleteUsesExactUserForBothMemoryTypes ( t * testing . T ) {
calls := 0
c := testClient ( t , func ( w http . ResponseWriter , r * http . Request ) {
calls ++
var input map [ string ] any
json . NewDecoder ( r . Body ). Decode ( & input )
f := input [ "filter" ].( map [ string ] any )
if f [ "user_id" ].([] any )[ 0 ] != "alice" {
t . Error ( "unscoped delete" )
}
fmt . Fprint ( w , `{"code":0}` )
})
if err := c . DeleteUser ( context . Background (), "" ); err == nil || calls != 0 {
t . Fatal ( "empty deletion accepted" )
}
if err := c . DeleteUser ( context . Background (), "alice" ); err != nil || calls != 2 {
t . Fatal ( err , calls )
}
}
2026-10-08 10:06:44 +08:00
func TestVikingDeletionRateLimitPreservesCooldownAndFailure ( t * testing . T ) {
for _ , status := range [] int { http . StatusTooManyRequests , http . StatusOK } {
t . Run ( fmt . Sprint ( status ), func ( t * testing . T ) {
calls := 0
c := testClient ( t , func ( w http . ResponseWriter , r * http . Request ) {
calls ++
w . Header (). Set ( "Retry-After" , "120" )
w . WriteHeader ( status )
fmt . Fprint ( w , `{"code":1000029,"message":"private provider body"}` )
})
err := c . DeleteUser ( context . Background (), "alice" )
if err == nil || ! outbox . IsRetryable ( err ) || outbox . RetryDelay ( err ) != 2 * time . Minute || calls != 1 {
t . Fatalf ( "rate limit must remain an incomplete deletion: calls=%d delay=%s err=%v" , calls , outbox . RetryDelay ( err ), err )
}
if strings . Contains ( err . Error (), "private" ) {
t . Fatal ( "provider body leaked into retained error" )
}
})
}
}
func TestVikingRetryDelayParsesSecondsAndDates ( t * testing . T ) {
now := time . Date ( 2026 , 10 , 4 , 14 , 0 , 0 , 0 , time . UTC )
for _ , tt := range [] struct {
header string
want time . Duration
}{
{ "" , time . Minute }, { "invalid" , time . Minute }, { "-1" , time . Minute },
{ "120" , 2 * time . Minute }, { now . Add ( 90 * time . Second ). Format ( http . TimeFormat ), 90 * time . Second },
{ now . Add ( - time . Minute ). Format ( http . TimeFormat ), time . Minute }, { "9223372036854775807" , 24 * time . Hour },
} {
if got := vikingRetryDelay ( tt . header , now ); got != tt . want {
t . Errorf ( "Retry-After %q = %s, want %s" , tt . header , got , tt . want )
}
}
}