package dialogue import ( "testing" "time" ) var pendingBase = time.Date(2026, 8, 6, 9, 0, 0, 0, time.UTC) func TestCapabilityForCoversEveryIntent(t *testing.T) { for _, in := range []Intent{ IntentAct, IntentReminder, IntentFact, IntentNote, IntentQuery, IntentChat, IntentSystem, } { if CapabilityFor(in) == "" { t.Errorf("intent %q maps to no capability", in) } } if got := CapabilityFor(Intent("nonsense")); got != "" { t.Errorf("unknown intent became capability %q, want empty", got) } } // A parked question must read as the action it is assembling, without the // caller having to name the capability twice. func TestPendingQuestionActionDerivesCapability(t *testing.T) { q := &PendingQuestion{ Intent: IntentReminder, Slots: Slots{Text: "позвонить маме"}, Missing: []Slot{SlotTime}, Utterance: "напомни позвонить маме", Asked: pendingBase, TTL: time.Minute, Attempts: 1, MaxAttempts: 2, } a := q.Action() if a.Capability != CapReminderCreate { t.Errorf("capability = %q, want %q", a.Capability, CapReminderCreate) } if a.Utterance != q.Utterance || a.Attempts != 1 || a.MaxAttempts != 2 || !a.Asked.Equal(pendingBase) { t.Errorf("action did not carry the question's fields: %+v", a) } } func TestPendingActionGaps(t *testing.T) { for _, tc := range []struct { name string action PendingAction want []Slot complete bool }{ { name: "time still missing", action: PendingAction{Missing: []Slot{SlotTime}, Slots: Slots{Text: "позвонить маме"}}, want: []Slot{SlotTime}, complete: false, }, { name: "asked slot now filled", action: PendingAction{Missing: []Slot{SlotTime}, Slots: Slots{Time: pendingBase, HasTime: true}}, want: nil, complete: true, }, { name: "two gaps reported in ask order", action: PendingAction{Missing: []Slot{SlotText, SlotTime}}, want: []Slot{SlotText, SlotTime}, complete: false, }, { name: "nothing asked is complete", action: PendingAction{}, want: nil, complete: true, }, } { t.Run(tc.name, func(t *testing.T) { got := tc.action.Gaps() if len(got) != len(tc.want) { t.Fatalf("gaps = %v, want %v", got, tc.want) } for i := range got { if got[i] != tc.want[i] { t.Fatalf("gaps = %v, want %v", got, tc.want) } } if tc.action.Complete() != tc.complete { t.Errorf("Complete() = %v, want %v", tc.action.Complete(), tc.complete) } }) } } // The typed action must answer the TTL and attempt-cap questions the same way // the parked question always did — this is a widening, not new behaviour. func TestPendingActionTTLAndAttempts(t *testing.T) { a := PendingAction{Asked: pendingBase, TTL: time.Minute} if a.IsExpired(pendingBase.Add(30 * time.Second)) { t.Error("expired inside the TTL") } if !a.IsExpired(pendingBase.Add(2 * time.Minute)) { t.Error("not expired past the TTL") } a.Attempts = DefaultMaxAttempts - 1 if !a.CanAsk() { t.Error("cannot ask with an attempt left") } a.Attempts = DefaultMaxAttempts if a.CanAsk() { t.Error("asked past the default cap") } a = PendingAction{Asked: pendingBase, TTL: time.Minute, MaxAttempts: 1, Attempts: 1} if a.CanAsk() { t.Error("asked past an explicit cap of 1") } }