6759ff6003
A parked clarify said what she heard (an intent) and not what she was about to do, so the resolver had to infer the action from conversational history instead of reading it off an object. PendingAction names the capability being assembled in the ecosystem's dotted form (reminder.create, fact.write, act.run), the slots it has, the slots it still wants, when it was asked, attempts and TTL. Gaps() computes the missing slots from the slots rather than trusting Missing, because Missing is what she asked and the slots are what she got. CapabilityFor maps every dialogue.Intent, so the mapping lives here and dialogue still does not import router (the cycle rule). Nothing reads it yet: this is the widening V-560 to V-562 build on.
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
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")
|
|
}
|
|
}
|