Files
HeatGuard/internal/action/selector_test.go
vikingowl 21154d5d7f feat: add heating support with heat pump modeling and cold risk detection
Model heating mode when rooms have net heat loss in cold weather (<10°C).
AC units with heat pump capability (canHeat) provide heating capacity,
with the same 20% headroom threshold used for cooling. Adds cold risk
detection, cold-weather actions, and full frontend support including
heating mode timeline colors, room budget heating display, and i18n.
2026-02-11 00:00:43 +01:00

161 lines
4.5 KiB
Go

package action
import (
"testing"
"github.com/cnachtigall/heatwave-autopilot/internal/heat"
"github.com/cnachtigall/heatwave-autopilot/internal/risk"
)
func TestMatches_TempThreshold(t *testing.T) {
a := Action{When: TimeCondition{MinTempC: 30}}
if Matches(a, HourContext{TempC: 35}) != true {
t.Error("should match at 35C")
}
if Matches(a, HourContext{TempC: 25}) != false {
t.Error("should not match at 25C")
}
}
func TestMatches_HourRange(t *testing.T) {
a := Action{When: TimeCondition{HourFrom: 6, HourTo: 9}}
if Matches(a, HourContext{Hour: 7}) != true {
t.Error("should match at hour 7")
}
if Matches(a, HourContext{Hour: 12}) != false {
t.Error("should not match at hour 12")
}
}
func TestMatches_NightOnly(t *testing.T) {
a := Action{When: TimeCondition{NightOnly: true}}
if Matches(a, HourContext{IsDay: false}) != true {
t.Error("should match at night")
}
if Matches(a, HourContext{IsDay: true}) != false {
t.Error("should not match during day")
}
}
func TestMatches_MinRisk(t *testing.T) {
a := Action{When: TimeCondition{MinRisk: "high"}}
if Matches(a, HourContext{RiskLevel: risk.High}) != true {
t.Error("should match High")
}
if Matches(a, HourContext{RiskLevel: risk.Extreme}) != true {
t.Error("should match Extreme")
}
if Matches(a, HourContext{RiskLevel: risk.Moderate}) != false {
t.Error("should not match Moderate")
}
if Matches(a, HourContext{RiskLevel: risk.Low}) != false {
t.Error("should not match Low")
}
}
func TestMatches_BudgetStatus(t *testing.T) {
a := Action{When: TimeCondition{BudgetStatus: "marginal"}}
if Matches(a, HourContext{BudgetStatus: heat.Marginal}) != true {
t.Error("should match Marginal")
}
if Matches(a, HourContext{BudgetStatus: heat.Overloaded}) != true {
t.Error("should match Overloaded")
}
if Matches(a, HourContext{BudgetStatus: heat.Comfortable}) != false {
t.Error("should not match Comfortable")
}
}
func TestMatches_HighHumidity(t *testing.T) {
a := Action{When: TimeCondition{HighHumidity: true, MinTempC: 26}}
if Matches(a, HourContext{HumidityPct: 80, TempC: 30}) != true {
t.Error("should match at 80% humidity")
}
if Matches(a, HourContext{HumidityPct: 50, TempC: 30}) != false {
t.Error("should not match at 50% humidity")
}
}
func TestMatches_MaxTempThreshold(t *testing.T) {
a := Action{When: TimeCondition{MaxTempC: 5}}
if Matches(a, HourContext{TempC: 3}) != true {
t.Error("should match at 3C (below max 5)")
}
if Matches(a, HourContext{TempC: 10}) != false {
t.Error("should not match at 10C (above max 5)")
}
}
func TestMatches_NegativeMaxTemp(t *testing.T) {
a := Action{When: TimeCondition{MaxTempC: -5}}
if Matches(a, HourContext{TempC: -8}) != true {
t.Error("should match at -8C (below max -5)")
}
if Matches(a, HourContext{TempC: 0}) != false {
t.Error("should not match at 0C (above max -5)")
}
}
func TestMatches_NegativeMinTemp(t *testing.T) {
a := Action{When: TimeCondition{MinTempC: -10}}
if Matches(a, HourContext{TempC: -5}) != true {
t.Error("should match at -5C (above min -10)")
}
if Matches(a, HourContext{TempC: -15}) != false {
t.Error("should not match at -15C (below min -10)")
}
}
func TestSelectActions_ColdActions(t *testing.T) {
actions, err := LoadDefaultActions()
if err != nil {
t.Fatalf("LoadDefaultActions: %v", err)
}
ctx := HourContext{TempC: -8, Hour: 12, IsDay: true}
result := SelectActions(actions, ctx)
var ids []string
for _, a := range result {
ids = append(ids, a.ID)
}
wantIDs := map[string]bool{
"close_windows_cold": true,
"use_heating": true,
"check_insulation": true,
}
for id := range wantIDs {
found := false
for _, got := range ids {
if got == id {
found = true
break
}
}
if !found {
t.Errorf("expected action %q in results, got %v", id, ids)
}
}
}
func TestSelectActions_SortedByPriority(t *testing.T) {
actions := []Action{
{ID: "low_impact_high_effort", Impact: ImpactLow, Effort: EffortHigh},
{ID: "high_impact_no_effort", Impact: ImpactHigh, Effort: EffortNone},
{ID: "med_impact_low_effort", Impact: ImpactMedium, Effort: EffortLow},
}
ctx := HourContext{TempC: 35, Hour: 12, IsDay: true}
result := SelectActions(actions, ctx)
if len(result) != 3 {
t.Fatalf("len = %d, want 3", len(result))
}
if result[0].ID != "high_impact_no_effort" {
t.Errorf("first = %s, want high_impact_no_effort", result[0].ID)
}
if result[1].ID != "med_impact_low_effort" {
t.Errorf("second = %s, want med_impact_low_effort", result[1].ID)
}
if result[2].ID != "low_impact_high_effort" {
t.Errorf("third = %s, want low_impact_high_effort", result[2].ID)
}
}