feat: expand companion platform, device controls, and memoir workflows
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.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -0,0 +1,154 @@
package billing
import (
"context"
"errors"
"os"
"strings"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
)
func TestUsageOverviewIncludesCallSessionsIntegration(t *testing.T) {
url := os.Getenv("TEST_DATABASE_URL")
if url == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := database.Open(ctx, url, "usage-call-session-test")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
service, err := NewService(pool, audit.NewRepository())
if err != nil {
t.Fatal(err)
}
exec := func(q string, args ...any) {
t.Helper()
if _, err := pool.Exec(ctx, q, args...); err != nil {
t.Fatal(err)
}
}
tenant := "10000000-0000-0000-0000-000000000001"
household, subject, device, legacyDevice, snapshot, contextID := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()
// Each fixture has its own household, so other billing tests cannot alter its totals.
exec(`INSERT INTO household(id,public_id,tenant_id,name,owner_user_id,status) VALUES($1::uuid,$1::uuid,$2::uuid,'usage fixture','10000000-0000-0000-0000-000000000101','active')`, household, tenant)
exec(`INSERT INTO household_member(tenant_id,household_id,user_id,role,status) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000101','owner','active')`, tenant, household)
exec(`INSERT INTO subject_profile(id,public_id,tenant_id,household_id,preferred_name,status) VALUES($1::uuid,$1::uuid,$2::uuid,$3::uuid,'usage subject','active')`, subject, tenant, household)
for _, id := range []string{device, legacyDevice} {
exec(`INSERT INTO device(id,public_id,tenant_id,serial_digest,model,hardware_revision,status,credential_ref,minimum_security_version,minimum_firmware_version) VALUES($1::uuid,$1::uuid,$2::uuid,$3,'usage device','test','registered','local-dev://usage','1','1')`, id, tenant, strings.Repeat(strings.ReplaceAll(id, "-", ""), 2))
}
exec(`INSERT INTO consent_snapshot(id,tenant_id,subject_id,snapshot_version,effective_consents,source_consent_ids) VALUES($1::uuid,$2::uuid,$3::uuid,1,'{}','[]')`, snapshot, tenant, subject)
exec(`INSERT INTO context_snapshot(id,tenant_id,subject_id,profile_version,consent_snapshot_id,prompt_bundle_version,engine_mode,content_digest) VALUES($1::uuid,$2::uuid,$3::uuid,1,$4::uuid,'test','s2s_hybrid',$5)`, contextID, tenant, subject, snapshot, strings.Repeat("0", 64))
defer func() {
for _, q := range []string{
`DELETE FROM call_utterance WHERE session_id IN (SELECT id FROM call_session WHERE subject_id=$1::uuid)`,
`DELETE FROM call_session WHERE subject_id=$1::uuid`,
`DELETE FROM conversation WHERE subject_id=$1::uuid`,
`DELETE FROM context_snapshot WHERE subject_id=$1::uuid`,
`DELETE FROM consent_snapshot WHERE subject_id=$1::uuid`,
`DELETE FROM subject_profile WHERE id=$1::uuid`,
} {
exec(q, subject)
}
exec(`DELETE FROM device WHERE id IN ($1::uuid,$2::uuid)`, device, legacyDevice)
exec(`DELETE FROM household_member WHERE household_id=$1::uuid`, household)
exec(`DELETE FROM web_event WHERE household_id=$1::uuid`, household)
exec(`DELETE FROM household WHERE id=$1::uuid`, household)
}()
now := time.Now().UTC()
loc, _ := time.LoadLocation("Asia/Shanghai")
local := now.In(loc)
midnight := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -1)
from := midnight.AddDate(0, 0, -1)
// Use prior calendar days so fixtures remain finished even just after midnight.
start := midnight.Add(10 * time.Hour)
direct := func(at time.Time, duration time.Duration, state string) string {
t.Helper()
id := uuid.NewString()
var ended any = at.Add(duration)
if duration < 0 {
ended = nil
}
exec(`INSERT INTO call_session(id,device_id,tenant_id,subject_id,boot_id,hook_event_id,call_generation,binding_generation,call_state,created_at,ended_at) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$1::uuid,$1,0,0,$5,$6,$7)`, id, device, tenant, subject, state, at, ended)
return id
}
legacy := uuid.NewString()
exec(`INSERT INTO conversation(id,public_id,tenant_id,household_id,subject_id,device_id,consent_snapshot_id,context_snapshot_id,provider_task_ref,engine_mode,retention_mode,status,allocation_expires_at,started_at,ended_at,created_at) VALUES($1::uuid,$1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,$6::uuid,$7::uuid,$1,'s2s_hybrid','none','COMPLETED',$8,$8,$9,$8)`, legacy, tenant, household, subject, legacyDevice, snapshot, contextID, midnight.Add(-10*time.Minute), midnight.Add(-7*time.Minute))
crossMidnight := direct(midnight.Add(-time.Minute), 2*time.Minute, "ENDED")
ended := direct(start, 4*time.Minute, "ENDED")
failed := direct(start.Add(5*time.Minute), 30*time.Second, "FAILED")
pending := direct(start.Add(10*time.Minute), -1, "STARTING")
// Multiple final utterances must not multiply a session, and no transcript
// consent or retained text is needed to count the other sessions.
for i := 0; i < 3; i++ {
exec(`INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,$2,1,'USER','fixture',false,$3)`, ended, uuid.NewString(), start.Add(time.Second))
}
direct(from.Add(-time.Minute), time.Second, "ENDED") // outside range
direct(now.Add(time.Minute), time.Second, "ENDED") // upper bound is exclusive
owner := identity.Principal{Issuer: billingTestIssuer, Subject: "00000000-0000-0000-0000-000000000101"}
result, err := service.FamilyUsageOverview(ctx, owner, household, "", "Asia/Shanghai", from, now)
if err != nil {
t.Fatal(err)
}
if result.ConversationCount != 5 || result.TotalDurationMS != 570000 || result.AverageDurationMS != 114000 || len(result.RecentConversations) != 5 {
t.Fatalf("mixed totals: %+v", result)
}
if result.CostAvailable || result.Currency != "" || result.CostMicros != 0 {
t.Fatalf("unrated costs: %+v", result)
}
statuses := map[string]string{}
for _, item := range result.RecentConversations {
statuses[item.ID] = item.Status
}
if statuses[crossMidnight] != "COMPLETED" || statuses[ended] != "COMPLETED" || statuses[failed] != "FAILED" || statuses[pending] != "STARTING_AGENT" {
t.Fatalf("statuses: %v", statuses)
}
for i := 1; i < len(result.RecentConversations); i++ {
if result.RecentConversations[i].StartedAt.After(result.RecentConversations[i-1].StartedAt) {
t.Fatal("recent sessions not ordered")
}
}
yesterday := midnight.AddDate(0, 0, -1).Format("2006-01-02")
var yesterdayDuration int64
for _, day := range result.Daily {
if day.Date == yesterday {
yesterdayDuration = day.DurationMS
}
}
if yesterdayDuration != 300000 || result.TodayDurationMS != 0 || result.ActiveDays != 2 {
t.Fatalf("local date totals: %+v", result)
}
// This device has no active binding and no legacy conversations. Its new
// session history must still allow a scoped device filter.
filtered, err := service.FamilyUsageOverview(ctx, owner, household, device, "Asia/Shanghai", from, now)
if err != nil || filtered.ConversationCount != 4 || filtered.TotalDurationMS != 390000 || filtered.Scope.DeviceID != device {
t.Fatalf("device totals: %+v err=%v", filtered, err)
}
// Moving the current device binding does not change the original subject's
// household ownership of historical sessions.
exec(`INSERT INTO device_binding(tenant_id,device_id,household_id,subject_id,generation) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000201','10000000-0000-0000-0000-000000000401',1)`, tenant, device)
filtered, err = service.FamilyUsageOverview(ctx, owner, household, device, "Asia/Shanghai", from, now)
exec(`DELETE FROM device_binding WHERE device_id=$1::uuid`, device)
if err != nil || filtered.ConversationCount != 4 {
t.Fatalf("rebound history: %+v err=%v", filtered, err)
}
other := identity.Principal{Issuer: billingTestIssuer, Subject: "00000000-0000-0000-0000-000000000103"}
if _, err := service.FamilyUsageOverview(ctx, other, household, "", "Asia/Shanghai", from, now); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("cross-tenant access: %v", err)
}
if _, err := service.FamilyUsageOverview(ctx, owner, "10000000-0000-0000-0000-000000000211", device, "Asia/Shanghai", from, now); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("cross-household device access: %v", err)
}
operator := identity.Principal{Issuer: billingTestIssuer, Subject: billingAdminSubject}
ops, err := service.OpsUsageOverview(ctx, operator, billingTenantPublicID, household, device, "Asia/Shanghai", from, now)
if err != nil || ops.ConversationCount != 4 || ops.TotalDurationMS != 390000 {
t.Fatalf("ops totals: %+v err=%v", ops, err)
}
}