package iot import ( "context" "strings" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/bus" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/urgency" ) // captureUpdates 订阅 DeviceUpdateSubject 并把收到的载荷送进返回的通道。 func captureUpdates(t *testing.T, m *Manager) <-chan DeviceUpdate { t.Helper() ch := make(chan DeviceUpdate, 8) unsub, err := m.Bus().Subscribe(context.Background(), DeviceUpdateSubject, func(_ context.Context, e bus.Event) error { if d, ok := e.Payload.(DeviceUpdate); ok { ch <- d } return nil }) if err != nil { t.Fatalf("Subscribe: %v", err) } t.Cleanup(unsub) return ch } // TestControl_UnlockIsCriticalSafetyEvent 验证门锁开启被标成安全域: // 这是 IoT 侧唯一的严重度升级出处,也是 quiet hours 豁免的生产端。 func TestControl_UnlockIsCriticalSafetyEvent(t *testing.T) { t.Parallel() m, err := New(config.IoTConfig{Provider: "mock"}, nil) if err != nil { t.Fatalf("New: %v", err) } updates := captureUpdates(t, m) if _, err := m.Control(context.Background(), "lock.front", "unlock", nil); err != nil { t.Fatalf("Control: %v", err) } got := recvIoT(t, updates, 2*time.Second) if got.Severity != SeverityCritical { t.Fatalf("Severity = %v, want %v (an unlocked door is a safety event)", got.Severity, SeverityCritical) } if !strings.Contains(got.Message, "安全域") { t.Fatalf("Message = %q, want it to name the safety domain", got.Message) } } // TestControl_RoutineStaysRoutine 是保守默认的反面断言:常规设备变化(以及 // 锁门这类非安全域动作)绝不被升级——安静时段照旧扣下它们。 func TestControl_RoutineStaysRoutine(t *testing.T) { t.Parallel() cases := []struct { name string device string action string }{ {name: "开灯", device: "light.living", action: "on"}, {name: "空调设定温度", device: "ac.living", action: "set_temperature"}, {name: "锁门", device: "lock.front", action: "lock"}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() m, err := New(config.IoTConfig{Provider: "mock"}, nil) if err != nil { t.Fatalf("New: %v", err) } updates := captureUpdates(t, m) params := map[string]any{"temperature": 25} if _, err := m.Control(context.Background(), tc.device, tc.action, params); err != nil { t.Fatalf("Control: %v", err) } got := recvIoT(t, updates, 2*time.Second) if got.Severity != SeverityRoutine { t.Fatalf("Severity = %v, want %v (routine transitions must stay routine)", got.Severity, SeverityRoutine) } if strings.Contains(got.Message, "安全域") { t.Fatalf("Message = %q, want no safety-domain wording", got.Message) } }) } } // TestAlertSeverity_String 验证严重度标签经通用词汇转发(日志与记录依赖它), // 且 IoT 侧的类型就是通用紧急度的别名 —— 它没有自己的专有严重度体系。 func TestAlertSeverity_String(t *testing.T) { t.Parallel() cases := []AlertSeverity{SeverityRoutine, SeverityWarning, SeverityCritical, AlertSeverity(7)} for _, in := range cases { if got, want := in.String(), urgency.Level(int(in)).String(); got != want { t.Fatalf("AlertSeverity(%d).String() = %q, want the generic label %q", int(in), got, want) } } if got := SeverityCritical.String(); got != "critical" { t.Fatalf("SeverityCritical.String() = %q, want %q", got, "critical") } } // TestDeviceUpdate_DeclaresUrgencyOnGenericChannel 钉住 IoT 走的是**通用**通道: // 设备跃迁载荷实现 urgency.Declarer,backthink 因此不必认识 IoT 的专有字段; // 未标注严重度的跃迁声明零值 routine(默认保守)。 func TestDeviceUpdate_DeclaresUrgencyOnGenericChannel(t *testing.T) { t.Parallel() var declared urgency.Declarer = DeviceUpdate{Severity: SeverityCritical} if got := declared.Urgency(); got != urgency.LevelCritical { t.Fatalf("DeviceUpdate.Urgency() = %v, want %v", got, urgency.LevelCritical) } if got := (DeviceUpdate{}).Urgency(); got != urgency.LevelRoutine { t.Fatalf("unlabelled DeviceUpdate.Urgency() = %v, want %v", got, urgency.LevelRoutine) } } // TestSafetySeverity_RuleTable 直接钉住规则表:未登记的设备类型与动作一律不升级。 func TestSafetySeverity_RuleTable(t *testing.T) { t.Parallel() cases := []struct { name string deviceType string action string wantSev AlertSeverity wantHit bool }{ {name: "门锁开启_命中", deviceType: "lock", action: "unlock", wantSev: SeverityCritical, wantHit: true}, {name: "门锁锁闭_不命中", deviceType: "lock", action: "lock", wantSev: SeverityRoutine, wantHit: false}, {name: "门锁翻转_不命中", deviceType: "lock", action: "toggle", wantSev: SeverityRoutine, wantHit: false}, {name: "灯_不命中", deviceType: "light", action: "on", wantSev: SeverityRoutine, wantHit: false}, {name: "传感器_不命中", deviceType: "sensor", action: "unlock", wantSev: SeverityRoutine, wantHit: false}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() sev, hit := safetySeverity(tc.deviceType, tc.action) if hit != tc.wantHit || sev != tc.wantSev { t.Fatalf("safetySeverity(%q, %q) = (%v, %v), want (%v, %v)", tc.deviceType, tc.action, sev, hit, tc.wantSev, tc.wantHit) } }) } }