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