spaxel/test/acceptance/as3_fall_test.go
jedarden 386820e7b7 test: implement acceptance scenario integration tests (AS-1 through AS-6)
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>
2026-05-05 07:38:16 -04:00

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)
}