the suspend contract, and the row that was waiting for it (V-561)

The parseable twin of the owner's transcript goes green and loses its skip: Rome
is answered, the question survives the side query on the same attempt, and the
answer after it completes the reminder he actually asked for.

His transcript verbatim stays skipped, and V-561 was never going to unskip it.
What is left there is the parser — StubDateTimeParser reads neither "на 9" nor
"на завтра", so the third turn lands as an answer that filled nothing. The skip
reason now names V-543 and V-562 instead of this task.

Two V-560 tests asserted the drop notice and now assert the suspend: nothing
says a request was let go, the reply ends with the resumed question, and the
parked question is still there on attempt 1 with what it was about intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 01:23:29 +04:00
parent c8a5b5416e
commit 9ed259660d
5 changed files with 63 additions and 30 deletions
+23 -6
View File
@@ -167,8 +167,12 @@ func TestTurnRoleNamesACorrection(t *testing.T) {
// TestRomeIsAnsweredAndTheReminderIsNotInvented — the measured failure of
// 2026-08-05, end to end through the real cascade. "напомни позвонить маме"
// parks the time question; the weather question that follows must not become
// its answer, must not create a reminder for a time nobody asked for, and must
// not be dropped in silence.
// its answer and must not create a reminder for a time nobody asked for.
//
// V-560 got that far by DROPPING the parked request and saying so, and the
// owner rejected the notice on sight: he did not ask to lose the reminder. So
// the contract here is V-561's — the flow is suspended, this turn's reply ends
// with the question coming back, and nothing says anything was let go.
func TestRomeIsAnsweredAndTheReminderIsNotInvented(t *testing.T) {
ctx := context.Background()
h, st := newRoutingClarifyHandler(t)
@@ -180,14 +184,27 @@ func TestRomeIsAnsweredAndTheReminderIsNotInvented(t *testing.T) {
if strings.Contains(reply, "напомню") {
t.Fatalf("the question was eaten as the reminder's time again: %q", reply)
}
if !strings.HasPrefix(reply, clarifyDropped) {
t.Fatalf("the parked request died without a word: %q", reply)
if strings.Contains(reply, clarifyDropped) {
t.Fatalf("a side query suspends the flow; nothing was dropped, so nothing may say so: %q", reply)
}
resumed, _ := clarifyResumedFor(dialogue.SlotTime)
if !strings.HasSuffix(reply, resumed) {
t.Fatalf("the reply must end with the resumed question %q, got %q", resumed, reply)
}
if reminders, err := st.DueReminders(ctx, h.now().Add(48*time.Hour)); err != nil || len(reminders) != 0 {
t.Fatalf("a reminder was invented for a time nobody asked for: %v err=%v", reminders, err)
}
if h.clarifyStore.Get(dialogueIDFor(sourceText, "web"), h.now()) != nil {
t.Fatal("the parked question must be gone, not left to eat the next turn")
// Still parked, and still on its first attempt: he answered the side query,
// not this question, so no retry may have been spent on it.
q := h.clarifyStore.Get(dialogueIDFor(sourceText, "web"), h.now())
if q == nil {
t.Fatal("the parked question was dropped instead of suspended")
}
if q.Attempts != 1 {
t.Fatalf("the side query spent a clarify attempt: attempts = %d, want 1", q.Attempts)
}
if !strings.Contains(q.Utterance, "маме") {
t.Fatalf("the suspended request lost what it was about: %q", q.Utterance)
}
}