Files
Cyrene_v2/internal/iot/safety_test.go
T
AskaEth f91a83ff81
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(urgency): 通用紧急度通道(Declarer/Level + 只抬不压折算)+ IoT 首个生产者 + 她自己的结论走结构化标记穿透安静时段
2026-10-05 13:11:04 +08:00

147 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
})
}
}