Files
story/internal/callmemory/viking_console_test.go
T

140 lines
5.8 KiB
Go
Raw Normal View History

package callmemory
import (
"context"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/memoryidentity"
"errors"
"github.com/google/uuid"
"strings"
"testing"
)
type fakeVikingConsole struct {
user string
records []VikingRecord
failQuery, failDelete, keepAfterDelete, limited bool
queries, deletes int
}
func (p *fakeVikingConsole) ChatMemories(_ context.Context, user, kind, query string, limit int, start, end int64) ([]VikingRecord, bool, error) {
p.queries++
if p.failQuery {
return nil, false, errors.New("provider unreachable")
}
out := []VikingRecord{}
if user == p.user {
for _, r := range p.records {
if r.Kind == kind {
out = append(out, r)
}
}
}
return out, p.limited, nil
}
func (p *fakeVikingConsole) DeleteChatMemory(_ context.Context, kind, id string) error {
p.deletes++
if p.failDelete {
return errors.New("uncertain delete")
}
if !p.keepAfterDelete {
p.records = nil
}
return nil
}
func consoleFixture(t *testing.T) (fixture, *fakeVikingConsole, VikingDelete) {
f := setup(t)
r := VikingRecord{ID: "synthetic-event", Kind: "event", Time: 123000, Content: "合成记忆", Digest: strings.Repeat("a", 64)}
p := &fakeVikingConsole{user: memoryidentity.UserID(f.tenant, f.subject), records: []VikingRecord{r}}
f.s.ConfigureVikingConsole(p)
return f, p, VikingDelete{ElderID: f.elder, OperationID: uuid.NewString(), Kind: "event", ID: r.ID, Digest: r.Digest, Time: r.Time, Reason: "合成删除测试"}
}
func consoleError(t *testing.T, f fixture, principal identity.Principal, op string, in any, code string) {
t.Helper()
_, e := f.s.MemoryRequest(context.Background(), principal, op, jsonBytes(in))
var typed *Error
if !errors.As(e, &typed) || typed.Code != code {
t.Fatalf("want %s, got %v", code, e)
}
}
func TestVikingConsoleDeleteVerifiedAuditAndReplay(t *testing.T) {
f, p, in := consoleFixture(t)
out := f.request(t, "viking/query", VikingQuery{ElderID: f.elder, Kind: "event"})
if out["source"] != "VIKING_NATIVE_CHAT" {
t.Fatal(out)
}
if !f.request(t, "viking/delete", in)["deleted"].(bool) {
t.Fatal("not verified")
}
f.request(t, "viking/delete", in)
if p.deletes != 1 {
t.Fatal("replay deleted again", p.deletes)
}
var state, result string
if e := f.p.QueryRow(context.Background(), `SELECT detail->>'state',result FROM audit_event WHERE id=$1::uuid`, in.OperationID).Scan(&state, &result); e != nil || state != "DELETED" || result != "success" {
t.Fatal(state, result, e)
}
in.ID = "other"
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_OPERATION_CONFLICT")
}
func TestVikingConsoleCannotDeleteAnotherElderOrChangedRecord(t *testing.T) {
f, p, in := consoleFixture(t)
g := setup(t)
in.ElderID = g.elder
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_RECORD_NOT_FOUND")
in.ElderID = f.elder
p.records[0].Digest = strings.Repeat("b", 64)
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_RECORD_CHANGED")
if p.deletes != 0 {
t.Fatal("unsafe provider delete")
}
outsider := identity.Principal{Issuer: f.principal.Issuer, Subject: uuid.NewString()}
consoleError(t, f, outsider, "viking/query", VikingQuery{ElderID: f.elder, Kind: "event"}, "MEMORY_ACCESS_DENIED")
}
func TestVikingConsoleFailureNeverReportsDeletedAndRetryReconciles(t *testing.T) {
f, p, in := consoleFixture(t)
p.failDelete = true
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_DELETE_UNCONFIRMED")
p.failDelete = false
p.records = nil // The first external request may have succeeded after a timeout.
if !f.request(t, "viking/delete", in)["deleted"].(bool) || p.deletes != 1 {
t.Fatal("uncertain retry not reconciled")
}
}
func TestVikingConsoleAckWithoutAbsenceIsUnconfirmed(t *testing.T) {
f, p, in := consoleFixture(t)
p.keepAfterDelete = true
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_DELETE_UNCONFIRMED")
p.keepAfterDelete = false
f.request(t, "viking/delete", in)
}
func TestVikingConsoleProviderFailureAndTruncationDoNotProveAbsence(t *testing.T) {
f, p, in := consoleFixture(t)
p.failQuery = true
consoleError(t, f, f.principal, "viking/query", VikingQuery{ElderID: f.elder, Kind: "event"}, "VIKING_QUERY_FAILED")
p.failQuery = false
p.failDelete = true
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_DELETE_UNCONFIRMED")
p.records = nil
p.limited = true
consoleError(t, f, f.principal, "viking/delete", in, "VIKING_RECORD_NOT_FOUND")
}
func TestVikingConsoleSensitivePermissionAndReadOnlyGrant(t *testing.T) {
f, p, in := consoleFixture(t)
user, sub := uuid.NewString(), uuid.NewString()
f.exec(t, `INSERT INTO user_account(id,public_id,tenant_id,oidc_issuer,oidc_subject,display_name,status) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$2,'console-reader','active')`, user, sub, f.tenant, f.principal.Issuer)
f.exec(t, `INSERT INTO household_member(tenant_id,household_id,user_id,role,status) VALUES($1::uuid,'10000000-0000-0000-0000-000000000201',$2::uuid,'viewer','active')`, f.tenant, user)
f.exec(t, `INSERT INTO memory_access_grant(subject_id,user_id,can_read,can_write,sensitive_read) VALUES($1::uuid,$2::uuid,true,false,false)`, f.subject, user)
reader := identity.Principal{Issuer: f.principal.Issuer, Subject: sub}
consoleError(t, f, reader, "viking/query", VikingQuery{ElderID: f.elder, Kind: "event"}, "VIKING_SENSITIVE_ACCESS_REQUIRED")
f.exec(t, `UPDATE memory_access_grant SET sensitive_read=true WHERE subject_id=$1::uuid AND user_id=$2::uuid`, f.subject, user)
out, e := f.s.MemoryRequest(context.Background(), reader, "viking/query", jsonBytes(VikingQuery{ElderID: f.elder, Kind: "event"}))
if e != nil || out.(map[string]any)["can_write"] != false {
t.Fatal(out, e)
}
consoleError(t, f, reader, "viking/delete", in, "MEMORY_ACCESS_DENIED")
if p.deletes != 0 {
t.Fatal("readonly delete")
}
}