Files
story/internal/callmemory/viking_console_test.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

140 lines
5.8 KiB
Go

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