/trace grows a turn-decisions table under the rule trace (V-564)
Both tables answer the same question, who won and who lost, one about nudges and the other about utterances, so they share a page rather than splitting the nav. A turn is one collapsible row; never_asked is coloured like a block, because it usually is one. A read failure is logged and the rule trace above it still renders: a daemon too old to know the method is the ordinary case during a rolling deploy.
This commit is contained in:
@@ -65,6 +65,7 @@ type fakeCore struct {
|
||||
// for handleTrace tests
|
||||
tickTrace ipc.TickTrace
|
||||
traceErr error
|
||||
turns []ipc.TurnDecision
|
||||
|
||||
// for handleChatAPI tests
|
||||
chatText string
|
||||
@@ -173,6 +174,10 @@ func (f *fakeCore) RevertFact(_ context.Context, key string) (int64, error) {
|
||||
return f.revertNewID, nil
|
||||
}
|
||||
|
||||
func (f *fakeCore) TurnDecisions(_ context.Context, _ int) ([]ipc.TurnDecision, error) {
|
||||
return f.turns, nil
|
||||
}
|
||||
|
||||
func (f *fakeCore) TickTrace(_ context.Context) (ipc.TickTrace, error) {
|
||||
if f.traceErr != nil {
|
||||
return ipc.TickTrace{}, f.traceErr
|
||||
@@ -825,6 +830,41 @@ func TestHandleTrace(t *testing.T) {
|
||||
t.Error("rendered 'nothing fired' but a winner was set")
|
||||
}
|
||||
})
|
||||
|
||||
// The turn arbitration shares this page (V-564). A reader must see the
|
||||
// winner, a loser and the claimants that were never asked, because the last
|
||||
// of those is what the hardcoded ordering hides.
|
||||
t.Run("renders the turn decision record", func(t *testing.T) {
|
||||
core := &fakeCore{turns: []ipc.TurnDecision{{
|
||||
Ts: time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC),
|
||||
Utterance: "какая погода в риме",
|
||||
Winner: "query:weather",
|
||||
Claims: []ipc.TurnClaim{
|
||||
{Stage: "query", Claimant: "weather", Intent: "query", Outcome: "won"},
|
||||
{Stage: "query", Claimant: "calendar", Outcome: "declined", Reason: "no answer"},
|
||||
{Stage: "query", Claimant: "kiwix", Outcome: "never_asked"},
|
||||
},
|
||||
}}}
|
||||
rr := httptest.NewRecorder()
|
||||
handleTrace(rr, httptest.NewRequest(http.MethodGet, "/trace", nil), core)
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{"какая погода в риме", "query:weather", "calendar", "kiwix", "never_asked"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("rendered page is missing %q", want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no turns renders the empty note, not an error", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
handleTrace(rr, httptest.NewRequest(http.MethodGet, "/trace", nil), &fakeCore{})
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "no turn has run") {
|
||||
t.Error("empty ring did not render its note")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- handleRevert ---
|
||||
|
||||
+18
-1
@@ -1415,12 +1415,29 @@ func handleTrace(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
http.Error(w, "core read failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
// The turn records share this page rather than getting one of their own
|
||||
// (V-564): both answer the same question — who won, who lost and why — and
|
||||
// one is about nudges while the other is about utterances. A read failure
|
||||
// here is not fatal to the page: the rule trace above it still renders, and
|
||||
// a daemon too old to know the method is the ordinary case during a rolling
|
||||
// deploy.
|
||||
turns, err := core.TurnDecisions(ctx, 25)
|
||||
if err != nil {
|
||||
log.Printf("trace: turn decisions: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := traceTmpl.Execute(w, trace); err != nil {
|
||||
if err := traceTmpl.Execute(w, traceData{Tick: trace, Turns: turns}); err != nil {
|
||||
log.Printf("trace render: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// traceData — what trace.html renders: the last tick's rule arbitration and the
|
||||
// last turns' claim arbitration.
|
||||
type traceData struct {
|
||||
Tick ipc.TickTrace
|
||||
Turns []ipc.TurnDecision
|
||||
}
|
||||
|
||||
func handleMorning(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
if core == nil {
|
||||
http.Error(w, "morning disabled (no -core)", http.StatusServiceUnavailable)
|
||||
|
||||
+22
-2
@@ -1,9 +1,9 @@
|
||||
{{template "shellTop" "trace"}}
|
||||
<h1>Rule Trace</h1>
|
||||
<div class="hint mb-4">{{.Now | ago}} — winner: <strong>{{if .Winner}}{{.Winner}}{{else}}nothing fired{{end}}</strong></div>
|
||||
<div class="hint mb-4">{{.Tick.Now | ago}} — winner: <strong>{{if .Tick.Winner}}{{.Tick.Winner}}{{else}}nothing fired{{end}}</strong></div>
|
||||
<div class=scroll><table class=mono>
|
||||
<tr><th>rule<th>sev<th>predicate<th>gate<th>blocked by<th>detail<th>selected<th>lost to</tr>
|
||||
{{range .Rules}}<tr>
|
||||
{{range .Tick.Rules}}<tr>
|
||||
<td>{{.RuleName}}</td>
|
||||
<td>{{.Severity}}</td>
|
||||
<td class={{if .PredicateResult}}green{{else}}gray{{end}}>{{.PredicateResult}}</td>
|
||||
@@ -21,5 +21,25 @@
|
||||
<td>{{.LostTo}}</td>
|
||||
</tr>{{end}}
|
||||
</table></div>
|
||||
|
||||
<h1 class=mt-4>Turn Decisions</h1>
|
||||
<div class="hint mb-4">Who claimed each utterance, who lost it, and who was never asked. In memory, newest first, cleared on restart.</div>
|
||||
{{if not .Turns}}<div class=hint>no turn has run since the daemon started</div>{{end}}
|
||||
{{range .Turns}}
|
||||
<details class=mb-4>
|
||||
<summary><span class=mono>{{.Utterance}}</span> — <strong>{{if .Winner}}{{.Winner}}{{else}}nobody{{end}}</strong> <span class=hint>{{.Ts | ago}}</span></summary>
|
||||
<div class=scroll><table class=mono>
|
||||
<tr><th>stage<th>claimant<th>would have been<th>score<th>outcome<th>why</tr>
|
||||
{{range .Claims}}<tr>
|
||||
<td>{{.Stage}}</td>
|
||||
<td>{{.Claimant}}</td>
|
||||
<td>{{if .Intent}}{{.Intent}}{{else}}—{{end}}</td>
|
||||
<td>{{if .HasScore}}{{printf "%.3f" .Score}}{{else}}—{{end}}</td>
|
||||
<td class={{if eq .Outcome "won"}}green{{else if eq .Outcome "never_asked"}}red{{else}}gray{{end}}>{{.Outcome}}</td>
|
||||
<td>{{.Reason}}</td>
|
||||
</tr>{{end}}
|
||||
</table></div>
|
||||
</details>
|
||||
{{end}}
|
||||
{{template "shellBottom"}}
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user