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
+255
View File
@@ -0,0 +1,255 @@
package main
import (
"context"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
const maximumSimulatedFirmwareSize = 268435456
type simulatedDeviceCommand struct {
ID string `json:"id"`
Type string `json:"type"`
Payload map[string]any `json:"payload"`
}
type simulatedFirmwareManifest struct {
ReleaseID string
Version string
Digest string
Signature []byte
SigningKeyID string
DownloadPath string
}
type firmwareExecutionOutcome string
const (
firmwareCompleted firmwareExecutionOutcome = "completed"
firmwareFailed firmwareExecutionOutcome = "failed"
firmwareInterrupted firmwareExecutionOutcome = "interrupted"
firmwareRebootRequired firmwareExecutionOutcome = "reboot_required"
)
func (s *simulator) executeFirmwareCommand(ctx context.Context, command simulatedDeviceCommand) (firmwareExecutionOutcome, error) {
manifest, err := parseSimulatedFirmwareManifest(command.Payload)
if err != nil {
if ackErr := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"result_code": "MANIFEST_INVALID", "error_message": err.Error()}); ackErr != nil {
return firmwareFailed, ackErr
}
return firmwareFailed, nil
}
fault, err := simulatedFirmwareFault()
if err != nil {
return firmwareFailed, err
}
delay, err := simulatorOptionalDuration("SIM_FIRMWARE_STEP_DELAY", 30*time.Second)
if err != nil {
return firmwareFailed, err
}
abEnabled, err := simulatedABFirmwareEnabled()
if err != nil {
return firmwareFailed, err
}
if !abEnabled && isABFirmwareFault(fault) {
return firmwareFailed, errors.New("A/B firmware fault injection requires SIM_FIRMWARE_AB_UPDATE=true")
}
if abEnabled {
if handled, outcome, resumeErr := s.resumeABFirmwareCommand(ctx, command, manifest, fault, delay); handled || resumeErr != nil {
return outcome, resumeErr
}
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{"phase": "downloading", "progress_percent": 5, "result_code": "DOWNLOAD_STARTED"}); err != nil {
return firmwareFailed, err
}
if fault == "download_failed" {
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 5, "result_code": "DOWNLOAD_FAILED"})
}
content, err := s.downloadFirmware(ctx, manifest.DownloadPath)
if err != nil {
if ackErr := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 5, "result_code": "DOWNLOAD_FAILED", "error_message": err.Error()}); ackErr != nil {
return firmwareFailed, ackErr
}
return firmwareFailed, nil
}
digestValue := sha256.Sum256(content)
actualDigest := hex.EncodeToString(digestValue[:])
if fault == "digest_mismatch" || actualDigest != manifest.Digest {
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "DIGEST_MISMATCH", "expected_sha256": manifest.Digest, "actual_sha256": actualDigest, "bytes_downloaded": len(content)})
}
publicKey, err := simulatedFirmwareTrustedKey(manifest.SigningKeyID)
if err != nil {
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "SIGNING_KEY_UNTRUSTED", "signing_key_id": manifest.SigningKeyID, "error_message": err.Error()})
}
signedManifest := fmt.Sprintf("firmware-manifest-v1\nmodel=%s\nhardware_revision=%s\nversion=%s\nsha256=%s\nsize=%d\n", env("SIM_DEVICE_MODEL", "simulated-terminal"), env("SIM_DEVICE_HARDWARE_REVISION", "sim-rev-a"), manifest.Version, manifest.Digest, len(content))
if fault == "signature_mismatch" || !ed25519.Verify(publicKey, []byte(signedManifest), manifest.Signature) {
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "SIGNATURE_MISMATCH", "signing_key_id": manifest.SigningKeyID, "verified_sha256": actualDigest, "bytes_downloaded": len(content)})
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "ARTIFACT_VERIFIED", "verified_sha256": actualDigest, "verified_signing_key_id": manifest.SigningKeyID, "bytes_downloaded": len(content)}); err != nil {
return firmwareFailed, err
}
if fault == "interrupt_after_download" {
return firmwareInterrupted, nil
}
if err := waitForContext(ctx, delay); err != nil {
return firmwareInterrupted, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{"phase": "installing", "progress_percent": 80, "result_code": "INSTALL_STARTED"}); err != nil {
return firmwareFailed, err
}
if fault == "install_failed" {
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "installing", "progress_percent": 80, "result_code": "INSTALL_FAILED"})
}
if fault == "interrupt_after_install" {
return firmwareInterrupted, nil
}
if err := waitForContext(ctx, delay); err != nil {
return firmwareInterrupted, err
}
if abEnabled {
return s.stageABFirmwareCommand(ctx, command, manifest, fault)
}
if err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{"phase": "confirmed", "progress_percent": 100, "result_code": "SIMULATED_OK", "installed_version": manifest.Version, "verified_sha256": actualDigest, "verified_signing_key_id": manifest.SigningKeyID}); err != nil {
return firmwareFailed, err
}
return firmwareCompleted, nil
}
func parseSimulatedFirmwareManifest(payload map[string]any) (simulatedFirmwareManifest, error) {
digest := stringValue(payload, "sha256")
signature, signatureErr := base64.StdEncoding.DecodeString(stringValue(payload, "signature"))
manifest := simulatedFirmwareManifest{
ReleaseID: stringValue(payload, "release_id"),
Version: stringValue(payload, "version"),
Digest: digest,
Signature: signature,
SigningKeyID: stringValue(payload, "signing_key_id"),
DownloadPath: stringValue(payload, "download_path"),
}
if manifest.ReleaseID == "" || manifest.Version == "" || manifest.SigningKeyID == "" || len(manifest.Digest) != sha256.Size*2 || signatureErr != nil || len(manifest.Signature) != ed25519.SignatureSize {
return simulatedFirmwareManifest{}, errors.New("firmware command requires release_id, version, signing_key_id, a 64-character SHA-256 and a 64-byte Ed25519 signature")
}
if _, err := hex.DecodeString(manifest.Digest); err != nil {
return simulatedFirmwareManifest{}, errors.New("firmware command SHA-256 is not lowercase hexadecimal")
}
if manifest.Digest != strings.ToLower(manifest.Digest) {
return simulatedFirmwareManifest{}, errors.New("firmware command SHA-256 is not lowercase hexadecimal")
}
parsedPath, err := url.Parse(manifest.DownloadPath)
if err != nil || parsedPath.IsAbs() || parsedPath.Host != "" || parsedPath.RawQuery != "" || parsedPath.Fragment != "" || strings.Contains(parsedPath.Path, "..") || !strings.HasPrefix(parsedPath.Path, "/device/v2/firmware-releases/") || !strings.HasSuffix(parsedPath.Path, "/download") {
return simulatedFirmwareManifest{}, errors.New("firmware command download_path is outside the fixed device firmware boundary")
}
return manifest, nil
}
func stringValue(value map[string]any, key string) string {
item, _ := value[key].(string)
return strings.TrimSpace(item)
}
func simulatedFirmwareFault() (string, error) {
value := strings.ToLower(strings.TrimSpace(os.Getenv("SIM_FIRMWARE_FAULT")))
if value == "" || value == "none" {
return "none", nil
}
switch value {
case "download_failed", "digest_mismatch", "signature_mismatch", "install_failed", "interrupt_after_download", "interrupt_after_install", "power_loss_after_stage", "power_loss_during_boot", "boot_health_failed":
return value, nil
default:
return "", fmt.Errorf("SIM_FIRMWARE_FAULT must be none, download_failed, digest_mismatch, signature_mismatch, install_failed, interrupt_after_download, interrupt_after_install, power_loss_after_stage, power_loss_during_boot or boot_health_failed")
}
}
func isABFirmwareFault(value string) bool {
return value == "power_loss_after_stage" || value == "power_loss_during_boot" || value == "boot_health_failed"
}
func simulatedFirmwareTrustedKey(keyID string) (ed25519.PublicKey, error) {
raw := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_TRUSTED_KEYS"))
if raw == "" {
return nil, errors.New("SIM_FIRMWARE_TRUSTED_KEYS is empty")
}
var encoded map[string]string
if err := json.Unmarshal([]byte(raw), &encoded); err != nil {
return nil, errors.New("SIM_FIRMWARE_TRUSTED_KEYS must be a JSON object of key IDs to Base64 Ed25519 public keys")
}
value, exists := encoded[keyID]
if !exists {
return nil, fmt.Errorf("signing key %s is not provisioned", keyID)
}
publicKey, err := base64.StdEncoding.DecodeString(value)
if err != nil || len(publicKey) != ed25519.PublicKeySize {
return nil, fmt.Errorf("signing key %s is not a 32-byte Base64 Ed25519 public key", keyID)
}
return ed25519.PublicKey(publicKey), nil
}
func simulatorOptionalDuration(key string, maximum time.Duration) (time.Duration, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return 0, nil
}
parsed, err := time.ParseDuration(value)
if err != nil || parsed < 0 || parsed > maximum {
return 0, fmt.Errorf("%s must be a non-negative duration no greater than %s", key, maximum)
}
return parsed, nil
}
func (s *simulator) ackDeviceCommand(ctx context.Context, commandID, status string, result map[string]any) error {
body, err := json.Marshal(map[string]any{"status": status, "result_summary": result})
if err != nil {
return err
}
_, err = s.request(ctx, http.MethodPost, "/device/v2/commands/"+commandID+"/ack", body, "")
return err
}
func (s *simulator) downloadFirmware(ctx context.Context, path string) ([]byte, error) {
result, err := s.request(ctx, http.MethodGet, path, nil, "")
if err != nil {
return nil, err
}
var address struct {
DownloadURL string `json:"download_url"`
}
if err := decodeData(result.Body, &address); err != nil || address.DownloadURL == "" {
return nil, errors.New("firmware download response did not contain a private URL")
}
parsed, err := url.Parse(address.DownloadURL)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
return nil, errors.New("firmware private download URL is invalid")
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, address.DownloadURL, nil)
if err != nil {
return nil, err
}
response, err := s.client.Do(request)
if err != nil {
return nil, fmt.Errorf("download firmware object: %w", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("firmware object returned HTTP %d", response.StatusCode)
}
content, err := io.ReadAll(io.LimitReader(response.Body, maximumSimulatedFirmwareSize+1))
if err != nil {
return nil, fmt.Errorf("read firmware object: %w", err)
}
if len(content) == 0 || len(content) > maximumSimulatedFirmwareSize {
return nil, fmt.Errorf("firmware object size %d is outside 1 to %d bytes", len(content), maximumSimulatedFirmwareSize)
}
return content, nil
}
+298
View File
@@ -0,0 +1,298 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const (
firmwareBootStaged = "STAGED"
firmwareBootRebootPending = "REBOOT_PENDING"
firmwareBootConfirmed = "CONFIRMED"
firmwareBootRolledBack = "ROLLED_BACK"
)
type simulatedFirmwareBootState struct {
Phase string `json:"phase"`
CommandID string `json:"command_id"`
ReleaseID string `json:"release_id"`
TargetVersion string `json:"target_version"`
PreviousVersion string `json:"previous_version"`
Digest string `json:"sha256"`
ActiveSlot string `json:"active_slot"`
PreviousSlot string `json:"previous_slot"`
TargetSlot string `json:"target_slot"`
FailedSlot string `json:"failed_slot,omitempty"`
BootAttemptID string `json:"boot_attempt_id,omitempty"`
TerminalResultCode string `json:"terminal_result_code,omitempty"`
HealthDeadline time.Time `json:"health_deadline,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
func simulatedABFirmwareEnabled() (bool, error) {
value := strings.ToLower(strings.TrimSpace(os.Getenv("SIM_FIRMWARE_AB_UPDATE")))
if value == "" || value == "false" {
return false, nil
}
if value != "true" {
return false, errors.New("SIM_FIRMWARE_AB_UPDATE must be true or false")
}
statePath := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_STATE_FILE"))
if statePath == "" || !filepath.IsAbs(statePath) {
return false, errors.New("SIM_FIRMWARE_STATE_FILE must be an absolute path when A/B firmware simulation is enabled")
}
evidencePath := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_EVIDENCE_FILE"))
if evidencePath != "" && !filepath.IsAbs(evidencePath) {
return false, errors.New("SIM_FIRMWARE_EVIDENCE_FILE must be an absolute path")
}
return true, nil
}
func simulatedFirmwareHealthWindow() (time.Duration, error) {
value := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_BOOT_HEALTH_WINDOW"))
if value == "" {
return 2 * time.Minute, nil
}
duration, err := time.ParseDuration(value)
if err != nil || duration < time.Second || duration > 30*time.Minute {
return 0, errors.New("SIM_FIRMWARE_BOOT_HEALTH_WINDOW must be between 1s and 30m")
}
return duration, nil
}
func simulatedFirmwareInitialSlot() (string, error) {
value := strings.ToUpper(env("SIM_FIRMWARE_ACTIVE_SLOT", "A"))
if value != "A" && value != "B" {
return "", errors.New("SIM_FIRMWARE_ACTIVE_SLOT must be A or B")
}
return value, nil
}
func inactiveFirmwareSlot(active string) string {
if active == "A" {
return "B"
}
return "A"
}
func loadSimulatedFirmwareBootState() (*simulatedFirmwareBootState, error) {
path := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_STATE_FILE"))
content, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("read simulated firmware state: %w", err)
}
var state simulatedFirmwareBootState
decoder := json.NewDecoder(strings.NewReader(string(content)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&state); err != nil {
return nil, fmt.Errorf("decode simulated firmware state: %w", err)
}
if state.ActiveSlot != "A" && state.ActiveSlot != "B" {
return nil, errors.New("simulated firmware state has an invalid active slot")
}
switch state.Phase {
case firmwareBootStaged, firmwareBootRebootPending, firmwareBootConfirmed, firmwareBootRolledBack:
default:
return nil, errors.New("simulated firmware state has an invalid phase")
}
return &state, nil
}
func saveSimulatedFirmwareBootState(state simulatedFirmwareBootState) error {
path := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_STATE_FILE"))
if !filepath.IsAbs(path) {
return errors.New("SIM_FIRMWARE_STATE_FILE must be an absolute path")
}
state.UpdatedAt = time.Now().UTC()
content, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("open simulated firmware state: %w", err)
}
if _, err = file.Write(content); err == nil {
err = file.Sync()
}
closeErr := file.Close()
if err != nil {
return fmt.Errorf("write simulated firmware state: %w", err)
}
if closeErr != nil {
return fmt.Errorf("close simulated firmware state: %w", closeErr)
}
return nil
}
func (s *simulator) resumeABFirmwareCommand(ctx context.Context, command simulatedDeviceCommand, manifest simulatedFirmwareManifest, fault string, delay time.Duration) (bool, firmwareExecutionOutcome, error) {
state, err := loadSimulatedFirmwareBootState()
if err != nil || state == nil {
return false, firmwareFailed, err
}
if state.Phase == firmwareBootConfirmed || state.Phase == firmwareBootRolledBack {
if state.CommandID != command.ID {
return false, firmwareCompleted, nil
}
if state.Phase == firmwareBootConfirmed {
err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{
"phase": "confirmed", "progress_percent": 100, "result_code": "SIMULATED_AB_BOOT_CONFIRMED", "installed_version": manifest.Version,
"verified_sha256": manifest.Digest, "verified_signing_key_id": manifest.SigningKeyID, "active_slot": state.ActiveSlot, "previous_slot": state.PreviousSlot, "boot_attempt_id": state.BootAttemptID,
})
return true, firmwareCompleted, err
}
resultCode := state.TerminalResultCode
if resultCode == "" {
resultCode = "BOOT_HEALTH_FAILED_ROLLED_BACK"
}
err := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{
"phase": "rolled_back", "progress_percent": 95, "result_code": resultCode, "rollback_applied": true,
"active_slot": state.ActiveSlot, "failed_slot": state.FailedSlot, "rollback_version": state.TargetVersion, "boot_attempt_id": state.BootAttemptID,
})
return true, firmwareFailed, err
}
if state.CommandID != command.ID || state.ReleaseID != manifest.ReleaseID || state.TargetVersion != manifest.Version || state.Digest != manifest.Digest {
return true, firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "rolled_back", "progress_percent": 0, "result_code": "LOCAL_UPDATE_STATE_CONFLICT", "rollback_applied": false})
}
if state.Phase == firmwareBootStaged {
outcome, err := s.armSimulatedFirmwareBoot(ctx, command.ID, *state)
return true, outcome, err
}
bootState := *state
bootState.ActiveSlot = state.TargetSlot
if err := recordSimulatedFirmwareEvidence(command, manifest, "boot_verifying", bootState, "BOOT_HEALTH_CHECK_STARTED"); err != nil {
return true, firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{
"phase": "boot_verifying", "progress_percent": 95, "result_code": "BOOT_HEALTH_CHECK_STARTED",
"active_slot": state.TargetSlot, "previous_slot": state.PreviousSlot, "boot_attempt_id": state.BootAttemptID, "boot_health_deadline": state.HealthDeadline,
}); err != nil {
return true, firmwareFailed, err
}
if fault == "power_loss_during_boot" {
if err := recordSimulatedFirmwareEvidence(command, manifest, "power_loss_during_boot", *state, "SIMULATED_POWER_LOSS"); err != nil {
return true, firmwareFailed, err
}
return true, firmwareInterrupted, nil
}
rollbackCode := ""
if fault == "boot_health_failed" {
rollbackCode = "BOOT_HEALTH_FAILED_ROLLED_BACK"
} else if !state.HealthDeadline.IsZero() && time.Now().After(state.HealthDeadline) {
rollbackCode = "BOOT_HEALTH_TIMEOUT_ROLLED_BACK"
}
if rollbackCode != "" {
failedSlot := state.TargetSlot
state.Phase = firmwareBootRolledBack
state.ActiveSlot = state.PreviousSlot
state.FailedSlot = failedSlot
state.TerminalResultCode = rollbackCode
state.TargetSlot = ""
state.TargetVersion = state.PreviousVersion
if err := saveSimulatedFirmwareBootState(*state); err != nil {
return true, firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(command, manifest, "rolled_back", *state, rollbackCode); err != nil {
return true, firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{
"phase": "rolled_back", "progress_percent": 95, "result_code": rollbackCode, "rollback_applied": true,
"active_slot": state.ActiveSlot, "failed_slot": failedSlot, "rollback_version": state.PreviousVersion, "boot_attempt_id": state.BootAttemptID,
}); err != nil {
return true, firmwareFailed, err
}
return true, firmwareFailed, nil
}
if err := waitForContext(ctx, delay); err != nil {
return true, firmwareInterrupted, err
}
confirmedSlot := state.TargetSlot
state.Phase = firmwareBootConfirmed
state.ActiveSlot = confirmedSlot
state.TerminalResultCode = "SIMULATED_AB_BOOT_CONFIRMED"
state.TargetSlot = ""
state.PreviousVersion = state.TargetVersion
if err := saveSimulatedFirmwareBootState(*state); err != nil {
return true, firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(command, manifest, "confirmed", *state, "SIMULATED_AB_BOOT_CONFIRMED"); err != nil {
return true, firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{
"phase": "confirmed", "progress_percent": 100, "result_code": "SIMULATED_AB_BOOT_CONFIRMED", "installed_version": manifest.Version,
"verified_sha256": manifest.Digest, "verified_signing_key_id": manifest.SigningKeyID, "active_slot": confirmedSlot, "previous_slot": state.PreviousSlot, "boot_attempt_id": state.BootAttemptID,
}); err != nil {
return true, firmwareFailed, err
}
return true, firmwareCompleted, nil
}
func (s *simulator) stageABFirmwareCommand(ctx context.Context, command simulatedDeviceCommand, manifest simulatedFirmwareManifest, fault string) (firmwareExecutionOutcome, error) {
current, err := loadSimulatedFirmwareBootState()
if err != nil {
return firmwareFailed, err
}
activeSlot, err := simulatedFirmwareInitialSlot()
if err != nil {
return firmwareFailed, err
}
previousVersion := env("SIM_FIRMWARE_CURRENT_VERSION", "0.2.0-sim")
if current != nil && (current.Phase == firmwareBootConfirmed || current.Phase == firmwareBootRolledBack) {
activeSlot = current.ActiveSlot
previousVersion = current.TargetVersion
}
state := simulatedFirmwareBootState{
Phase: firmwareBootStaged, CommandID: command.ID, ReleaseID: manifest.ReleaseID, TargetVersion: manifest.Version,
PreviousVersion: previousVersion, Digest: manifest.Digest, ActiveSlot: activeSlot, PreviousSlot: activeSlot, TargetSlot: inactiveFirmwareSlot(activeSlot),
}
if err := saveSimulatedFirmwareBootState(state); err != nil {
return firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(command, manifest, "staging", state, "INACTIVE_SLOT_STAGED"); err != nil {
return firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{
"phase": "staging", "progress_percent": 85, "result_code": "INACTIVE_SLOT_STAGED", "active_slot": state.ActiveSlot, "target_slot": state.TargetSlot,
}); err != nil {
return firmwareFailed, err
}
if fault == "power_loss_after_stage" {
if err := recordSimulatedFirmwareEvidence(command, manifest, "power_loss_after_stage", state, "SIMULATED_POWER_LOSS"); err != nil {
return firmwareFailed, err
}
return firmwareInterrupted, nil
}
return s.armSimulatedFirmwareBoot(ctx, command.ID, state)
}
func (s *simulator) armSimulatedFirmwareBoot(ctx context.Context, commandID string, state simulatedFirmwareBootState) (firmwareExecutionOutcome, error) {
window, err := simulatedFirmwareHealthWindow()
if err != nil {
return firmwareFailed, err
}
state.Phase = firmwareBootRebootPending
state.BootAttemptID = nonce()
state.HealthDeadline = time.Now().UTC().Add(window)
if err := saveSimulatedFirmwareBootState(state); err != nil {
return firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(simulatedDeviceCommand{ID: commandID}, simulatedFirmwareManifest{ReleaseID: state.ReleaseID, Version: state.TargetVersion, Digest: state.Digest}, "reboot_pending", state, "REBOOT_ARMED"); err != nil {
return firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, commandID, "ACKNOWLEDGED", map[string]any{
"phase": "reboot_pending", "progress_percent": 90, "result_code": "REBOOT_ARMED", "active_slot": state.ActiveSlot,
"target_slot": state.TargetSlot, "boot_attempt_id": state.BootAttemptID, "boot_health_deadline": state.HealthDeadline,
}); err != nil {
return firmwareFailed, err
}
return firmwareRebootRequired, nil
}
+105
View File
@@ -0,0 +1,105 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
"data-terminal-cloud/internal/firmwareconformance"
)
func recordSimulatedFirmwareEvidence(command simulatedDeviceCommand, manifest simulatedFirmwareManifest, phase string, state simulatedFirmwareBootState, resultCode string) error {
path := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_EVIDENCE_FILE"))
if path == "" {
return nil
}
now := time.Now().UTC()
var evidence firmwareconformance.DeviceEvidence
content, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
partitionDigest := strings.ToLower(env("SIM_PARTITION_TABLE_SHA256", firmwareconformance.SHA256([]byte(env("SIM_DEVICE_MODEL", "simulated-terminal")+":"+env("SIM_DEVICE_HARDWARE_REVISION", "sim-rev-a")+":A/B"))))
secureBoot, parseErr := simulatedEvidenceBoolean("SIM_SECURE_BOOT_ENABLED", false)
if parseErr != nil {
return parseErr
}
evidence = firmwareconformance.DeviceEvidence{
SchemaVersion: firmwareconformance.DeviceEvidenceSchema, EvidenceSource: "SIMULATOR",
CandidateVersion: env("SIM_CANDIDATE_VERSION", "local-development"), BuildDigest: strings.ToLower(strings.TrimSpace(os.Getenv("SIM_BUILD_DIGEST"))),
DeviceID: env("SIM_DEVICE_ID", "10000000-0000-0000-0000-000000000511"), DeviceModel: env("SIM_DEVICE_MODEL", "simulated-terminal"), HardwareRevision: env("SIM_DEVICE_HARDWARE_REVISION", "sim-rev-a"),
ReleaseID: manifest.ReleaseID, CommandID: command.ID, Scenario: strings.ToUpper(strings.TrimSpace(os.Getenv("SIM_FIRMWARE_EVIDENCE_SCENARIO"))),
BootloaderVersion: env("SIM_BOOTLOADER_VERSION", "sim-bootloader-1"), PartitionTableSHA256: partitionDigest, SecureBootEnabled: secureBoot,
ArtifactSHA256: manifest.Digest, SigningKeyID: manifest.SigningKeyID,
InitialSlot: state.PreviousSlot, TargetSlot: state.TargetSlot, FinalSlot: state.PreviousSlot,
InitialVersion: state.PreviousVersion, TargetVersion: state.TargetVersion, FinalVersion: state.PreviousVersion,
BootAttemptID: state.BootAttemptID, CollectedAt: now, Events: []firmwareconformance.DeviceEvent{},
}
if evidence.BuildDigest == "" || evidence.Scenario == "" {
return errors.New("SIM_BUILD_DIGEST and SIM_FIRMWARE_EVIDENCE_SCENARIO are required when evidence capture is enabled")
}
} else if err != nil {
return fmt.Errorf("read simulated firmware evidence: %w", err)
} else {
evidence, err = firmwareconformance.ParseDeviceEvidence(content)
if err != nil {
return err
}
if evidence.CommandID != command.ID || evidence.ReleaseID != manifest.ReleaseID {
return errors.New("simulated firmware evidence belongs to a different command or release")
}
}
if manifest.SigningKeyID != "" && evidence.SigningKeyID == "" {
evidence.SigningKeyID = manifest.SigningKeyID
}
if state.BootAttemptID != "" {
evidence.BootAttemptID = state.BootAttemptID
}
event := firmwareconformance.DeviceEvent{
Phase: phase, OccurredAt: now, ActiveSlot: state.ActiveSlot, TargetSlot: state.TargetSlot, ResultCode: resultCode, BootAttemptID: state.BootAttemptID,
}
evidence.Events = append(evidence.Events, event)
evidence.CollectedAt = now
switch phase {
case "confirmed":
evidence.FinalSlot = state.ActiveSlot
evidence.FinalVersion = manifest.Version
case "rolled_back":
evidence.FinalSlot = state.ActiveSlot
evidence.FinalVersion = state.TargetVersion
}
encoded, err := json.MarshalIndent(evidence, "", " ")
if err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("open simulated firmware evidence: %w", err)
}
if _, err = file.Write(encoded); err == nil {
err = file.Sync()
}
closeErr := file.Close()
if err != nil {
return fmt.Errorf("write simulated firmware evidence: %w", err)
}
if closeErr != nil {
return fmt.Errorf("close simulated firmware evidence: %w", closeErr)
}
return nil
}
func simulatedEvidenceBoolean(key string, fallback bool) (bool, error) {
value := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
if value == "" {
return fallback, nil
}
if value == "true" {
return true, nil
}
if value == "false" {
return false, nil
}
return false, fmt.Errorf("%s must be true or false", key)
}
+611
View File
@@ -0,0 +1,611 @@
package main
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"data-terminal-cloud/internal/deviceauth"
)
type simulator struct {
baseURL, deviceID string
secret []byte
client *http.Client
}
type apiResult struct {
Status int
Header http.Header
Body []byte
}
type envelope struct {
Data json.RawMessage `json:"data"`
}
type createdConversation struct {
ID string `json:"conversation_id"`
RTC struct {
RoomID string `json:"room_id"`
UserID string `json:"user_id"`
ExpiresAt time.Time `json:"expires_at"`
} `json:"rtc"`
}
type deviceConversationState struct {
Status string `json:"status"`
Action string `json:"action"`
FailureCode string `json:"failure_code"`
}
type uploadBatch struct {
ID string `json:"id"`
Status string `json:"status"`
Segments []struct {
ID string `json:"id"`
Status string `json:"status"`
UploadURL string `json:"upload_url"`
ContentType string `json:"content_type"`
LastErrorCode string `json:"last_error_code"`
LocalDeleteAfter *time.Time `json:"local_delete_after"`
} `json:"segments"`
}
func main() {
if err := run(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, "模拟设备失败:", err)
os.Exit(1)
}
}
func run(ctx context.Context) error {
secret := []byte(env("SIM_DEVICE_SECRET", "local-simulated-device-secret-32-bytes-minimum"))
if len(secret) < 32 {
return errors.New("SIM_DEVICE_SECRET must contain at least 32 characters")
}
sim := &simulator{
baseURL: strings.TrimRight(env("SIM_API_URL", "http://localhost:58080"), "/"),
deviceID: env("SIM_DEVICE_ID", "10000000-0000-0000-0000-000000000511"),
secret: secret, client: &http.Client{Timeout: 15 * time.Second},
}
scenarios := parseScenarios(env("SIM_SCENARIO", "bootstrap"))
if scenarios["offline"] {
if err := sim.offlineRetry(ctx); err != nil {
return err
}
}
if scenarios["bootstrap"] || scenarios["conversation"] || scenarios["all"] {
if err := sim.bootstrap(ctx); err != nil {
return err
}
}
conversationID := strings.TrimSpace(os.Getenv("SIM_CONVERSATION_ID"))
if scenarios["conversation"] || scenarios["all"] {
id, err := sim.conversation(ctx)
if err != nil {
return err
}
conversationID = id
}
if scenarios["transcript"] || scenarios["all"] {
if conversationID == "" {
return errors.New("transcript scenario requires conversation or SIM_CONVERSATION_ID")
}
if err := sim.transcript(ctx, conversationID); err != nil {
return err
}
}
if scenarios["telemetry"] || scenarios["all"] {
if err := sim.telemetry(ctx, conversationID); err != nil {
return err
}
}
if scenarios["commands"] || scenarios["all"] {
if err := sim.commands(ctx); err != nil {
return err
}
}
if scenarios["media"] {
if conversationID == "" {
return errors.New("media scenario requires SIM_CONVERSATION_ID")
}
if err := sim.media(ctx, conversationID); err != nil {
return err
}
}
return nil
}
func (s *simulator) bootstrap(ctx context.Context) error {
query := url.Values{
"firmware_version": {env("SIM_FIRMWARE_VERSION", "0.2.0-sim")}, "board_rev": {"sim-rev-a"}, "rtc_sdk_version": {"mock-rtc-1"},
"codec": {"opus"}, "sample_rate": {"16000"}, "network": {"simulated"},
"device_time": {time.Now().UTC().Format(time.RFC3339)},
}
first, err := s.request(ctx, http.MethodGet, "/device/v2/bootstrap?"+query.Encode(), nil, "")
if err != nil {
return err
}
var bootstrap struct {
Device struct {
FirmwareCompatible bool `json:"firmware_compatible"`
MinimumFirmwareVersion *string `json:"minimum_firmware_version"`
} `json:"device"`
}
if err := decodeData(first.Body, &bootstrap); err != nil {
return err
}
if !bootstrap.Device.FirmwareCompatible {
minimum := "当前策略"
if bootstrap.Device.MinimumFirmwareVersion != nil {
minimum = *bootstrap.Device.MinimumFirmwareVersion
}
return fmt.Errorf("firmware update required before conversation; minimum=%s", minimum)
}
second, err := s.request(ctx, http.MethodGet, "/device/v2/config", nil, "")
if err != nil {
return err
}
fmt.Printf("bootstrap HTTP %d; config HTTP %d\n", first.Status, second.Status)
return nil
}
func (s *simulator) offlineRetry(ctx context.Context) error {
offline := *s
offline.baseURL = "http://127.0.0.1:1"
offline.client = &http.Client{Timeout: 250 * time.Millisecond}
if _, err := offline.request(ctx, http.MethodGet, "/device/v2/config", nil, ""); err == nil {
return errors.New("offline simulation unexpectedly reached a server")
}
fmt.Println("offline request retained locally; reconnecting with a new nonce")
_, err := s.request(ctx, http.MethodGet, "/device/v2/config", nil, "")
return err
}
func (s *simulator) conversation(ctx context.Context) (string, error) {
createBody := jsonBody(map[string]any{
"device_time": time.Now().UTC(), "monotonic_ms": 1000,
"capabilities": map[string]any{
"rtc": true, "audio_tracks": []string{"near", "far"}, "upload_resume": true,
"rtc_credential_renewal": true,
},
"network": map[string]any{"type": "simulated", "rssi": -48}, "requested_goal": "open_conversation",
})
createKey := "sim-create-" + nonce()
createdResponse, err := s.request(ctx, http.MethodPost, "/device/v2/conversations", createBody, createKey)
if err != nil {
return "", err
}
var created createdConversation
if err := decodeData(createdResponse.Body, &created); err != nil {
return "", err
}
replay, err := s.request(ctx, http.MethodPost, "/device/v2/conversations", createBody, createKey)
if err != nil || replay.Header.Get("Idempotency-Replayed") != "true" {
return "", fmt.Errorf("create replay failed: %w", err)
}
activate := jsonBody(map[string]any{
"joined_at": time.Now().UTC(), "room_id": created.RTC.RoomID, "rtc_user_id": created.RTC.UserID,
"rtc_join_result": "success", "config_revision": 1, "connection_reference": "sim-device",
})
activationPath := "/device/v2/conversations/" + created.ID + "/activate"
if _, err := s.request(ctx, http.MethodPost, activationPath, activate, "sim-activate-"+nonce()); err != nil {
return "", err
}
if err := s.waitForConversationActive(ctx, created.ID, 15*time.Second); err != nil {
return "", err
}
heartbeat := jsonBody(map[string]any{
"sequence_no": 1, "device_time": time.Now().UTC(), "monotonic_ms": 2000, "conversation_status": "ACTIVE",
"rtc": map[string]any{"joined": true, "uplink": true, "downlink": true}, "network": map[string]any{"type": "simulated", "rssi": -50},
"firmware_version": "0.2.0-sim", "reported_config_revision": 1,
"rtc_credential_expires_at": created.RTC.ExpiresAt,
"recording_queue": map[string]any{"pending_segments": 2, "failed_segments": 0}, "command_results": []any{},
})
if _, err := s.request(ctx, http.MethodPost, "/device/v2/conversations/"+created.ID+"/heartbeat", heartbeat, ""); err != nil {
return "", err
}
stop := jsonBody(map[string]any{
"reason": "simulated_hangup", "device_time": time.Now().UTC(), "last_heartbeat_sequence": 1,
"rtc_left": true, "recording": map[string]any{"near_segments": 1, "far_segments": 1},
})
stopKey := "sim-stop-" + nonce()
if _, err := s.request(ctx, http.MethodPost, "/device/v2/conversations/"+created.ID+"/stop", stop, stopKey); err != nil {
return "", err
}
replay, err = s.request(ctx, http.MethodPost, "/device/v2/conversations/"+created.ID+"/stop", stop, stopKey)
if err != nil || replay.Header.Get("Idempotency-Replayed") != "true" {
return "", fmt.Errorf("stop replay failed: %w", err)
}
time.Sleep(2 * time.Second)
fmt.Println("conversation and repeated requests completed:", created.ID)
return created.ID, nil
}
func (s *simulator) waitForConversationActive(ctx context.Context, conversationID string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for {
statusResponse, err := s.request(ctx, http.MethodGet, "/device/v2/conversations/"+conversationID, nil, "")
if err != nil {
return err
}
var state deviceConversationState
if err := decodeData(statusResponse.Body, &state); err != nil {
return err
}
switch state.Action {
case "continue":
if state.Status != "ACTIVE" {
return fmt.Errorf("conversation returned continue in unexpected state %s", state.Status)
}
return nil
case "leave_room":
code := state.FailureCode
if code == "" {
code = strings.ToLower(state.Status)
}
return fmt.Errorf("conversation cannot start; leave RTC room: %s", code)
case "wait":
default:
return fmt.Errorf("conversation returned unsupported device action %q", state.Action)
}
if time.Now().After(deadline) {
return errors.New("provider did not activate the simulated conversation within 15 seconds")
}
if err := waitForContext(ctx, 500*time.Millisecond); err != nil {
return err
}
}
}
func (s *simulator) transcript(ctx context.Context, conversationID string) error {
now := time.Now().UTC()
body := jsonBody(map[string]any{"turns": []any{map[string]any{
"turn_key": "sim-final-" + nonce(), "sequence_no": 100, "speaker": "subject",
"start_at": now.Add(-2 * time.Second), "end_at": now,
"final_text": "这是模拟设备离线缓存后补交的最终字幕。", "interrupted": false,
}}})
key := "sim-transcript-" + nonce()
if _, err := s.request(ctx, http.MethodPost, "/device/v2/conversations/"+conversationID+"/turns:batch", body, key); err != nil {
return err
}
replay, err := s.request(ctx, http.MethodPost, "/device/v2/conversations/"+conversationID+"/turns:batch", body, key)
if err != nil || replay.Header.Get("Idempotency-Replayed") != "true" {
return fmt.Errorf("transcript replay failed: %w", err)
}
fmt.Println("transcript supplement and replay completed")
return nil
}
func (s *simulator) telemetry(ctx context.Context, conversationID string) error {
sequence := time.Now().UnixNano()
event := map[string]any{
"event_key": "sim-status-" + strconv.FormatInt(sequence, 10), "sequence_no": sequence,
"occurred_at": time.Now().UTC(), "event_type": "status",
"status": map[string]any{
"firmware_version": "0.2.0-sim", "reported_config_revision": 1,
"network": map[string]any{"type": "simulated", "rssi": -52},
"rtc": map[string]any{"joined": false, "uplink": false, "downlink": false},
"recording_queue": map[string]any{"pending_segments": 0, "failed_segments": 0}, "conversation_status": "COMPLETED",
},
}
if conversationID != "" {
event["conversation_id"] = conversationID
}
body := jsonBody(map[string]any{"events": []any{event}})
key := "sim-telemetry-" + nonce()
if _, err := s.request(ctx, http.MethodPost, "/device/v2/telemetry:batch", body, key); err != nil {
return err
}
replay, err := s.request(ctx, http.MethodPost, "/device/v2/telemetry:batch", body, key)
if err != nil || replay.Header.Get("Idempotency-Replayed") != "true" {
return fmt.Errorf("telemetry replay failed: %w", err)
}
fmt.Println("telemetry batch and replay completed")
return nil
}
func (s *simulator) commands(ctx context.Context) error {
response, err := s.request(ctx, http.MethodGet, "/device/v2/commands?limit=20", nil, "")
if err != nil {
return err
}
var data struct {
Items []simulatedDeviceCommand `json:"items"`
}
if err := decodeData(response.Body, &data); err != nil {
return err
}
completed, failed := 0, 0
for _, command := range data.Items {
if command.Type == "firmware_update" {
outcome, err := s.executeFirmwareCommand(ctx, command)
if err != nil {
return err
}
switch outcome {
case firmwareCompleted:
completed++
case firmwareFailed:
failed++
case firmwareInterrupted:
fmt.Printf("firmware command %s intentionally interrupted; server state retained for recovery\n", command.ID)
return nil
case firmwareRebootRequired:
fmt.Printf("firmware command %s armed the inactive slot; restart the simulator to perform boot health confirmation\n", command.ID)
return nil
}
continue
}
if err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{"result_code": "SIMULATED_OK"}); err != nil {
return err
}
completed++
}
fmt.Printf("processed %d pending commands: %d completed, %d simulated failures\n", len(data.Items), completed, failed)
return nil
}
func (s *simulator) media(ctx context.Context, conversationID string) error {
durationMS, err := strconv.Atoi(env("SIM_MEDIA_DURATION_MS", "100"))
if err != nil || durationMS < 1 || durationMS > 600000 {
return errors.New("SIM_MEDIA_DURATION_MS must be between 1 and 600000")
}
mediaPath := strings.TrimSpace(os.Getenv("SIM_MEDIA_FILE"))
content := syntheticWAV(durationMS)
if mediaPath != "" {
content, err = os.ReadFile(mediaPath)
if err != nil {
return fmt.Errorf("read simulated media file: %w", err)
}
}
digest := sha256.Sum256(content)
manifest := jsonBody(map[string]any{"segments": []any{map[string]any{
"track": env("SIM_MEDIA_TRACK", "near"), "sequence_no": int(time.Now().Unix() % 1000000000),
"codec": "wav_pcm_s16le", "content_type": "audio/wav", "duration_ms": durationMS,
"expected_size": len(content), "expected_digest": hex.EncodeToString(digest[:]),
}}})
prepared, err := s.request(ctx, http.MethodPost, "/device/v2/conversations/"+conversationID+"/upload-batches", manifest, "sim-media-"+nonce())
if err != nil {
return err
}
var batch uploadBatch
if err := decodeData(prepared.Body, &batch); err != nil {
return err
}
if len(batch.Segments) != 1 || batch.Segments[0].UploadURL == "" {
return errors.New("media preparation did not return one upload URL")
}
uploadRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, batch.Segments[0].UploadURL, bytes.NewReader(content))
if err != nil {
return err
}
uploadRequest.Header.Set("Content-Type", "audio/wav")
uploadResponse, err := s.client.Do(uploadRequest)
if err != nil {
return fmt.Errorf("upload simulated media: %w", err)
}
uploadResponse.Body.Close()
if uploadResponse.StatusCode < 200 || uploadResponse.StatusCode >= 300 {
return fmt.Errorf("upload simulated media returned HTTP %d", uploadResponse.StatusCode)
}
if _, err := s.request(ctx, http.MethodPost, "/device/v2/upload-batches/"+batch.ID+"/complete", []byte(`{}`), ""); err != nil {
return err
}
verifyTimeout, err := simulatorDuration("SIM_MEDIA_VERIFY_TIMEOUT", 20*time.Second, 5*time.Minute)
if err != nil {
return err
}
verified, err := s.waitForVerifiedMedia(ctx, batch.ID, verifyTimeout)
if err != nil {
return err
}
if verified.LocalDeleteAfter == nil {
return errors.New("verified media segment did not return local_delete_after")
}
deleteWaitMax, err := simulatorDuration("SIM_MEDIA_DELETE_WAIT_MAX", 30*time.Second, 10*time.Minute)
if err != nil {
return err
}
wait := time.Until(*verified.LocalDeleteAfter) + 100*time.Millisecond
if wait > deleteWaitMax {
fmt.Printf("media verified; local encrypted copy retained until %s (outside simulator wait window)\n", verified.LocalDeleteAfter.UTC().Format(time.RFC3339))
return nil
}
if wait > 0 {
if err := waitForContext(ctx, wait); err != nil {
return err
}
}
// The simulator only releases its in-memory copy. SIM_MEDIA_FILE is an operator-owned
// fixture and is intentionally never removed from disk.
content = nil
ack := jsonBody(map[string]any{"status": "DELETED"})
if _, err := s.request(ctx, http.MethodPost, "/device/v2/media-segments/"+verified.ID+"/local-delete-ack", ack, "sim-local-delete-"+nonce()); err != nil {
return err
}
fmt.Println("media verified and local encrypted copy deletion acknowledged:", batch.ID)
return nil
}
type verifiedMediaSegment struct {
ID string
LocalDeleteAfter *time.Time
}
func (s *simulator) waitForVerifiedMedia(ctx context.Context, batchID string, timeout time.Duration) (verifiedMediaSegment, error) {
deadline := time.Now().Add(timeout)
for {
response, err := s.request(ctx, http.MethodGet, "/device/v2/upload-batches/"+batchID, nil, "")
if err != nil {
return verifiedMediaSegment{}, err
}
var batch uploadBatch
if err := decodeData(response.Body, &batch); err != nil {
return verifiedMediaSegment{}, err
}
if batch.Status == "FAILED" {
code := "SEGMENT_REJECTED"
if len(batch.Segments) > 0 && batch.Segments[0].LastErrorCode != "" {
code = batch.Segments[0].LastErrorCode
}
return verifiedMediaSegment{}, fmt.Errorf("media verification failed: %s", code)
}
if batch.Status == "COMPLETED" {
for _, segment := range batch.Segments {
if segment.Status == "VERIFIED" && segment.ID != "" {
return verifiedMediaSegment{ID: segment.ID, LocalDeleteAfter: segment.LocalDeleteAfter}, nil
}
}
return verifiedMediaSegment{}, errors.New("completed media batch has no verified segment")
}
if time.Now().After(deadline) {
return verifiedMediaSegment{}, fmt.Errorf("media verification did not complete within %s", timeout)
}
if err := waitForContext(ctx, 300*time.Millisecond); err != nil {
return verifiedMediaSegment{}, err
}
}
}
func simulatorDuration(key string, fallback, maximum time.Duration) (time.Duration, error) {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback, nil
}
parsed, err := time.ParseDuration(value)
if err != nil || parsed <= 0 || parsed > maximum {
return 0, fmt.Errorf("%s must be a positive duration no greater than %s", key, maximum)
}
return parsed, nil
}
func waitForContext(ctx context.Context, duration time.Duration) error {
timer := time.NewTimer(duration)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func (s *simulator) request(ctx context.Context, method, path string, body []byte, key string) (apiResult, error) {
request, err := http.NewRequestWithContext(ctx, method, s.baseURL+path, bytes.NewReader(body))
if err != nil {
return apiResult{}, err
}
timestamp := time.Now().UTC().Format(time.RFC3339)
requestNonce, err := newNonce()
if err != nil {
return apiResult{}, err
}
hash := sha256.Sum256(body)
digest := hex.EncodeToString(hash[:])
request.Header.Set(deviceauth.HeaderDeviceID, s.deviceID)
request.Header.Set(deviceauth.HeaderTimestamp, timestamp)
request.Header.Set(deviceauth.HeaderNonce, requestNonce)
request.Header.Set(deviceauth.HeaderContentSHA256, digest)
request.Header.Set(deviceauth.HeaderSignature, deviceauth.SignatureHeader(s.secret, method, request.URL.RequestURI(), timestamp, requestNonce, digest))
if len(body) > 0 {
request.Header.Set("Content-Type", "application/json")
}
if key != "" {
request.Header.Set("Idempotency-Key", key)
}
response, err := s.client.Do(request)
if err != nil {
return apiResult{}, err
}
defer response.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if err != nil {
return apiResult{}, err
}
result := apiResult{Status: response.StatusCode, Header: response.Header.Clone(), Body: responseBody}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return result, fmt.Errorf("%s returned HTTP %d: %s", request.URL.Path, response.StatusCode, responseBody)
}
return result, nil
}
func decodeData(body []byte, target any) error {
var result envelope
if err := json.Unmarshal(body, &result); err != nil {
return err
}
return json.Unmarshal(result.Data, target)
}
func parseScenarios(value string) map[string]bool {
result := map[string]bool{}
for _, item := range strings.Split(strings.ToLower(value), ",") {
if item = strings.TrimSpace(item); item != "" {
result[item] = true
}
}
return result
}
func syntheticWAV(durationMS int) []byte {
samples := 16000 * durationMS / 1000
dataSize := samples * 2
buffer := bytes.NewBuffer(make([]byte, 0, 44+dataSize))
buffer.WriteString("RIFF")
_ = binary.Write(buffer, binary.LittleEndian, uint32(36+dataSize))
buffer.WriteString("WAVEfmt ")
_ = binary.Write(buffer, binary.LittleEndian, uint32(16))
_ = binary.Write(buffer, binary.LittleEndian, uint16(1))
_ = binary.Write(buffer, binary.LittleEndian, uint16(1))
_ = binary.Write(buffer, binary.LittleEndian, uint32(16000))
_ = binary.Write(buffer, binary.LittleEndian, uint32(32000))
_ = binary.Write(buffer, binary.LittleEndian, uint16(2))
_ = binary.Write(buffer, binary.LittleEndian, uint16(16))
buffer.WriteString("data")
_ = binary.Write(buffer, binary.LittleEndian, uint32(dataSize))
buffer.Write(make([]byte, dataSize))
return buffer.Bytes()
}
func jsonBody(value any) []byte {
body, err := json.Marshal(value)
if err != nil {
panic(err)
}
return body
}
func nonce() string {
value, err := newNonce()
if err != nil {
panic(err)
}
return value
}
func newNonce() (string, error) {
value := make([]byte, 16)
if _, err := rand.Read(value); err != nil {
return "", err
}
return hex.EncodeToString(value), nil
}
func env(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
+429
View File
@@ -0,0 +1,429 @@
package main
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"data-terminal-cloud/internal/deviceauth"
)
func TestSimulatorRequestSignsBodyAndReturnsEnvelope(t *testing.T) {
secret := []byte("simulator-request-secret-at-least-32-bytes")
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body, _ := io.ReadAll(request.Body)
digest := sha256.Sum256(body)
if request.Header.Get(deviceauth.HeaderDeviceID) != "device-1" || request.Header.Get(deviceauth.HeaderContentSHA256) != hex.EncodeToString(digest[:]) || request.Header.Get(deviceauth.HeaderSignature) == "" {
t.Errorf("invalid signed headers: %v", request.Header)
}
if request.Header.Get("Idempotency-Key") != "idem-1" {
t.Errorf("idempotency key=%q", request.Header.Get("Idempotency-Key"))
}
writer.Header().Set("ETag", `"3"`)
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, `{"data":{"status":"ok"}}`)
}))
defer server.Close()
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: secret, client: server.Client()}
result, err := sim.request(context.Background(), http.MethodPost, "/device/v2/test", []byte(`{"value":1}`), "idem-1")
if err != nil || result.Status != http.StatusOK || result.Header.Get("ETag") != `"3"` {
t.Fatalf("result=%+v err=%v", result, err)
}
var payload struct {
Status string `json:"status"`
}
if err := decodeData(result.Body, &payload); err != nil || payload.Status != "ok" {
t.Fatalf("payload=%+v err=%v", payload, err)
}
}
func TestSimulatorHelpersAndValidation(t *testing.T) {
scenarios := parseScenarios(" Bootstrap,TELEMETRY, bootstrap ")
if !scenarios["bootstrap"] || !scenarios["telemetry"] || len(scenarios) != 2 {
t.Fatalf("scenarios=%v", scenarios)
}
wav := syntheticWAV(100)
if string(wav[:4]) != "RIFF" || string(wav[8:12]) != "WAVE" || binary.LittleEndian.Uint32(wav[40:44]) != uint32(len(wav)-44) {
t.Fatalf("invalid WAV header: size=%d", len(wav))
}
if len(jsonBody(map[string]int{"value": 1})) == 0 {
t.Fatal("json body is empty")
}
if value, err := newNonce(); err != nil || len(value) != 32 {
t.Fatalf("nonce=%q err=%v", value, err)
}
t.Setenv("SIM_DEVICE_SECRET", "short")
if err := run(context.Background()); err == nil || !strings.Contains(err.Error(), "at least 32") {
t.Fatalf("short secret error=%v", err)
}
}
func TestCreatedConversationIgnoresEngineMode(t *testing.T) {
decode := func(mode string) createdConversation {
var created createdConversation
body := []byte(`{"data":{"conversation_id":"conversation-1","rtc":{"room_id":"room-1","user_id":"device-1","expires_at":"2026-08-23T12:05:00Z"},"session":{"engine_mode":"` + mode + `"}}}`)
if err := decodeData(body, &created); err != nil {
t.Fatalf("decode mode %q: %v", mode, err)
}
return created
}
for _, mode := range []string{"s2s_pure", "s2s_hybrid", "cascade", "future_mode"} {
created := decode(mode)
if created.ID != "conversation-1" || created.RTC.RoomID != "room-1" || created.RTC.UserID != "device-1" {
t.Fatalf("mode %q changed terminal media inputs: %#v", mode, created)
}
}
}
func TestSimulatorWaitForConversationState(t *testing.T) {
for _, test := range []struct {
name, response, wantError string
}{
{name: "active", response: `{"data":{"status":"ACTIVE","action":"continue"}}`},
{name: "failed", response: `{"data":{"status":"FAILED","action":"leave_room","failure_code":"provider_start_rejected"}}`, wantError: "provider_start_rejected"},
} {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet || request.URL.Path != "/device/v2/conversations/conversation-1" {
t.Errorf("unexpected request %s %s", request.Method, request.URL.Path)
}
writer.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(writer, test.response)
}))
defer server.Close()
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: []byte("simulator-request-secret-at-least-32-bytes"), client: server.Client()}
err := sim.waitForConversationActive(context.Background(), "conversation-1", time.Second)
if test.wantError == "" && err != nil {
t.Fatalf("wait failed: %v", err)
}
if test.wantError != "" && (err == nil || !strings.Contains(err.Error(), test.wantError)) {
t.Fatalf("error=%v want %q", err, test.wantError)
}
})
}
}
func TestSimulatorHTTPAndMediaFailures(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusConflict)
_, _ = io.WriteString(writer, `{"error":{"code":"CONFLICT"}}`)
}))
defer server.Close()
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: []byte("simulator-request-secret-at-least-32-bytes"), client: server.Client()}
if _, err := sim.request(context.Background(), http.MethodGet, "/conflict", nil, ""); err == nil || !strings.Contains(err.Error(), "HTTP 409") {
t.Fatalf("conflict error=%v", err)
}
t.Setenv("SIM_MEDIA_DURATION_MS", "0")
if err := sim.media(context.Background(), "conversation-1"); err == nil || !strings.Contains(err.Error(), "between 1 and 600000") {
t.Fatalf("media validation error=%v", err)
}
var target map[string]any
if err := decodeData([]byte(`not-json`), &target); err == nil {
t.Fatal("invalid envelope accepted")
}
encoded, _ := json.Marshal(map[string]any{"data": "not-an-object"})
if err := decodeData(encoded, &target); err == nil {
t.Fatal("invalid data payload accepted")
}
}
func TestSimulatorMediaCompletesVerificationAndDeleteReceipt(t *testing.T) {
deleteAcknowledged := false
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
switch {
case request.Method == http.MethodPost && request.URL.Path == "/device/v2/conversations/conversation-1/upload-batches":
_, _ = io.WriteString(writer, `{"data":{"id":"batch-1","status":"UPLOADING","segments":[{"id":"segment-1","upload_url":"`+server.URL+`/upload","content_type":"audio/wav"}]}}`)
case request.Method == http.MethodPut && request.URL.Path == "/upload":
writer.WriteHeader(http.StatusOK)
case request.Method == http.MethodPost && request.URL.Path == "/device/v2/upload-batches/batch-1/complete":
_, _ = io.WriteString(writer, `{"data":{"id":"batch-1","status":"VERIFYING","segments":[]}}`)
case request.Method == http.MethodGet && request.URL.Path == "/device/v2/upload-batches/batch-1":
deleteAfter := time.Now().Add(-time.Second).UTC().Format(time.RFC3339Nano)
_, _ = io.WriteString(writer, `{"data":{"id":"batch-1","status":"COMPLETED","segments":[{"id":"segment-1","status":"VERIFIED","local_delete_after":"`+deleteAfter+`"}]}}`)
case request.Method == http.MethodPost && request.URL.Path == "/device/v2/media-segments/segment-1/local-delete-ack":
if request.Header.Get("Idempotency-Key") == "" {
t.Error("local delete receipt has no idempotency key")
}
deleteAcknowledged = true
_, _ = io.WriteString(writer, `{"data":{"id":"segment-1","status":"VERIFIED","local_delete_status":"DELETED"}}`)
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
t.Setenv("SIM_MEDIA_VERIFY_TIMEOUT", "2s")
t.Setenv("SIM_MEDIA_DELETE_WAIT_MAX", "2s")
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: []byte("simulator-request-secret-at-least-32-bytes"), client: server.Client()}
if err := sim.media(context.Background(), "conversation-1"); err != nil {
t.Fatalf("media flow error=%v", err)
}
if !deleteAcknowledged {
t.Fatal("local delete receipt was not sent")
}
}
func TestSimulatorFirmwareCommandSuccess(t *testing.T) {
content := []byte("signed simulated firmware image")
payload := signedSimulatorFirmwarePayload(t, content, "2.3.1")
acks := []map[string]any{}
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
switch {
case request.Method == http.MethodGet && request.URL.Path == "/device/v2/firmware-releases/release-1/download":
_, _ = io.WriteString(writer, `{"data":{"download_url":"`+server.URL+`/firmware.bin"}}`)
case request.Method == http.MethodGet && request.URL.Path == "/firmware.bin":
writer.Header().Set("Content-Type", "application/octet-stream")
_, _ = writer.Write(content)
case request.Method == http.MethodPost && request.URL.Path == "/device/v2/commands/command-1/ack":
var body map[string]any
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Errorf("decode ack: %v", err)
}
acks = append(acks, body)
_, _ = io.WriteString(writer, `{"data":{"id":"command-1"}}`)
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: []byte("simulator-request-secret-at-least-32-bytes"), client: server.Client()}
command := simulatedDeviceCommand{ID: "command-1", Type: "firmware_update", Payload: payload}
outcome, err := sim.executeFirmwareCommand(context.Background(), command)
if err != nil || outcome != firmwareCompleted {
t.Fatalf("outcome=%q err=%v", outcome, err)
}
if len(acks) != 4 || acks[0]["status"] != "ACKNOWLEDGED" || acks[1]["status"] != "ACKNOWLEDGED" || acks[2]["status"] != "ACKNOWLEDGED" || acks[3]["status"] != "SUCCEEDED" {
t.Fatalf("acks=%+v", acks)
}
firstResult := acks[0]["result_summary"].(map[string]any)
downloadResult := acks[1]["result_summary"].(map[string]any)
installResult := acks[2]["result_summary"].(map[string]any)
finalResult := acks[3]["result_summary"].(map[string]any)
if firstResult["phase"] != "downloading" || downloadResult["result_code"] != "ARTIFACT_VERIFIED" || downloadResult["verified_signing_key_id"] != "sim-test-key" || installResult["phase"] != "installing" || finalResult["installed_version"] != "2.3.1" || finalResult["verified_sha256"] != payload["sha256"] {
t.Fatalf("firmware results=%+v", acks)
}
}
func TestSimulatorFirmwareCommandFaultsAndInterruptions(t *testing.T) {
content := []byte("simulated firmware fault fixture")
tests := []struct {
fault string
wantOutcome firmwareExecutionOutcome
wantStatuses []string
wantCode string
}{
{fault: "download_failed", wantOutcome: firmwareFailed, wantStatuses: []string{"ACKNOWLEDGED", "FAILED"}, wantCode: "DOWNLOAD_FAILED"},
{fault: "digest_mismatch", wantOutcome: firmwareFailed, wantStatuses: []string{"ACKNOWLEDGED", "FAILED"}, wantCode: "DIGEST_MISMATCH"},
{fault: "signature_mismatch", wantOutcome: firmwareFailed, wantStatuses: []string{"ACKNOWLEDGED", "FAILED"}, wantCode: "SIGNATURE_MISMATCH"},
{fault: "install_failed", wantOutcome: firmwareFailed, wantStatuses: []string{"ACKNOWLEDGED", "ACKNOWLEDGED", "ACKNOWLEDGED", "FAILED"}, wantCode: "INSTALL_FAILED"},
{fault: "interrupt_after_download", wantOutcome: firmwareInterrupted, wantStatuses: []string{"ACKNOWLEDGED", "ACKNOWLEDGED"}, wantCode: "ARTIFACT_VERIFIED"},
{fault: "interrupt_after_install", wantOutcome: firmwareInterrupted, wantStatuses: []string{"ACKNOWLEDGED", "ACKNOWLEDGED", "ACKNOWLEDGED"}, wantCode: "INSTALL_STARTED"},
}
for _, test := range tests {
t.Run(test.fault, func(t *testing.T) {
t.Setenv("SIM_FIRMWARE_FAULT", test.fault)
payload := signedSimulatorFirmwarePayload(t, content, "2.3.1")
acks := []map[string]any{}
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
switch {
case request.Method == http.MethodGet && strings.HasSuffix(request.URL.Path, "/download"):
_, _ = io.WriteString(writer, `{"data":{"download_url":"`+server.URL+`/firmware.bin"}}`)
case request.Method == http.MethodGet && request.URL.Path == "/firmware.bin":
_, _ = writer.Write(content)
case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/ack"):
var body map[string]any
_ = json.NewDecoder(request.Body).Decode(&body)
acks = append(acks, body)
_, _ = io.WriteString(writer, `{"data":{"id":"command-1"}}`)
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: []byte("simulator-request-secret-at-least-32-bytes"), client: server.Client()}
command := simulatedDeviceCommand{ID: "command-1", Type: "firmware_update", Payload: payload}
outcome, err := sim.executeFirmwareCommand(context.Background(), command)
if err != nil || outcome != test.wantOutcome {
t.Fatalf("outcome=%q err=%v", outcome, err)
}
statuses := make([]string, 0, len(acks))
for _, ack := range acks {
statuses = append(statuses, ack["status"].(string))
}
if strings.Join(statuses, ",") != strings.Join(test.wantStatuses, ",") {
t.Fatalf("statuses=%v want=%v", statuses, test.wantStatuses)
}
lastResult := acks[len(acks)-1]["result_summary"].(map[string]any)
if lastResult["result_code"] != test.wantCode {
t.Fatalf("last result=%+v", lastResult)
}
})
}
}
func TestSimulatorFirmwareManifestAndConfigurationValidation(t *testing.T) {
digest := strings.Repeat("a", 64)
valid := map[string]any{"release_id": "release-1", "version": "2.3.1", "sha256": digest, "signature": base64.StdEncoding.EncodeToString(make([]byte, ed25519.SignatureSize)), "signing_key_id": "sim-test-key", "download_path": "/device/v2/firmware-releases/release-1/download"}
if _, err := parseSimulatedFirmwareManifest(valid); err != nil {
t.Fatalf("valid manifest error=%v", err)
}
for name, payload := range map[string]map[string]any{
"external path": {"release_id": "release-1", "version": "2.3.1", "sha256": digest, "signature": valid["signature"], "signing_key_id": "sim-test-key", "download_path": "https://example.invalid/firmware"},
"traversal": {"release_id": "release-1", "version": "2.3.1", "sha256": digest, "signature": valid["signature"], "signing_key_id": "sim-test-key", "download_path": "/device/v2/firmware-releases/../download"},
"uppercase": {"release_id": "release-1", "version": "2.3.1", "sha256": strings.Repeat("A", 64), "signature": valid["signature"], "signing_key_id": "sim-test-key", "download_path": "/device/v2/firmware-releases/release-1/download"},
} {
t.Run(name, func(t *testing.T) {
if _, err := parseSimulatedFirmwareManifest(payload); err == nil {
t.Fatalf("invalid manifest accepted: %+v", payload)
}
})
}
t.Setenv("SIM_FIRMWARE_FAULT", "unknown")
if _, err := simulatedFirmwareFault(); err == nil {
t.Fatal("unknown firmware fault was accepted")
}
t.Setenv("SIM_FIRMWARE_STEP_DELAY", "31s")
if _, err := simulatorOptionalDuration("SIM_FIRMWARE_STEP_DELAY", 30*time.Second); err == nil {
t.Fatal("excessive firmware step delay was accepted")
}
t.Setenv("SIM_FIRMWARE_TRUSTED_KEYS", `{}`)
if _, err := simulatedFirmwareTrustedKey("sim-test-key"); err == nil {
t.Fatal("unprovisioned firmware signing key was accepted")
}
t.Setenv("SIM_FIRMWARE_AB_UPDATE", "yes")
if _, err := simulatedABFirmwareEnabled(); err == nil {
t.Fatal("invalid A/B firmware flag was accepted")
}
t.Setenv("SIM_FIRMWARE_AB_UPDATE", "true")
t.Setenv("SIM_FIRMWARE_STATE_FILE", "relative-state.json")
if _, err := simulatedABFirmwareEnabled(); err == nil {
t.Fatal("relative A/B firmware state path was accepted")
}
}
func TestSimulatorABFirmwareBootConfirmationAndRollback(t *testing.T) {
for _, test := range []struct {
name, stageFault, bootFault, wantOutcome, wantPhase, wantSlot, wantCode string
retryAfterBoot bool
wantAckCount int
}{
{name: "healthy boot", stageFault: "none", bootFault: "none", wantOutcome: string(firmwareCompleted), wantPhase: firmwareBootConfirmed, wantSlot: "B", wantCode: "SIMULATED_AB_BOOT_CONFIRMED", wantAckCount: 7},
{name: "failed boot rolls back", stageFault: "none", bootFault: "boot_health_failed", wantOutcome: string(firmwareFailed), wantPhase: firmwareBootRolledBack, wantSlot: "A", wantCode: "BOOT_HEALTH_FAILED_ROLLED_BACK", wantAckCount: 7},
{name: "power loss after staging resumes", stageFault: "power_loss_after_stage", bootFault: "none", wantOutcome: string(firmwareCompleted), wantPhase: firmwareBootConfirmed, wantSlot: "B", wantCode: "SIMULATED_AB_BOOT_CONFIRMED", wantAckCount: 7},
{name: "power loss during boot resumes", stageFault: "none", bootFault: "power_loss_during_boot", retryAfterBoot: true, wantOutcome: string(firmwareCompleted), wantPhase: firmwareBootConfirmed, wantSlot: "B", wantCode: "SIMULATED_AB_BOOT_CONFIRMED", wantAckCount: 8},
} {
t.Run(test.name, func(t *testing.T) {
content := []byte("simulated A/B firmware image")
payload := signedSimulatorFirmwarePayload(t, content, "3.1.0")
stateFile := filepath.Join(t.TempDir(), "firmware-state.json")
t.Setenv("SIM_FIRMWARE_AB_UPDATE", "true")
t.Setenv("SIM_FIRMWARE_STATE_FILE", stateFile)
t.Setenv("SIM_FIRMWARE_BOOT_HEALTH_WINDOW", "30s")
t.Setenv("SIM_FIRMWARE_CURRENT_VERSION", "0.2.0-sim")
t.Setenv("SIM_FIRMWARE_FAULT", test.stageFault)
acks := []map[string]any{}
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
switch {
case request.Method == http.MethodGet && strings.HasSuffix(request.URL.Path, "/download"):
_, _ = io.WriteString(writer, `{"data":{"download_url":"`+server.URL+`/firmware.bin"}}`)
case request.Method == http.MethodGet && request.URL.Path == "/firmware.bin":
_, _ = writer.Write(content)
case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/ack"):
var body map[string]any
_ = json.NewDecoder(request.Body).Decode(&body)
acks = append(acks, body)
_, _ = io.WriteString(writer, `{"data":{"id":"command-ab"}}`)
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
sim := simulator{baseURL: server.URL, deviceID: "device-1", secret: []byte("simulator-request-secret-at-least-32-bytes"), client: server.Client()}
command := simulatedDeviceCommand{ID: "command-ab", Type: "firmware_update", Payload: payload}
outcome, err := sim.executeFirmwareCommand(context.Background(), command)
if err != nil {
t.Fatalf("staging outcome=%q err=%v", outcome, err)
}
if test.stageFault == "power_loss_after_stage" {
if outcome != firmwareInterrupted {
t.Fatalf("power loss staging outcome=%q", outcome)
}
staged, stateErr := loadSimulatedFirmwareBootState()
if stateErr != nil || staged.Phase != firmwareBootStaged {
t.Fatalf("power loss staged state=%+v err=%v", staged, stateErr)
}
t.Setenv("SIM_FIRMWARE_FAULT", "none")
outcome, err = sim.executeFirmwareCommand(context.Background(), command)
}
if err != nil || outcome != firmwareRebootRequired {
t.Fatalf("reboot arming outcome=%q err=%v", outcome, err)
}
staged, err := loadSimulatedFirmwareBootState()
if err != nil || staged.Phase != firmwareBootRebootPending || staged.ActiveSlot != "A" || staged.TargetSlot != "B" || staged.BootAttemptID == "" {
t.Fatalf("staged state=%+v err=%v", staged, err)
}
t.Setenv("SIM_FIRMWARE_FAULT", test.bootFault)
outcome, err = sim.executeFirmwareCommand(context.Background(), command)
if test.retryAfterBoot {
if err != nil || outcome != firmwareInterrupted {
t.Fatalf("boot power loss outcome=%q err=%v", outcome, err)
}
t.Setenv("SIM_FIRMWARE_FAULT", "none")
outcome, err = sim.executeFirmwareCommand(context.Background(), command)
}
if err != nil || string(outcome) != test.wantOutcome {
t.Fatalf("boot outcome=%q err=%v", outcome, err)
}
finalState, err := loadSimulatedFirmwareBootState()
if err != nil || finalState.Phase != test.wantPhase || finalState.ActiveSlot != test.wantSlot {
t.Fatalf("final state=%+v err=%v", finalState, err)
}
if len(acks) != test.wantAckCount {
t.Fatalf("ack count=%d acks=%+v", len(acks), acks)
}
lastResult := acks[len(acks)-1]["result_summary"].(map[string]any)
if lastResult["result_code"] != test.wantCode {
t.Fatalf("last result=%+v", lastResult)
}
})
}
}
func signedSimulatorFirmwarePayload(t *testing.T, content []byte, version string) map[string]any {
t.Helper()
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
digest := sha256.Sum256(content)
digestText := hex.EncodeToString(digest[:])
manifest := fmt.Sprintf("firmware-manifest-v1\nmodel=simulated-terminal\nhardware_revision=sim-rev-a\nversion=%s\nsha256=%s\nsize=%d\n", version, digestText, len(content))
trustedKeys, _ := json.Marshal(map[string]string{"sim-test-key": base64.StdEncoding.EncodeToString(publicKey)})
t.Setenv("SIM_FIRMWARE_TRUSTED_KEYS", string(trustedKeys))
return map[string]any{
"release_id": "release-1", "version": version, "sha256": digestText,
"signature": base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, []byte(manifest))),
"signing_key_id": "sim-test-key", "download_path": "/device/v2/firmware-releases/release-1/download",
}
}