ci / verify (push) Canceled after 0s
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.
140 lines
5.8 KiB
Go
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")
|
|
}
|
|
}
|