feat: complete device onboarding and mobile binding workflows
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-12 08:57:29 +08:00
parent 7846f30d0b
commit 98fb2965d0
116 changed files with 15669 additions and 64 deletions
+126
View File
@@ -0,0 +1,126 @@
package onboarding
import (
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/embeddingprovider"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/orchestration"
"data-terminal-cloud/internal/outbox"
"data-terminal-cloud/internal/rtc"
"github.com/google/uuid"
"testing"
"time"
)
func TestIntegrationFullFirstCallThroughOrchestration(t *testing.T) {
f := newFixture(t)
f.bound()
j := f.registerSDK()
// This test exercises the existing SaaS call path for a device whose supplier
// credential is already available in the platform credential store.
tx, err := f.s.pool.Begin(f.ctx)
must(t, err)
must(t, f.s.storeRuntimeCredential(f.ctx, tx, f.d, j, []byte("fixture-device-secret-at-least-32-bytes")))
must(t, tx.Commit(f.ctx))
ack := ActivationAckInput{ActivationID: j.ID, ConfigVersion: j.ConfigVersion, LocalGeneration: 1}
_, err = f.s.ActivationAck(f.ctx, f.d, uuid.NewString(), requestJSON(ack), ack)
must(t, err)
attempt := AttemptInput{ActivationID: j.ID, ConfigVersion: j.ConfigVersion, AttemptID: uuid.NewString()}
_, err = f.s.Attempt(f.ctx, f.d, uuid.NewString(), requestJSON(attempt), attempt)
must(t, err)
tokens, err := rtc.NewLocalTokenIssuer([]byte("first-call-fixture-token-key-32-bytes-minimum"))
must(t, err)
leases, err := orchestration.NewRedisLeaseCache(f.s.redis, "onboarding-test:"+uuid.NewString())
must(t, err)
sessions, err := orchestration.NewService(idempotency.NewExecutor(f.s.pool), orchestration.NewRepository(), audit.NewRepository(), outbox.NewStore(f.s.pool), tokens, leases, orchestration.ServiceOptions{RTCAppID: "test-app", VoiceProvider: "mock", EngineMode: "s2s_hybrid", AllocationTTL: time.Minute, TokenTTL: time.Hour, HeartbeatInterval: 10, HardwareBotID: j.Bot, HardwareTransport: j.Transport})
must(t, err)
must(t, sessions.ConfigureMemoryEmbedding(f.s.pool, embeddingprovider.Local{}))
identity := deviceauth.Identity{ID: f.d.ID, PublicID: f.d.PublicID, TenantID: f.d.TenantID, Status: f.d.Status, ActivationState: f.d.Lifecycle, EnrollmentMode: "physical_confirm", BindingGeneration: f.d.Generation, ActivationTest: true}
var revision int64
must(t, f.s.pool.QueryRow(f.ctx, `SELECT desired_revision FROM device_twin WHERE device_id=$1::uuid`, f.d.ID).Scan(&revision))
input := orchestration.HardwareAgentCreateInput{DeviceTime: time.Now(), ConfigRevision: revision, RequestedGoal: "首次通话测试", Capabilities: orchestration.HardwareAgentCapabilities{HardwareAgent: true, SDKVersion: "1.0.0", Transports: []string{"high_quality"}, DynamicSessionConfig: true, ServerEventCallback: true}}
_, _, err = sessions.CreateHardwareAgent(f.ctx, identity, uuid.NewString(), uuid.NewString(), requestJSON(input), input)
if err != orchestration.ErrOnboardingState {
t.Fatalf("normal call before READY: %v", err)
}
input.Intent = "activation_test"
input.ActivationID = j.ID
input.AttemptID = attempt.AttemptID
input.PermitToken = ""
key := uuid.NewString()
created, replayed, err := sessions.CreateHardwareAgent(f.ctx, identity, key, uuid.NewString(), requestJSON(input), input)
must(t, err)
if replayed || created.Session.MaximumDurationSeconds != 60 {
t.Fatal("test allocation missing duration")
}
again, replayed, err := sessions.CreateHardwareAgent(f.ctx, identity, key, uuid.NewString(), requestJSON(input), input)
must(t, err)
if !replayed || again.ConversationID != created.ConversationID {
t.Fatal("test retry allocated another conversation")
}
_, _, err = sessions.CreateHardwareAgent(f.ctx, identity, uuid.NewString(), uuid.NewString(), requestJSON(input), input)
if err == nil {
t.Fatal("same permit created two conversations")
}
startedID, audioID, endedID := uuid.NewString(), uuid.NewString(), uuid.NewString()
event := orchestration.HardwareAgentEventInput{EventID: startedID, SequenceNo: 1, EventType: "session_started", OccurredAt: time.Now(), ProviderSessionRef: created.Runtime.ProviderSessionRef, SessionNonce: created.Runtime.SessionNonce, ProviderConnectionRef: "fixture-connection", BotID: created.Runtime.BotID, Transport: created.Runtime.Transport, ConfigDigest: created.Runtime.ConfigDigest, DynamicConfigApplied: true}
_, err = sessions.ProcessHardwareAgentEvent(f.ctx, identity, created.ConversationID, requestJSON(event), event)
must(t, err)
event.EventID = audioID
event.SequenceNo = 2
event.EventType = "first_audio_received"
event.OccurredAt = time.Now()
_, err = sessions.ProcessHardwareAgentEvent(f.ctx, identity, created.ConversationID, requestJSON(event), event)
must(t, err)
event.EventID = endedID
event.SequenceNo = 3
event.EventType = "session_ended"
event.OccurredAt = time.Now()
_, err = sessions.ProcessHardwareAgentEvent(f.ctx, identity, created.ConversationID, requestJSON(event), event)
must(t, err)
result := ResultInput{ActivationID: j.ID, AttemptID: attempt.AttemptID, Stage: "CONVERSATION_TEST", Outcome: "SUCCESS_OBSERVED"}
_, err = f.s.ActivationResult(f.ctx, f.d, uuid.NewString(), requestJSON(result), result)
must(t, err)
f.refresh()
if f.d.Lifecycle != "ACTIVE" {
t.Fatalf("lifecycle=%s", f.d.Lifecycle)
}
var state string
must(t, f.s.pool.QueryRow(f.ctx, `SELECT status FROM device_activation_job WHERE id=$1::uuid`, j.ID).Scan(&state))
if state != "READY" {
t.Fatal(state)
}
// Renewing an otherwise unchanged configuration requires durable ACK, but
// reuses this generation's observed first-call evidence.
_, err = f.s.pool.Exec(f.ctx, `UPDATE device_activation_job SET config_expires_at=now()-interval '1 second' WHERE id=$1::uuid`, j.ID)
must(t, err)
tx, err = f.s.pool.Begin(f.ctx)
must(t, err)
currentJob, err := readJob(f.ctx, tx, f.d.ID)
must(t, err)
must(t, f.s.ensureConfig(f.ctx, tx, f.d, &currentJob))
must(t, tx.Commit(f.ctx))
if currentJob.ConfigVersion <= j.ConfigVersion || currentJob.Status != "REGISTERED_OBSERVED" || currentJob.Ack != nil || currentJob.Engine == nil {
t.Fatal("invalid renewal state")
}
ack.ConfigVersion = currentJob.ConfigVersion
ack.LocalGeneration++
_, err = f.s.ActivationAck(f.ctx, f.d, uuid.NewString(), requestJSON(ack), ack)
must(t, err)
must(t, f.s.pool.QueryRow(f.ctx, `SELECT status FROM device_activation_job WHERE id=$1::uuid`, j.ID).Scan(&state))
if state != "READY" {
t.Fatal("renewal ACK did not restore READY")
}
if f.registrar.calls.Load() != 0 {
t.Fatal("renewal repeated provider registration")
}
input.Intent = ""
input.ActivationID = ""
input.AttemptID = ""
input.PermitToken = ""
identity.ActivationTest = false
identity.ActivationState = "ACTIVE"
_, _, err = sessions.CreateHardwareAgent(f.ctx, identity, uuid.NewString(), uuid.NewString(), requestJSON(input), input)
must(t, err)
}