Initial commit: smartrun MVP
Garmin run classification and progression tracker. Go backend (MCP client to mcp-garmin, SQLite store, deterministic rule engine, REST API) and React/TS frontend (Dashboard, Review Queue, Workout Kinds). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
61
backend/internal/classify/rule_test.go
Normal file
61
backend/internal/classify/rule_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package classify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNode_ValidateAcceptsWellFormedTree(t *testing.T) {
|
||||
n := Node{Match: MatchAll, Conditions: []Node{
|
||||
{Metric: "avg_pace_sec_per_km", Op: OpBetween, Value: []any{270.0, 300.0}},
|
||||
{Match: MatchAny, Conditions: []Node{
|
||||
{Metric: "aerobic_training_effect", Op: OpGte, Value: 3.5},
|
||||
{Metric: "lap_interval_pattern", Op: OpEq, Value: true},
|
||||
}},
|
||||
}}
|
||||
if err := n.Validate(); err != nil {
|
||||
t.Errorf("Validate() = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_ValidateRejectsMalformed(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
n Node
|
||||
}{
|
||||
{"unknown match", Node{Match: "xor", Conditions: []Node{{Metric: "x", Op: OpGt, Value: 1.0}}}},
|
||||
{"branch with no conditions", Node{Match: MatchAll}},
|
||||
{"leaf with no metric", Node{Op: OpGt, Value: 1.0}},
|
||||
{"between with non-array value", Node{Metric: "x", Op: OpBetween, Value: 5.0}},
|
||||
{"between with wrong-length array", Node{Metric: "x", Op: OpBetween, Value: []any{1.0}}},
|
||||
{"unsupported op", Node{Metric: "x", Op: "~=", Value: 1.0}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if err := c.n.Validate(); err == nil {
|
||||
t.Error("Validate() = nil, want an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_RoundTripsThroughJSON(t *testing.T) {
|
||||
raw := `{
|
||||
"match": "all",
|
||||
"conditions": [
|
||||
{"metric": "avg_pace_sec_per_km", "op": "between", "value": [270, 300]},
|
||||
{"metric": "lap_interval_pattern", "op": "==", "value": true}
|
||||
]
|
||||
}`
|
||||
var n Node
|
||||
if err := json.Unmarshal([]byte(raw), &n); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if err := n.Validate(); err != nil {
|
||||
t.Fatalf("Validate() after round-trip = %v", err)
|
||||
}
|
||||
matched, _ := n.Evaluate(MetricContext{"avg_pace_sec_per_km": 285, "lap_interval_pattern": 1})
|
||||
if !matched {
|
||||
t.Error("expected match after JSON round-trip")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user