Implemented all 6 acceptance scenarios as verifiable integration tests: - AS-1: First-time setup in under 5 minutes - AS-2: Person detected while walking - AS-3: Fall alert fires correctly - AS-4: BLE identity resolves to person name - AS-5: OTA update succeeds / rollback on bad firmware - AS-6: Replay shows recorded history Each scenario includes multiple test cases covering pass/fail criteria. Tests use spaxel-sim as the test harness for simulating CSI data without hardware. The integration test entry point runs all scenarios sequentially for CI/CD verification. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
166 lines
4.2 KiB
Go
166 lines
4.2 KiB
Go
// Package acceptance provides AS-3: Fall alert fires correctly.
|
|
//
|
|
// Pass criteria:
|
|
// - spaxel-sim --scenario fall runs with falling walker
|
|
// - Fall alert appears in /api/events within 30 seconds
|
|
// - Alert has severity="critical" or "warning"
|
|
// - Webhook is called with fall alert payload
|
|
//
|
|
// Fail criteria:
|
|
// - No fall alert generated
|
|
// - Alert severity is incorrect
|
|
// - Webhook not called
|
|
package acceptance
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAS3_FallDetection verifies fall detection and alerting.
|
|
func TestAS3_FallDetection(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping acceptance test in short mode")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
|
|
defer cancel()
|
|
|
|
h := NewTestHarness(t)
|
|
defer h.Stop()
|
|
|
|
// Start webhook server to receive alerts
|
|
webhookURL := h.StartWebhookServer()
|
|
t.Logf("Webhook server started: %s", webhookURL)
|
|
|
|
// Start mothership with webhook URL
|
|
if err := h.Start(ctx); err != nil {
|
|
t.Fatalf("Failed to start mothership: %v", err)
|
|
}
|
|
|
|
// Set PIN
|
|
if err := h.SetPIN(ctx, "1234"); err != nil {
|
|
t.Fatalf("Failed to set PIN: %v", err)
|
|
}
|
|
|
|
// Run simulator with fall scenario
|
|
simCtx, simCancel := context.WithTimeout(ctx, 90*time.Second)
|
|
defer simCancel()
|
|
if err := h.RunSimulator(simCtx, []string{
|
|
"--nodes", "2",
|
|
"--walkers", "1",
|
|
"--duration", "0", // Run until cancelled
|
|
"--scenario", "fall",
|
|
"--fall-delay", "5s",
|
|
"--fall-duration", "800ms",
|
|
"--stillness", "15s",
|
|
}); err != nil {
|
|
t.Fatalf("Failed to start simulator: %v", err)
|
|
}
|
|
|
|
// Wait for fall alert
|
|
t.Run("FallAlertGenerated", func(t *testing.T) {
|
|
fallAlert, err := h.WaitForEvent(ctx, "fall_alert", 45*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Fall alert not detected within 45 seconds: %v", err)
|
|
}
|
|
|
|
if fallAlert["type"] != "fall_alert" {
|
|
t.Errorf("Expected event type=fall_alert, got %v", fallAlert["type"])
|
|
}
|
|
|
|
// Check severity
|
|
if severity, ok := fallAlert["severity"].(string); ok {
|
|
if severity != "critical" && severity != "warning" {
|
|
t.Errorf("Expected severity=critical or warning, got %v", severity)
|
|
}
|
|
} else {
|
|
t.Error("Missing severity field in fall alert")
|
|
}
|
|
|
|
t.Logf("AS-3: Fall alert detected: %+v", fallAlert)
|
|
})
|
|
|
|
// Verify webhook was called
|
|
t.Run("WebhookCalled", func(t *testing.T) {
|
|
// Wait a moment for webhook to be called
|
|
time.Sleep(2 * time.Second)
|
|
|
|
if !h.WebhookCalled() {
|
|
t.Error("Webhook was not called for fall alert")
|
|
} else {
|
|
t.Log("AS-3: Webhook called successfully")
|
|
}
|
|
})
|
|
|
|
// Also check for FallDetected event type
|
|
t.Run("FallDetectedEvent", func(t *testing.T) {
|
|
events, err := h.GetEvents(ctx, "FallDetected", 5)
|
|
if err != nil {
|
|
t.Logf("Failed to get FallDetected events: %v", err)
|
|
return
|
|
}
|
|
|
|
if len(events) > 0 {
|
|
t.Logf("AS-3: Found %d FallDetected events", len(events))
|
|
}
|
|
})
|
|
|
|
t.Log("AS-3: Fall detection test completed")
|
|
}
|
|
|
|
// TestAS3_FallAlertSeverity verifies fall alert has correct severity.
|
|
func TestAS3_FallAlertSeverity(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping acceptance test in short mode")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
|
|
defer cancel()
|
|
|
|
h := NewTestHarness(t)
|
|
defer h.Stop()
|
|
|
|
// Start mothership
|
|
if err := h.Start(ctx); err != nil {
|
|
t.Fatalf("Failed to start mothership: %v", err)
|
|
}
|
|
|
|
// Set PIN
|
|
if err := h.SetPIN(ctx, "1234"); err != nil {
|
|
t.Fatalf("Failed to set PIN: %v", err)
|
|
}
|
|
|
|
// Run simulator with fall scenario
|
|
simCtx, simCancel := context.WithTimeout(ctx, 90*time.Second)
|
|
defer simCancel()
|
|
if err := h.RunSimulator(simCtx, []string{
|
|
"--nodes", "2",
|
|
"--walkers", "1",
|
|
"--duration", "0", // Run until cancelled
|
|
"--scenario", "fall",
|
|
"--fall-delay", "5s",
|
|
"--stillness", "10s",
|
|
}); err != nil {
|
|
t.Fatalf("Failed to start simulator: %v", err)
|
|
}
|
|
|
|
// Wait for fall alert
|
|
fallAlert, err := h.WaitForEvent(ctx, "fall_alert", 45*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Fall alert not detected: %v", err)
|
|
}
|
|
|
|
// Verify severity
|
|
severity, ok := fallAlert["severity"].(string)
|
|
if !ok {
|
|
t.Fatal("Missing severity field in fall alert")
|
|
}
|
|
|
|
if severity != "critical" && severity != "warning" {
|
|
t.Errorf("Expected severity=critical or warning, got %v", severity)
|
|
}
|
|
|
|
t.Logf("AS-3: Fall alert severity verified: %s", severity)
|
|
}
|