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") } }