Count the rides a question takes, without the reset (V-663)

MaxSuspends did not move the number it was written for. Twenty-six of 140
turns carried a parked clarify tail before it landed and twenty-six after.

Two bounds rearm each other. An aside spends no attempt, so MaxAttempts
never reaches it. A turn reading as a failed answer zeroes Suspends, so
MaxSuspends never reaches the asides. Alternating them restores each bound
with the other's traffic. Measured on 2026-08-08: one question about a
reminder's day rode turns 7 to 13.

PendingQuestion.Rides is the same event counted without the resets. Set
once, incremented only in noteSuspended, carried across the re-park in
askRemainingGap, read by nothing that could lower it. MaxRides is 4, one
looser than MaxSuspends so the tighter statement about a run stays
reachable.

It ends the measured ride one turn early and no more. Most of that ride is
attempts, spent because classifyTurnRole reads "спасибо" and "привет" as
failed answers. Said so in the constant and in the design doc rather than
claiming a fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
This commit is contained in:
2026-08-08 21:38:32 +04:00
parent bbefda66e2
commit de9884e063
4 changed files with 147 additions and 3 deletions
+64
View File
@@ -372,4 +372,68 @@ func TestAnAnsweredGapResetsTheSuspendBudget(t *testing.T) {
if q.Suspends != 0 {
t.Fatalf("answering a gap must reset the suspend budget: suspends = %d", q.Suspends)
}
// The ride it already took is carried across the re-park (V-663). Resetting
// both counters here is what let one question ride twenty-six replies.
if q.Rides != 1 {
t.Fatalf("the aside it already took was forgotten: rides = %d", q.Rides)
}
}
// TestTwoBoundsCannotRearmEachOther — V-663.
//
// MaxSuspends landed and the measurement did not move: twenty-six of 140 turns
// carried a tail before it and twenty-six after. This is the shape it misses,
// taken from the 2026-08-08 run, where one question rode turns 7 to 13.
//
// An aside spends no attempt, so MaxAttempts never reaches it. A turn that
// reads as a failed answer zeroes Suspends, so MaxSuspends never reaches the
// asides either. Alternating the two rearms each bound with the other's
// traffic. Rides counts both kinds and is never reset, so it is what ends this.
func TestTwoBoundsCannotRearmEachOther(t *testing.T) {
ctx := context.Background()
h, _ := newRoutingClarifyHandler(t)
id := dialogueIDFor(sourceText, "web")
resumed, _ := clarifyResumedFor(dialogue.SlotTime)
if reply := h.handleText(ctx, "web", "напомни позвонить маме"); !strings.Contains(reply, "?") {
t.Fatalf("expected the time question, got %q", reply)
}
// Two asides. Each one rides and neither spends an attempt.
for i := 0; i < 2; i++ {
reply := h.handleText(ctx, "web", "какие у меня напоминания?")
if !strings.HasSuffix(reply, resumed) {
t.Fatalf("aside %d: the question must come back, got %q", i+1, reply)
}
}
q := h.clarifyStore.Get(id, h.now())
if q == nil || q.Rides != 2 || q.Suspends != 2 {
t.Fatalf("after two asides: %+v", q)
}
// A failed answer. It spends an attempt and puts Suspends back to zero, so
// the old bound starts over. Rides does not.
h.handleText(ctx, "web", "спасибо")
if q = h.clarifyStore.Get(id, h.now()); q == nil || q.Suspends != 0 {
t.Fatalf("the failed answer did not rearm the old bound: %+v", q)
}
if q.Rides != 2 {
t.Fatalf("the rides it already took were forgotten: %+v", q)
}
// Three more asides. Suspends only reaches three, one short of firing, and
// would go back to zero on the next pleasantry. Rides passes MaxRides and
// the request goes, out loud.
h.handleText(ctx, "web", "какие у меня напоминания?")
h.handleText(ctx, "web", "какие у меня напоминания?")
reply := h.handleText(ctx, "web", "какие у меня напоминания?")
if !strings.Contains(reply, clarifyDropped) {
t.Fatalf("the question rode four asides and was let go in silence: %q", reply)
}
if strings.HasSuffix(reply, resumed) {
t.Fatalf("a question she has let go must not be asked again: %q", reply)
}
if h.clarifyStore.Get(id, h.now()) != nil {
t.Fatal("the question must be gone once she has said she let it go")
}
}