Files
HeatGuard/internal/action/selector_test.go
vikingowl 1c9db02334 feat: add web UI with full CRUD setup page
Add server-side rendered setup UI accessible via `heatwave web`.
The dashboard is now re-rendered per request and includes a nav bar
linking to the new /setup page. Setup provides full CRUD for profiles,
rooms, devices, occupants, AC units (with room assignment), scenario
toggles, and forecast fetching — all via POST/redirect/GET forms.

- Add ShowNav field to DashboardData for conditional nav bar
- Extract fetchForecastForProfile() for reuse by web handler
- Create setup.html.tmpl with Tailwind-styled entity sections
- Create web_handlers.go with 15 route handlers and flash cookies
- Switch web.go from pre-rendered to per-request dashboard rendering
- Graceful dashboard fallback when no forecast data exists
2026-02-09 10:39:00 +01:00

101 lines
3.0 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 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)
}
}