mavweb: add rule trace page at /trace
New template showing the most recent tick's rule evaluation results with color-coded table, expandable gate detail panels, nav links to dash and history. Uses the TickTrace IPC method from #15.
This commit is contained in:
@@ -55,6 +55,10 @@ type fakeCore struct {
|
||||
// for handleHistory tests
|
||||
historyFacts []ipc.Fact
|
||||
historyErr error
|
||||
|
||||
// for handleTrace tests
|
||||
tickTrace ipc.TickTrace
|
||||
traceErr error
|
||||
}
|
||||
|
||||
func (f *fakeCore) EnableTool(_ context.Context, name string, cmd []string, destructive bool, scope string, _ time.Time) error {
|
||||
@@ -131,6 +135,13 @@ func (f *fakeCore) RevertFact(_ context.Context, _ string) (int64, error) {
|
||||
return f.revertNewID, nil
|
||||
}
|
||||
|
||||
func (f *fakeCore) TickTrace(_ context.Context) (ipc.TickTrace, error) {
|
||||
if f.traceErr != nil {
|
||||
return ipc.TickTrace{}, f.traceErr
|
||||
}
|
||||
return f.tickTrace, nil
|
||||
}
|
||||
|
||||
// --- GET ---
|
||||
|
||||
func TestHandleTools_GET_RendersAndEscapes(t *testing.T) {
|
||||
@@ -578,6 +589,88 @@ func TestHandleHistory(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// --- handleTrace ---
|
||||
|
||||
func TestHandleTrace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("nil core returns 503", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
handleTrace(rr, httptest.NewRequest(http.MethodGet, "/trace", nil), nil)
|
||||
if rr.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("status = %d, want 503", rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("core TickTrace error returns 502", func(t *testing.T) {
|
||||
core := &fakeCore{traceErr: ipc.ErrNoFact}
|
||||
rr := httptest.NewRecorder()
|
||||
handleTrace(rr, httptest.NewRequest(http.MethodGet, "/trace", nil), core)
|
||||
if rr.Code != http.StatusBadGateway {
|
||||
t.Errorf("status = %d, want 502", rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("renders template with trace data", func(t *testing.T) {
|
||||
now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC)
|
||||
snooze := time.Date(2025, 6, 1, 13, 0, 0, 0, time.UTC)
|
||||
core := &fakeCore{
|
||||
tickTrace: ipc.TickTrace{
|
||||
Now: now,
|
||||
Winner: "win-rule",
|
||||
Rules: []ipc.RuleTrace{
|
||||
{
|
||||
RuleName: "win-rule",
|
||||
Severity: 5,
|
||||
PredicateResult: true,
|
||||
GateResult: true,
|
||||
GateDetail: ipc.GateDetail{Presence: "present"},
|
||||
WasSelected: true,
|
||||
},
|
||||
{
|
||||
RuleName: "lose-rule",
|
||||
Severity: 3,
|
||||
PredicateResult: true,
|
||||
GateResult: false,
|
||||
GateBlockedBy: "quiet_hours",
|
||||
GateDetail: ipc.GateDetail{
|
||||
QuietHours: true,
|
||||
Presence: "away",
|
||||
SnoozeUntil: &snooze,
|
||||
},
|
||||
WasSelected: false,
|
||||
LostTo: "win-rule",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
handleTrace(rr, httptest.NewRequest(http.MethodGet, "/trace", nil), core)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := rr.Body.String()
|
||||
if !strings.Contains(body, "win-rule") {
|
||||
t.Error("rendered output missing winning rule name")
|
||||
}
|
||||
if !strings.Contains(body, "lose-rule") {
|
||||
t.Error("rendered output missing losing rule name")
|
||||
}
|
||||
if !strings.Contains(body, "quiet_hours") {
|
||||
t.Error("rendered output missing gate blocked by")
|
||||
}
|
||||
if !strings.Contains(body, "5") {
|
||||
t.Error("rendered output missing severity")
|
||||
}
|
||||
if !strings.Contains(body, "3") {
|
||||
t.Error("rendered output missing severity for second rule")
|
||||
}
|
||||
if strings.Contains(body, "nothing fired") {
|
||||
t.Error("rendered 'nothing fired' but a winner was set")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- handleRevert ---
|
||||
|
||||
func TestHandleRevert(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user