package dialogue import ( "testing" "time" ) func parked(text string, asked time.Time) *PendingQuestion { return &PendingQuestion{ Intent: IntentReminder, Missing: []Slot{SlotTime}, Utterance: text, Asked: asked, TTL: time.Minute, } } func TestStackPushPeekPop(t *testing.T) { s := NewClarifyStore(time.Minute) if dropped := s.Push("voice", parked("напомни позвонить маме", pendingBase)); dropped != nil { t.Fatalf("first push dropped %q", dropped.Utterance) } if dropped := s.Push("voice", parked("погода в риме", pendingBase)); dropped != nil { t.Fatalf("second push dropped %q", dropped.Utterance) } if got := s.Depth("voice"); got != 2 { t.Fatalf("depth = %d, want 2", got) } if got := s.Peek("voice", pendingBase); got == nil || got.Utterance != "погода в риме" { t.Fatalf("peek = %+v, want the newest", got) } // Peek must not consume: two peeks are the same question. if got := s.Peek("voice", pendingBase); got == nil || got.Utterance != "погода в риме" { t.Fatalf("second peek = %+v, want the newest still", got) } got := s.Pop("voice", pendingBase) if got == nil || got.Utterance != "погода в риме" { t.Fatalf("pop = %+v, want the newest", got) } // The flow underneath survived the one on top of it. if got := s.Peek("voice", pendingBase); got == nil || got.Utterance != "напомни позвонить маме" { t.Fatalf("after pop, peek = %+v, want the suspended flow", got) } if got := s.Pop("voice", pendingBase); got == nil { t.Fatal("pop of the last entry returned nil") } if s.Peek("voice", pendingBase) != nil || s.Depth("voice") != 0 { t.Error("stack not empty after popping everything") } if s.Pop("voice", pendingBase) != nil { t.Error("pop of an empty stack returned something") } } // A popped entry is gone: it must not come back on the next peek. func TestStackPoppedEntryIsGone(t *testing.T) { s := NewClarifyStore(time.Minute) s.Push("voice", parked("напомни", pendingBase)) s.Pop("voice", pendingBase) if got := s.Peek("voice", pendingBase); got != nil { t.Errorf("peek after pop = %+v, want nil", got) } } // Past MaxStackDepth the oldest entry comes back to the caller instead of // vanishing — it is the caller's job to say it was dropped. func TestStackDepthBoundReturnsTheDroppedEntry(t *testing.T) { s := NewClarifyStore(time.Minute) for i := 0; i < MaxStackDepth; i++ { if dropped := s.Push("voice", parked("first", pendingBase)); dropped != nil { t.Fatalf("push %d dropped early", i) } } dropped := s.Push("voice", parked("newest", pendingBase)) if dropped == nil { t.Fatal("push past the bound dropped an entry silently") } if dropped.Utterance != "first" { t.Errorf("dropped %q, want the oldest", dropped.Utterance) } if got := s.Depth("voice"); got != MaxStackDepth { t.Errorf("depth = %d, want %d", got, MaxStackDepth) } if got := s.Peek("voice", pendingBase); got == nil || got.Utterance != "newest" { t.Errorf("peek = %+v, want the newest", got) } } // Put still replaces rather than stacks: a re-ask is another question about the // same action, so the daemon's depth stays one. func TestPutReplacesTopWithoutGrowing(t *testing.T) { s := NewClarifyStore(time.Minute) s.Put("voice", parked("напомни", pendingBase)) s.Put("voice", parked("напомни ещё раз", pendingBase)) if got := s.Depth("voice"); got != 1 { t.Fatalf("depth = %d, want 1", got) } if got := s.Peek("voice", pendingBase); got == nil || got.Utterance != "напомни ещё раз" { t.Fatalf("peek = %+v, want the replacement", got) } } // An expired top takes the stack with it, and TakeExpired is what reports it — // the whole exchange timed out, and one notice is what a reply can carry. func TestStackExpiryDropsTheStackAndIsReported(t *testing.T) { s := NewClarifyStore(time.Minute) s.Push("voice", parked("напомни", pendingBase)) s.Push("voice", parked("погода", pendingBase)) late := pendingBase.Add(2 * time.Minute) if s.Peek("voice", late) != nil { t.Error("peek returned an expired question") } if s.Depth("voice") != 0 { t.Error("expired stack survived a peek") } s.Push("voice", parked("напомни", pendingBase)) s.Push("voice", parked("погода", pendingBase)) // Two died, and the count says two: the notice that reports this has a // plural wording since V-561, and it is chosen off this number. if n := s.TakeExpired("voice", late); n != 2 { t.Errorf("TakeExpired reported %d timed-out questions, want 2", n) } if s.Depth("voice") != 0 { t.Error("TakeExpired left entries behind") } if n := s.TakeExpired("voice", late); n != 0 { t.Errorf("TakeExpired reported twice: %d", n) } // Pop of an expired top yields nothing rather than a dead action. s.Push("voice", parked("напомни", pendingBase)) if s.Pop("voice", late) != nil { t.Error("pop returned an expired question") } } // Delete ends the exchange, every level of it. func TestStackDeleteDropsAll(t *testing.T) { s := NewClarifyStore(time.Minute) s.Push("voice", parked("напомни", pendingBase)) s.Push("voice", parked("погода", pendingBase)) s.Delete("voice") if s.Depth("voice") != 0 || s.Peek("voice", pendingBase) != nil { t.Error("Delete left questions parked") } } // Stacks are per dialogue id: the mic and the web must not read each other's. func TestStacksAreIsolatedByID(t *testing.T) { s := NewClarifyStore(time.Minute) s.Push("voice", parked("напомни", pendingBase)) s.Push("web", parked("погода", pendingBase)) s.Delete("voice") if got := s.Peek("web", pendingBase); got == nil || got.Utterance != "погода" { t.Errorf("web stack = %+v, want its own question", got) } }