8015fdbb79
Replace nearest-neighbour personal routing with a frozen class-balanced linear head measured on historical, stratified, cross-validation, holdout, and fresh challenge gates (V-702). Close the four repair handoff holes, preserve nested clarification flows, and route Russian possession statements through structural grammar rather than lexical exceptions (V-573). Owner explicitly requested direct commits to master.
174 lines
6.2 KiB
Go
174 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
func TestCompleteTopKeepsSuspendedFlow(t *testing.T) {
|
|
s := NewClarifyStore(time.Minute)
|
|
bottom := parked("напомни", pendingBase)
|
|
top := parked("погода", pendingBase)
|
|
s.Push("voice", bottom)
|
|
s.Push("voice", top)
|
|
|
|
completed, resumed := s.CompleteTop("voice", pendingBase)
|
|
if completed != top || resumed != bottom {
|
|
t.Fatalf("completed=%p resumed=%p, want top=%p bottom=%p", completed, resumed, top, bottom)
|
|
}
|
|
if got := s.Depth("voice"); got != 1 {
|
|
t.Fatalf("depth=%d, want one surviving flow", 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)
|
|
}
|
|
}
|