chore: establish local platform baseline

This commit is contained in:
Codex Local
2026-08-26 06:41:54 +08:00
commit 29bdf4900f
575 changed files with 95644 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
package device
import (
"context"
"os"
"testing"
"data-terminal-cloud/internal/deviceauth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestRepositoryPullRedeliverAcknowledgeCompleteAndExpireCommands(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
repository := NewRepository(pool)
identity := deviceauth.Identity{
ID: "10000000-0000-0000-0000-000000000501", PublicID: "10000000-0000-0000-0000-000000000511",
TenantID: "10000000-0000-0000-0000-000000000001", Status: "registered",
}
successID, successPublicID := uuid.NewString(), uuid.NewString()
failureID, failurePublicID := uuid.NewString(), uuid.NewString()
expiredID, expiredPublicID := uuid.NewString(), uuid.NewString()
keyPrefix := "device-command-core-" + uuid.NewString()
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM device_command WHERE id=ANY($1::uuid[])`, []string{successID, failureID, expiredID})
}()
insert := func(id, publicID, suffix, commandType, expires string) {
t.Helper()
if _, err := pool.Exec(ctx, `INSERT INTO device_command(id,public_id,tenant_id,device_id,command_type,payload,status,requested_by,idempotency_key,request_digest,expires_at)
VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5,$6::jsonb,'QUEUED','10000000-0000-0000-0000-000000000101'::uuid,$7,repeat('d',64),`+expires+`)`,
id, publicID, identity.TenantID, identity.ID, commandType, `{"fixture":"`+suffix+`"}`, keyPrefix+"-"+suffix); err != nil {
t.Fatal(err)
}
}
insert(successID, successPublicID, "success", "apply_config", "now()+interval '1 hour'")
insert(failureID, failurePublicID, "failure", "diagnostics", "now()+interval '1 hour'")
insert(expiredID, expiredPublicID, "expired", "reboot", "now()-interval '1 second'")
items, err := repository.PullCommands(ctx, identity, 0)
if err != nil || len(items) != 2 {
t.Fatalf("first pull items=%+v err=%v", items, err)
}
for _, item := range items {
if item.Status != "DELIVERED" || item.DeliveryAttempts != 1 {
t.Fatalf("first delivery item=%+v", item)
}
}
items, err = repository.PullCommands(ctx, identity, 99)
if err != nil || len(items) != 2 {
t.Fatalf("redelivery items=%+v err=%v", items, err)
}
for _, item := range items {
if item.Status != "DELIVERED" || item.DeliveryAttempts != 2 {
t.Fatalf("redelivery item=%+v", item)
}
}
var expiredStatus string
if err := pool.QueryRow(ctx, `SELECT status FROM device_command WHERE id=$1::uuid`, expiredID).Scan(&expiredStatus); err != nil || expiredStatus != "EXPIRED" {
t.Fatalf("expired status=%s err=%v", expiredStatus, err)
}
acknowledged, err := repository.AcknowledgeCommand(ctx, identity, successPublicID, CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"result_code": "RECEIVED"}})
if err != nil || acknowledged.Status != "ACKNOWLEDGED" || acknowledged.DeliveryAttempts != 2 {
t.Fatalf("acknowledged=%+v err=%v", acknowledged, err)
}
repeatedAck, err := repository.AcknowledgeCommand(ctx, identity, successPublicID, CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"result_code": "RECEIVED"}})
if err != nil || repeatedAck.Status != "ACKNOWLEDGED" {
t.Fatalf("repeated acknowledgement=%+v err=%v", repeatedAck, err)
}
items, err = repository.PullCommands(ctx, identity, 10)
if err != nil || len(items) != 2 {
t.Fatalf("in-flight recovery pull items=%+v err=%v", items, err)
}
for _, item := range items {
if item.DeliveryAttempts != 3 {
t.Fatalf("in-flight recovery attempts item=%+v", item)
}
if item.ID == successPublicID && item.Status != "ACKNOWLEDGED" {
t.Fatalf("acknowledged command regressed during recovery pull: %+v", item)
}
if item.ID == failurePublicID && item.Status != "DELIVERED" {
t.Fatalf("delivered command changed unexpectedly during recovery pull: %+v", item)
}
}
completed, err := repository.AcknowledgeCommand(ctx, identity, successPublicID, CommandAcknowledgement{Status: "SUCCEEDED", ResultSummary: map[string]any{"result_code": "APPLIED"}})
if err != nil || completed.Status != "SUCCEEDED" {
t.Fatalf("completed=%+v err=%v", completed, err)
}
failed, err := repository.AcknowledgeCommand(ctx, identity, failurePublicID, CommandAcknowledgement{Status: "FAILED", ResultSummary: map[string]any{"result_code": "DIAGNOSTIC_FAILED"}})
if err != nil || failed.Status != "FAILED" {
t.Fatalf("failed=%+v err=%v", failed, err)
}
if _, err := repository.AcknowledgeCommand(ctx, identity, successPublicID, CommandAcknowledgement{Status: "FAILED", ResultSummary: map[string]any{}}); err == nil {
t.Fatal("terminal command changed from SUCCEEDED to FAILED")
}
wrongDevice := identity
wrongDevice.ID = uuid.NewString()
if _, err := repository.AcknowledgeCommand(ctx, wrongDevice, failurePublicID, CommandAcknowledgement{Status: "FAILED", ResultSummary: map[string]any{}}); err == nil {
t.Fatal("different device acknowledged command")
}
if _, err := repository.AcknowledgeCommand(ctx, identity, failurePublicID, CommandAcknowledgement{Status: "FAILED", ResultSummary: map[string]any{"invalid": make(chan int)}}); err == nil {
t.Fatal("unencodable result summary accepted")
}
var successResult, failureResult string
if err := pool.QueryRow(ctx, `SELECT result_summary->>'result_code' FROM device_command WHERE id=$1::uuid`, successID).Scan(&successResult); err != nil || successResult != "APPLIED" {
t.Fatalf("success result=%q err=%v", successResult, err)
}
if err := pool.QueryRow(ctx, `SELECT result_summary->>'result_code' FROM device_command WHERE id=$1::uuid`, failureID).Scan(&failureResult); err != nil || failureResult != "DIAGNOSTIC_FAILED" {
t.Fatalf("failure result=%q err=%v", failureResult, err)
}
}
+31
View File
@@ -0,0 +1,31 @@
package device
import (
"context"
"crypto/sha256"
"encoding/hex"
"github.com/jackc/pgx/v5"
)
func recordFirmwareAssignmentEvent(ctx context.Context, tx pgx.Tx, commandID, commandStatus, assignmentStatus string, progress *int, resultJSON []byte) error {
digest := sha256.Sum256(resultJSON)
_, err := tx.Exec(ctx, `
INSERT INTO firmware_assignment_event(
assignment_id,tenant_id,device_id,command_id,command_status,assignment_status,phase,progress_percent,result_code,
active_slot,target_slot,previous_slot,failed_slot,boot_attempt_id,boot_health_deadline,rollback_applied,rollback_version,
installed_version,verified_sha256,verified_signing_key_id,
result_digest,result_summary
)
SELECT fa.id,fa.tenant_id,fa.device_id,fa.command_id,$2,$3,NULLIF($6::jsonb->>'phase',''),$4,NULLIF($6::jsonb->>'result_code',''),
NULLIF($6::jsonb->>'active_slot',''),NULLIF($6::jsonb->>'target_slot',''),NULLIF($6::jsonb->>'previous_slot',''),
NULLIF($6::jsonb->>'failed_slot',''),NULLIF($6::jsonb->>'boot_attempt_id',''),NULLIF($6::jsonb->>'boot_health_deadline',''),
CASE WHEN jsonb_typeof($6::jsonb->'rollback_applied')='boolean' THEN ($6::jsonb->>'rollback_applied')::boolean ELSE NULL END,
NULLIF($6::jsonb->>'rollback_version',''),NULLIF($6::jsonb->>'installed_version',''),NULLIF($6::jsonb->>'verified_sha256',''),
NULLIF($6::jsonb->>'verified_signing_key_id',''),$5,$6::jsonb
FROM firmware_assignment fa
WHERE fa.command_id=$1::uuid
ON CONFLICT(assignment_id,command_status,result_digest) DO NOTHING`,
commandID, commandStatus, assignmentStatus, progress, hex.EncodeToString(digest[:]), resultJSON)
return err
}
+246
View File
@@ -0,0 +1,246 @@
package device
import (
"context"
"encoding/json"
"fmt"
"time"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/firmwareversion"
"github.com/jackc/pgx/v5/pgxpool"
)
type BootstrapReport struct {
FirmwareVersion string
BoardRevision string
RTCSDKVersion string
Codec string
SampleRate int
Network string
DeviceTime time.Time
}
type Binding struct {
Bound bool `json:"bound"`
HouseholdID *string `json:"household_id,omitempty"`
SubjectID *string `json:"subject_id,omitempty"`
SubjectName *string `json:"subject_name,omitempty"`
}
type Bootstrap struct {
DeviceID string `json:"device_id"`
Status string `json:"status"`
Model string `json:"model"`
HardwareRevision *string `json:"hardware_revision,omitempty"`
MinimumSecurityVersion *string `json:"minimum_security_version,omitempty"`
MinimumFirmwareVersion *string `json:"minimum_firmware_version,omitempty"`
FirmwareCompatible bool `json:"firmware_compatible"`
ConfigRevision int64 `json:"config_revision"`
Binding Binding `json:"binding"`
}
type Configuration struct {
DeviceID string `json:"device_id"`
Revision int64 `json:"revision"`
DesiredConfig json.RawMessage `json:"desired_config"`
ReportedRevision *int64 `json:"reported_revision,omitempty"`
Binding Binding `json:"binding"`
}
type Command struct {
ID string `json:"id"`
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
Status string `json:"status"`
ExpiresAt time.Time `json:"expires_at"`
DeliveryAttempts int `json:"delivery_attempts"`
CreatedAt time.Time `json:"created_at"`
}
type CommandAcknowledgement struct {
Status string
ResultSummary map[string]any
}
type Repository struct{ pool *pgxpool.Pool }
func NewRepository(pool *pgxpool.Pool) *Repository { return &Repository{pool: pool} }
func (r *Repository) ReportBootstrap(ctx context.Context, identity deviceauth.Identity, report BootstrapReport) (Bootstrap, error) {
reportedConfig, err := json.Marshal(map[string]any{
"codec": report.Codec, "sample_rate": report.SampleRate, "network": report.Network,
"device_time": report.DeviceTime.UTC().Format(time.RFC3339),
})
if err != nil {
return Bootstrap{}, fmt.Errorf("marshal device report: %w", err)
}
tag, err := r.pool.Exec(ctx, `
UPDATE device_twin
SET firmware_version = $3, board_revision = NULLIF($4, ''), rtc_sdk_version = NULLIF($5, ''),
reported_config = $6::jsonb, last_reported_at = now(), updated_at = now()
WHERE device_id = $1::uuid AND tenant_id = $2::uuid`,
identity.ID, identity.TenantID, report.FirmwareVersion, report.BoardRevision,
report.RTCSDKVersion, reportedConfig,
)
if err != nil {
return Bootstrap{}, fmt.Errorf("update device bootstrap report: %w", err)
}
if tag.RowsAffected() != 1 {
return Bootstrap{}, fmt.Errorf("device twin is missing")
}
return r.readBootstrap(ctx, identity)
}
func (r *Repository) GetConfiguration(ctx context.Context, identity deviceauth.Identity) (Configuration, error) {
var result Configuration
var householdID, subjectID, subjectName *string
err := r.pool.QueryRow(ctx, `
SELECT d.public_id::text, dt.desired_revision, dt.desired_config, dt.reported_revision,
h.public_id::text, s.public_id::text, s.preferred_name
FROM device d
JOIN device_twin dt ON dt.device_id = d.id AND dt.tenant_id = d.tenant_id
LEFT JOIN device_binding b ON b.device_id = d.id AND b.tenant_id = d.tenant_id AND b.unbound_at IS NULL
LEFT JOIN household h ON h.id = b.household_id AND h.tenant_id = b.tenant_id
LEFT JOIN subject_profile s ON s.id = b.subject_id AND s.tenant_id = b.tenant_id
WHERE d.id = $1::uuid AND d.tenant_id = $2::uuid`, identity.ID, identity.TenantID,
).Scan(&result.DeviceID, &result.Revision, &result.DesiredConfig, &result.ReportedRevision,
&householdID, &subjectID, &subjectName)
if err != nil {
return Configuration{}, fmt.Errorf("read device configuration: %w", err)
}
result.Binding = newBinding(householdID, subjectID, subjectName)
return result, nil
}
func (r *Repository) PullCommands(ctx context.Context, identity deviceauth.Identity, limit int) ([]Command, error) {
if limit < 1 || limit > 20 {
limit = 10
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(context.Background())
if _, err := tx.Exec(ctx, `UPDATE device_command SET status='EXPIRED',completed_at=now(),version=version+1,updated_at=now() WHERE tenant_id=$1::uuid AND device_id=$2::uuid AND status IN ('QUEUED','DELIVERED','ACKNOWLEDGED') AND expires_at<=now()`, identity.TenantID, identity.ID); err != nil {
return nil, err
}
rows, err := tx.Query(ctx, `WITH selected AS (SELECT id FROM device_command WHERE tenant_id=$1::uuid AND device_id=$2::uuid AND status IN ('QUEUED','DELIVERED','ACKNOWLEDGED') AND expires_at>now() ORDER BY created_at,id LIMIT $3 FOR UPDATE SKIP LOCKED) UPDATE device_command dc SET status=CASE WHEN dc.status='ACKNOWLEDGED' THEN 'ACKNOWLEDGED' ELSE 'DELIVERED' END,delivery_attempts=delivery_attempts+1,delivered_at=COALESCE(delivered_at,now()),version=version+1,updated_at=now() FROM selected WHERE dc.id=selected.id RETURNING dc.public_id::text,dc.command_type,dc.payload,dc.status,dc.expires_at,dc.delivery_attempts,dc.created_at`, identity.TenantID, identity.ID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Command{}
for rows.Next() {
var item Command
if err := rows.Scan(&item.ID, &item.Type, &item.Payload, &item.Status, &item.ExpiresAt, &item.DeliveryAttempts, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return items, nil
}
func (r *Repository) AcknowledgeCommand(ctx context.Context, identity deviceauth.Identity, publicID string, input CommandAcknowledgement) (Command, error) {
resultJSON, err := json.Marshal(input.ResultSummary)
if err != nil {
return Command{}, err
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return Command{}, err
}
defer tx.Rollback(context.Background())
var item Command
var internalID string
err = tx.QueryRow(ctx, `UPDATE device_command SET status=$4,result_summary=$5::jsonb,acknowledged_at=COALESCE(acknowledged_at,now()),completed_at=CASE WHEN $4 IN ('SUCCEEDED','FAILED') THEN COALESCE(completed_at,now()) ELSE completed_at END,version=version+1,updated_at=now() WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid AND expires_at>now() AND (($4='ACKNOWLEDGED' AND status='DELIVERED') OR ($4 IN ('SUCCEEDED','FAILED') AND status IN ('DELIVERED','ACKNOWLEDGED')) OR status=$4) RETURNING id::text,public_id::text,command_type,payload,status,expires_at,delivery_attempts,created_at`, publicID, identity.TenantID, identity.ID, input.Status, resultJSON).Scan(&internalID, &item.ID, &item.Type, &item.Payload, &item.Status, &item.ExpiresAt, &item.DeliveryAttempts, &item.CreatedAt)
if err != nil {
return Command{}, fmt.Errorf("acknowledge device command: %w", err)
}
assignmentStatus, progress := firmwareAssignmentProgress(input)
if assignmentStatus != "" {
if _, err := tx.Exec(ctx, `UPDATE firmware_assignment SET status=$2,progress_percent=COALESCE($3,progress_percent),last_result=$4::jsonb,last_reported_at=now(),updated_at=now() WHERE command_id=$1::uuid`, internalID, assignmentStatus, progress, resultJSON); err != nil {
return Command{}, err
}
if err := recordFirmwareAssignmentEvent(ctx, tx, internalID, input.Status, assignmentStatus, progress, resultJSON); err != nil {
return Command{}, err
}
if _, err := tx.Exec(ctx, `UPDATE firmware_rollout ro SET status=CASE WHEN EXISTS(SELECT 1 FROM firmware_assignment fa WHERE fa.rollout_id=ro.id AND fa.status='FAILED') THEN 'PAUSED' ELSE 'COMPLETED' END,completed_at=CASE WHEN NOT EXISTS(SELECT 1 FROM firmware_assignment fa WHERE fa.rollout_id=ro.id AND fa.status NOT IN ('CONFIRMED','FAILED')) THEN now() ELSE completed_at END,updated_at=now() WHERE id IN (SELECT fa.rollout_id FROM firmware_assignment fa JOIN device_command dc ON dc.id=fa.command_id WHERE dc.public_id=$1::uuid) AND NOT EXISTS(SELECT 1 FROM firmware_assignment open WHERE open.rollout_id=ro.id AND open.status NOT IN ('CONFIRMED','FAILED'))`, publicID); err != nil {
return Command{}, err
}
}
if err := tx.Commit(ctx); err != nil {
return Command{}, err
}
return item, nil
}
func firmwareAssignmentProgress(input CommandAcknowledgement) (string, *int) {
if input.Status == "SUCCEEDED" {
value := 100
return "CONFIRMED", &value
}
if input.Status == "FAILED" {
return "FAILED", nil
}
if input.Status != "ACKNOWLEDGED" {
return "", nil
}
phase, _ := input.ResultSummary["phase"].(string)
status := ""
switch phase {
case "downloading":
status = "DOWNLOADING"
case "installing", "staging", "reboot_pending", "boot_verifying":
status = "INSTALLING"
default:
return "", nil
}
value, ok := input.ResultSummary["progress_percent"].(float64)
if !ok {
return status, nil
}
progress := int(value)
if progress < 0 {
progress = 0
}
if progress > 100 {
progress = 100
}
return status, &progress
}
func (r *Repository) readBootstrap(ctx context.Context, identity deviceauth.Identity) (Bootstrap, error) {
var result Bootstrap
var householdID, subjectID, subjectName, currentFirmware *string
err := r.pool.QueryRow(ctx, `
SELECT d.public_id::text, d.status, d.model, d.hardware_revision,
d.minimum_security_version, d.minimum_firmware_version, dt.firmware_version, dt.desired_revision,
h.public_id::text, s.public_id::text, s.preferred_name
FROM device d
JOIN device_twin dt ON dt.device_id = d.id AND dt.tenant_id = d.tenant_id
LEFT JOIN device_binding b ON b.device_id = d.id AND b.tenant_id = d.tenant_id AND b.unbound_at IS NULL
LEFT JOIN household h ON h.id = b.household_id AND h.tenant_id = b.tenant_id
LEFT JOIN subject_profile s ON s.id = b.subject_id AND s.tenant_id = b.tenant_id
WHERE d.id = $1::uuid AND d.tenant_id = $2::uuid`, identity.ID, identity.TenantID,
).Scan(&result.DeviceID, &result.Status, &result.Model, &result.HardwareRevision,
&result.MinimumSecurityVersion, &result.MinimumFirmwareVersion, &currentFirmware, &result.ConfigRevision,
&householdID, &subjectID, &subjectName)
if err != nil {
return Bootstrap{}, fmt.Errorf("read device bootstrap: %w", err)
}
result.Binding = newBinding(householdID, subjectID, subjectName)
result.FirmwareCompatible = result.MinimumFirmwareVersion == nil || (currentFirmware != nil && firmwareversion.AtLeast(*currentFirmware, *result.MinimumFirmwareVersion))
return result, nil
}
func newBinding(householdID, subjectID, subjectName *string) Binding {
return Binding{Bound: householdID != nil && subjectID != nil, HouseholdID: householdID, SubjectID: subjectID, SubjectName: subjectName}
}
@@ -0,0 +1,48 @@
package device
import (
"context"
"os"
"testing"
"time"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/deviceauth"
)
func TestRepositoryReportsBootstrapAndKeepsTenantBoundary(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := database.Open(ctx, databaseURL, "device-integration-test")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
repository := NewRepository(pool)
identity := deviceauth.Identity{
ID: "10000000-0000-0000-0000-000000000501", PublicID: "10000000-0000-0000-0000-000000000511",
TenantID: "10000000-0000-0000-0000-000000000001", Status: "registered",
}
result, err := repository.ReportBootstrap(ctx, identity, BootstrapReport{
FirmwareVersion: "0.1.1-test", BoardRevision: "sim-rev-a", RTCSDKVersion: "mock-rtc-1",
Codec: "opus", SampleRate: 16000, Network: "test", DeviceTime: time.Now(),
})
if err != nil {
t.Fatal(err)
}
if result.DeviceID != identity.PublicID || !result.Binding.Bound || result.ConfigRevision != 1 || !result.FirmwareCompatible || result.MinimumFirmwareVersion == nil || *result.MinimumFirmwareVersion != "0.1.0-sim" {
t.Fatalf("bootstrap = %#v", result)
}
configuration, err := repository.GetConfiguration(ctx, identity)
if err != nil || configuration.DeviceID != identity.PublicID || !configuration.Binding.Bound {
t.Fatalf("configuration = %#v, err = %v", configuration, err)
}
wrongTenant := identity
wrongTenant.TenantID = "20000000-0000-0000-0000-000000000001"
if _, err := repository.GetConfiguration(ctx, wrongTenant); err == nil {
t.Fatal("cross-tenant device configuration was not denied")
}
}
+84
View File
@@ -0,0 +1,84 @@
package device
import "testing"
func TestFirmwareAssignmentProgress(t *testing.T) {
tests := []struct {
name string
input CommandAcknowledgement
wantStatus string
wantProgress int
wantProgressOK bool
}{
{
name: "download progress",
input: CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"phase": "downloading", "progress_percent": float64(25)}},
wantStatus: "DOWNLOADING",
wantProgress: 25,
wantProgressOK: true,
},
{
name: "install progress",
input: CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"phase": "installing", "progress_percent": float64(80)}},
wantStatus: "INSTALLING",
wantProgress: 80,
wantProgressOK: true,
},
{
name: "reboot health verification remains installing",
input: CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"phase": "boot_verifying", "progress_percent": float64(95)}},
wantStatus: "INSTALLING",
wantProgress: 95,
wantProgressOK: true,
},
{
name: "negative progress is clamped",
input: CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"phase": "downloading", "progress_percent": float64(-2)}},
wantStatus: "DOWNLOADING",
wantProgress: 0,
wantProgressOK: true,
},
{
name: "oversized progress is clamped",
input: CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"phase": "installing", "progress_percent": float64(101)}},
wantStatus: "INSTALLING",
wantProgress: 100,
wantProgressOK: true,
},
{
name: "success confirms assignment",
input: CommandAcknowledgement{Status: "SUCCEEDED", ResultSummary: map[string]any{"installed_version": "2.3.1"}},
wantStatus: "CONFIRMED",
wantProgress: 100,
wantProgressOK: true,
},
{
name: "failure marks assignment failed",
input: CommandAcknowledgement{Status: "FAILED", ResultSummary: map[string]any{"error": "checksum mismatch"}},
wantStatus: "FAILED",
},
{
name: "unknown acknowledgement phase is ignored",
input: CommandAcknowledgement{Status: "ACKNOWLEDGED", ResultSummary: map[string]any{"phase": "restarting"}},
wantStatus: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, progress := firmwareAssignmentProgress(tt.input)
if status != tt.wantStatus {
t.Fatalf("status = %q, want %q", status, tt.wantStatus)
}
if tt.wantProgressOK {
if progress == nil || *progress != tt.wantProgress {
t.Fatalf("progress = %v, want %d", progress, tt.wantProgress)
}
return
}
if progress != nil {
t.Fatalf("progress = %d, want nil", *progress)
}
})
}
}
+307
View File
@@ -0,0 +1,307 @@
package device
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrTelemetryConfigAhead = errors.New("reported configuration is ahead of desired configuration")
ErrTelemetryConversationMissing = errors.New("telemetry conversation is missing or belongs to another device")
ErrTelemetryCommandConflict = errors.New("telemetry command result conflicts with command state")
)
type TelemetryNetwork struct {
Type string `json:"type"`
RSSI *int `json:"rssi,omitempty"`
}
type TelemetryRTC struct {
Joined bool `json:"joined"`
Uplink bool `json:"uplink"`
Downlink bool `json:"downlink"`
}
type TelemetryRecordingQueue struct {
PendingSegments int `json:"pending_segments"`
FailedSegments int `json:"failed_segments"`
LastFailureCode string `json:"last_failure_code,omitempty"`
LastFailureAt *time.Time `json:"last_failure_at,omitempty"`
}
type TelemetryStatus struct {
FirmwareVersion string `json:"firmware_version"`
ReportedConfigRevision int64 `json:"reported_config_revision"`
Network TelemetryNetwork `json:"network"`
RTC TelemetryRTC `json:"rtc"`
RecordingQueue TelemetryRecordingQueue `json:"recording_queue"`
ConversationStatus string `json:"conversation_status,omitempty"`
}
type TelemetryError struct {
Code string `json:"code"`
Severity string `json:"severity"`
}
type TelemetryCommandResult struct {
CommandID string `json:"command_id"`
Status string `json:"status"`
ResultSummary map[string]any `json:"result_summary"`
}
type TelemetryEvent struct {
EventKey string `json:"event_key"`
SequenceNo int64 `json:"sequence_no"`
OccurredAt time.Time `json:"occurred_at"`
EventType string `json:"event_type"`
ConversationID string `json:"conversation_id,omitempty"`
Status *TelemetryStatus `json:"status,omitempty"`
Error *TelemetryError `json:"error,omitempty"`
CommandResult *TelemetryCommandResult `json:"command_result,omitempty"`
}
type TelemetryBatchInput struct {
Events []TelemetryEvent `json:"events"`
}
type TelemetryConflict struct {
EventKey string `json:"event_key"`
SequenceNo int64 `json:"sequence_no"`
Reason string `json:"reason"`
}
type TelemetryConflictError struct {
Conflicts []TelemetryConflict
}
func (e *TelemetryConflictError) Error() string { return "telemetry event conflicts with stored event" }
type TelemetryAction struct {
Type string `json:"type"`
ConversationID string `json:"conversation_id,omitempty"`
}
type TelemetryBatchResponse struct {
AcceptedCount int `json:"accepted_count"`
DuplicateCount int `json:"duplicate_count"`
DesiredConfigRevision int64 `json:"desired_config_revision"`
Actions []TelemetryAction `json:"actions"`
}
type TelemetryService struct {
pool *pgxpool.Pool
executor *idempotency.Executor
}
func NewTelemetryService(pool *pgxpool.Pool, executor *idempotency.Executor) (*TelemetryService, error) {
if pool == nil || executor == nil {
return nil, errors.New("telemetry database and idempotency executor are required")
}
return &TelemetryService{pool: pool, executor: executor}, nil
}
func (s *TelemetryService) Submit(ctx context.Context, identity deviceauth.Identity, idempotencyKey string, rawRequest []byte, input TelemetryBatchInput) (TelemetryBatchResponse, bool, error) {
tenantID := identity.TenantID
result, err := s.executor.Execute(ctx, idempotency.Request{
TenantID: &tenantID, ActorKey: "device:" + identity.PublicID, Scope: "POST /device/v2/telemetry:batch",
Key: idempotencyKey, RequestHash: idempotency.Hash(rawRequest), ExpiresAfter: 24 * time.Hour,
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
response, submitErr := s.submit(operationContext, tx, identity, input)
if submitErr != nil {
return idempotency.Result{}, submitErr
}
body, marshalErr := json.Marshal(response)
if marshalErr != nil {
return idempotency.Result{}, marshalErr
}
resourceID := identity.ID
return idempotency.Result{StatusCode: http.StatusOK, Body: body, ResourceID: &resourceID}, nil
})
if err != nil {
return TelemetryBatchResponse{}, false, err
}
var response TelemetryBatchResponse
if err := json.Unmarshal(result.Body, &response); err != nil {
return TelemetryBatchResponse{}, result.Replayed, fmt.Errorf("decode telemetry response: %w", err)
}
return response, result.Replayed, nil
}
func (s *TelemetryService) submit(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, input TelemetryBatchInput) (TelemetryBatchResponse, error) {
var desiredRevision int64
var reportedRevision *int64
if err := tx.QueryRow(ctx, `SELECT desired_revision,reported_revision FROM device_twin WHERE device_id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&desiredRevision, &reportedRevision); err != nil {
return TelemetryBatchResponse{}, fmt.Errorf("read device telemetry state: %w", err)
}
response := TelemetryBatchResponse{DesiredConfigRevision: desiredRevision, Actions: []TelemetryAction{}}
conflicts := make([]TelemetryConflict, 0)
stopActions := make(map[string]struct{})
for _, event := range input.Events {
eventPayload, err := json.Marshal(event)
if err != nil {
return TelemetryBatchResponse{}, err
}
digest := idempotency.Hash(eventPayload)
rows, err := tx.Query(ctx, `SELECT event_key,sequence_no,payload_digest FROM device_telemetry_event WHERE device_id=$1::uuid AND (event_key=$2 OR sequence_no=$3) FOR UPDATE`, identity.ID, event.EventKey, event.SequenceNo)
if err != nil {
return TelemetryBatchResponse{}, err
}
matches := 0
exact := false
for rows.Next() {
matches++
var storedKey, storedDigest string
var storedSequence int64
if err := rows.Scan(&storedKey, &storedSequence, &storedDigest); err != nil {
rows.Close()
return TelemetryBatchResponse{}, err
}
if storedKey == event.EventKey && storedSequence == event.SequenceNo && storedDigest == digest {
exact = true
}
}
rows.Close()
if rows.Err() != nil {
return TelemetryBatchResponse{}, rows.Err()
}
if matches > 0 {
if matches == 1 && exact {
response.DuplicateCount++
continue
}
conflicts = append(conflicts, TelemetryConflict{EventKey: event.EventKey, SequenceNo: event.SequenceNo, Reason: "EVENT_KEY_OR_SEQUENCE_ALREADY_USED"})
continue
}
var conversationInternalID *string
if event.ConversationID != "" {
var internalID, conversationStatus string
err := tx.QueryRow(ctx, `SELECT id::text,status FROM conversation WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid`, event.ConversationID, identity.TenantID, identity.ID).Scan(&internalID, &conversationStatus)
if errors.Is(err, pgx.ErrNoRows) {
return TelemetryBatchResponse{}, ErrTelemetryConversationMissing
}
if err != nil {
return TelemetryBatchResponse{}, err
}
conversationInternalID = &internalID
if conversationStatus == "STOPPING" || conversationStatus == "COMPLETED" || conversationStatus == "FAILED" || conversationStatus == "EXPIRED" {
stopActions[event.ConversationID] = struct{}{}
}
}
var errorCode, errorSeverity *string
var commandInternalID *string
switch event.EventType {
case "status":
if event.Status.ReportedConfigRevision > desiredRevision {
return TelemetryBatchResponse{}, ErrTelemetryConfigAhead
}
if _, err := tx.Exec(ctx, `UPDATE device_twin SET
telemetry_sequence=$3,
reported_revision=CASE WHEN reported_revision IS NULL OR reported_revision<=$4 THEN $4 ELSE reported_revision END,
reported_config=CASE WHEN reported_revision IS NULL OR reported_revision<=$4 THEN reported_config || jsonb_build_object('telemetry',$5::jsonb) ELSE reported_config END,
firmware_version=NULLIF($6,''),last_reported_at=now(),updated_at=now()
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND telemetry_sequence<$3`, identity.ID, identity.TenantID,
event.SequenceNo, event.Status.ReportedConfigRevision, eventPayload, event.Status.FirmwareVersion); err != nil {
return TelemetryBatchResponse{}, err
}
if reportedRevision == nil || *reportedRevision < event.Status.ReportedConfigRevision {
value := event.Status.ReportedConfigRevision
reportedRevision = &value
}
case "error":
errorCode, errorSeverity = &event.Error.Code, &event.Error.Severity
if _, err := tx.Exec(ctx, `UPDATE device_twin SET telemetry_sequence=$3,last_error_code=$4,last_error_severity=$5,last_error_at=$6,last_reported_at=now(),updated_at=now() WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND telemetry_sequence<$3`, identity.ID, identity.TenantID, event.SequenceNo, event.Error.Code, event.Error.Severity, event.OccurredAt); err != nil {
return TelemetryBatchResponse{}, err
}
case "command_result":
internalID, err := applyTelemetryCommandResult(ctx, tx, identity, *event.CommandResult)
if err != nil {
return TelemetryBatchResponse{}, err
}
commandInternalID = &internalID
if _, err := tx.Exec(ctx, `UPDATE device_twin SET telemetry_sequence=$3,last_reported_at=now(),updated_at=now() WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND telemetry_sequence<$3`, identity.ID, identity.TenantID, event.SequenceNo); err != nil {
return TelemetryBatchResponse{}, err
}
}
if _, err := tx.Exec(ctx, `INSERT INTO device_telemetry_event(
tenant_id,device_id,conversation_id,event_key,sequence_no,event_type,occurred_at,payload_digest,payload,error_code,error_severity,command_id
) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6,$7,$8,$9::jsonb,$10,$11,$12::uuid)`, identity.TenantID, identity.ID,
conversationInternalID, event.EventKey, event.SequenceNo, event.EventType, event.OccurredAt, digest, eventPayload,
errorCode, errorSeverity, commandInternalID); err != nil {
return TelemetryBatchResponse{}, err
}
response.AcceptedCount++
}
if len(conflicts) > 0 {
return TelemetryBatchResponse{}, &TelemetryConflictError{Conflicts: conflicts}
}
if _, err := tx.Exec(ctx, `SELECT record_device_presence($1::uuid,$2::uuid,now())`, identity.TenantID, identity.ID); err != nil {
return TelemetryBatchResponse{}, err
}
if reportedRevision == nil || *reportedRevision < desiredRevision {
response.Actions = append(response.Actions, TelemetryAction{Type: "refresh_config"})
}
var hasCommands bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_command WHERE tenant_id=$1::uuid AND device_id=$2::uuid AND status IN ('QUEUED','DELIVERED','ACKNOWLEDGED') AND expires_at>now())`, identity.TenantID, identity.ID).Scan(&hasCommands); err != nil {
return TelemetryBatchResponse{}, err
}
if hasCommands {
response.Actions = append(response.Actions, TelemetryAction{Type: "pull_commands"})
}
for conversationID := range stopActions {
response.Actions = append(response.Actions, TelemetryAction{Type: "stop_conversation", ConversationID: conversationID})
}
return response, nil
}
func applyTelemetryCommandResult(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, input TelemetryCommandResult) (string, error) {
resultJSON, err := json.Marshal(input.ResultSummary)
if err != nil {
return "", err
}
var internalID string
err = tx.QueryRow(ctx, `UPDATE device_command SET status=$4,result_summary=$5::jsonb,
acknowledged_at=COALESCE(acknowledged_at,now()),
completed_at=CASE WHEN $4 IN ('SUCCEEDED','FAILED') THEN COALESCE(completed_at,now()) ELSE completed_at END,
version=version+1,updated_at=now()
WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid AND expires_at>now()
AND (($4='ACKNOWLEDGED' AND status='DELIVERED') OR ($4 IN ('SUCCEEDED','FAILED') AND status IN ('DELIVERED','ACKNOWLEDGED')) OR status=$4)
RETURNING id::text`, input.CommandID, identity.TenantID, identity.ID, input.Status, resultJSON).Scan(&internalID)
if errors.Is(err, pgx.ErrNoRows) {
return "", ErrTelemetryCommandConflict
}
if err != nil {
return "", err
}
assignmentStatus, progress := firmwareAssignmentProgress(CommandAcknowledgement{Status: input.Status, ResultSummary: input.ResultSummary})
if assignmentStatus != "" {
if _, err := tx.Exec(ctx, `UPDATE firmware_assignment SET status=$2,progress_percent=COALESCE($3,progress_percent),last_result=$4::jsonb,last_reported_at=now(),updated_at=now() WHERE command_id=$1::uuid`, internalID, assignmentStatus, progress, resultJSON); err != nil {
return "", err
}
if err := recordFirmwareAssignmentEvent(ctx, tx, internalID, input.Status, assignmentStatus, progress, resultJSON); err != nil {
return "", err
}
if _, err := tx.Exec(ctx, `UPDATE firmware_rollout ro SET
status=CASE WHEN EXISTS(SELECT 1 FROM firmware_assignment fa WHERE fa.rollout_id=ro.id AND fa.status='FAILED') THEN 'PAUSED' ELSE 'COMPLETED' END,
completed_at=CASE WHEN NOT EXISTS(SELECT 1 FROM firmware_assignment fa WHERE fa.rollout_id=ro.id AND fa.status NOT IN ('CONFIRMED','FAILED')) THEN now() ELSE completed_at END,
updated_at=now()
WHERE id IN (SELECT fa.rollout_id FROM firmware_assignment fa WHERE fa.command_id=$1::uuid)
AND NOT EXISTS(SELECT 1 FROM firmware_assignment open WHERE open.rollout_id=ro.id AND open.status NOT IN ('CONFIRMED','FAILED'))`, internalID); err != nil {
return "", err
}
}
return internalID, nil
}
@@ -0,0 +1,135 @@
package device
import (
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"github.com/jackc/pgx/v5/pgxpool"
)
const (
telemetryDeviceID = "94000000-0000-0000-0000-000000000501"
telemetryDevicePublicID = "94000000-0000-0000-0000-000000000511"
telemetryCommandID = "94000000-0000-0000-0000-000000000601"
telemetryCommandPublic = "94000000-0000-0000-0000-000000000611"
)
func TestTelemetryServiceAtomicallyUpdatesTwinAndCommand(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := database.Open(ctx, databaseURL, "device-telemetry-test")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
cleanupTelemetryFixture(t, ctx, pool)
defer cleanupTelemetryFixture(t, context.Background(), pool)
if _, err := pool.Exec(ctx, `INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref)
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',repeat('7',64),'telemetry-test','registered','test://telemetry')`, telemetryDeviceID, telemetryDevicePublicID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO device_twin(device_id,tenant_id,desired_revision,reported_revision)
VALUES($1::uuid,'10000000-0000-0000-0000-000000000001',2,1)`, telemetryDeviceID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO device_command(id,public_id,tenant_id,device_id,command_type,payload,status,requested_by,idempotency_key,request_digest,expires_at,delivery_attempts)
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',$3::uuid,'diagnostics','{}','DELIVERED','10000000-0000-0000-0000-000000000101','telemetry-fixture',repeat('8',64),now()+interval '1 hour',1)`, telemetryCommandID, telemetryCommandPublic, telemetryDeviceID); err != nil {
t.Fatal(err)
}
service, err := NewTelemetryService(pool, idempotency.NewExecutor(pool))
if err != nil {
t.Fatal(err)
}
identity := deviceauth.Identity{ID: telemetryDeviceID, PublicID: telemetryDevicePublicID, TenantID: "10000000-0000-0000-0000-000000000001", Status: "registered"}
rssi := -51
input := TelemetryBatchInput{Events: []TelemetryEvent{
{EventKey: "status-1", SequenceNo: 1, OccurredAt: time.Date(2026, 8, 23, 12, 0, 0, 0, time.UTC), EventType: "status", Status: &TelemetryStatus{
FirmwareVersion: "1.2.3", ReportedConfigRevision: 1, Network: TelemetryNetwork{Type: "wifi", RSSI: &rssi},
RTC: TelemetryRTC{}, RecordingQueue: TelemetryRecordingQueue{PendingSegments: 2},
}},
{EventKey: "error-2", SequenceNo: 2, OccurredAt: time.Date(2026, 8, 23, 12, 0, 1, 0, time.UTC), EventType: "error", Error: &TelemetryError{Code: "RTC_RECONNECTING", Severity: "warning"}},
{EventKey: "command-3", SequenceNo: 3, OccurredAt: time.Date(2026, 8, 23, 12, 0, 2, 0, time.UTC), EventType: "command_result", CommandResult: &TelemetryCommandResult{
CommandID: telemetryCommandPublic, Status: "SUCCEEDED", ResultSummary: map[string]any{"result_code": "OK"},
}},
}}
body, _ := json.Marshal(input)
response, replayed, err := service.Submit(ctx, identity, "telemetry-idempotency-0001", body, input)
if err != nil || replayed || response.AcceptedCount != 3 || response.DuplicateCount != 0 || len(response.Actions) != 1 || response.Actions[0].Type != "refresh_config" {
t.Fatalf("response=%#v replayed=%v err=%v", response, replayed, err)
}
replayedResponse, replayed, err := service.Submit(ctx, identity, "telemetry-idempotency-0001", body, input)
if err != nil || !replayed || replayedResponse.AcceptedCount != 3 {
t.Fatalf("replayed response=%#v replayed=%v err=%v", replayedResponse, replayed, err)
}
duplicateResponse, replayed, err := service.Submit(ctx, identity, "telemetry-idempotency-0002", body, input)
if err != nil || replayed || duplicateResponse.AcceptedCount != 0 || duplicateResponse.DuplicateCount != 3 {
t.Fatalf("duplicate response=%#v replayed=%v err=%v", duplicateResponse, replayed, err)
}
conflicting := input
conflicting.Events = append([]TelemetryEvent(nil), input.Events...)
conflicting.Events[1].Error = &TelemetryError{Code: "RTC_FAILED", Severity: "error"}
conflictingBody, _ := json.Marshal(conflicting)
if _, _, err := service.Submit(ctx, identity, "telemetry-idempotency-0003", conflictingBody, conflicting); err == nil {
t.Fatal("conflicting telemetry was accepted")
} else {
var conflict *TelemetryConflictError
if !errors.As(err, &conflict) || len(conflict.Conflicts) != 1 {
t.Fatalf("conflicting telemetry error=%v", err)
}
}
ahead := TelemetryBatchInput{Events: []TelemetryEvent{{EventKey: "status-4", SequenceNo: 4, OccurredAt: time.Now().UTC(), EventType: "status", Status: &TelemetryStatus{
FirmwareVersion: "1.2.3", ReportedConfigRevision: 3, Network: TelemetryNetwork{Type: "wifi"}, RTC: TelemetryRTC{}, RecordingQueue: TelemetryRecordingQueue{},
}}}}
aheadBody, _ := json.Marshal(ahead)
if _, _, err := service.Submit(ctx, identity, "telemetry-idempotency-0004", aheadBody, ahead); !errors.Is(err, ErrTelemetryConfigAhead) {
t.Fatalf("ahead configuration error=%v", err)
}
var telemetrySequence int64
var firmware, errorCode, errorSeverity, commandStatus string
var events int
if err := pool.QueryRow(ctx, `SELECT telemetry_sequence,firmware_version,last_error_code,last_error_severity FROM device_twin WHERE device_id=$1::uuid`, telemetryDeviceID).Scan(&telemetrySequence, &firmware, &errorCode, &errorSeverity); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT status FROM device_command WHERE id=$1::uuid`, telemetryCommandID).Scan(&commandStatus); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM device_telemetry_event WHERE device_id=$1::uuid`, telemetryDeviceID).Scan(&events); err != nil {
t.Fatal(err)
}
if telemetrySequence != 3 || firmware != "1.2.3" || errorCode != "RTC_RECONNECTING" || errorSeverity != "warning" || commandStatus != "SUCCEEDED" || events != 3 {
t.Fatalf("sequence=%d firmware=%s error=%s/%s command=%s events=%d", telemetrySequence, firmware, errorCode, errorSeverity, commandStatus, events)
}
}
func cleanupTelemetryFixture(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
t.Helper()
statements := []struct {
query string
arg string
}{
{`DELETE FROM device_telemetry_event WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM idempotency_record WHERE actor_key=$1`, "device:" + telemetryDevicePublicID},
{`DELETE FROM device_presence_state WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM device_command WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM device_twin WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM device WHERE id=$1::uuid`, telemetryDeviceID},
}
for _, statement := range statements {
if _, err := pool.Exec(ctx, statement.query, statement.arg); err != nil {
t.Fatalf("cleanup telemetry fixture: %v", err)
}
}
}