9d80a39a30
The clarify store was keyed per reach in V-466. The dialogue session was not: five call sites read and wrote the constant voiceDialogueID, so anaphora, history and the ordinal candidate list were one slot for the whole daemon. The candidate list is the half that cost something. She recites tasks at the mic, he types "первую сделал" on /chat, and it closes the second task he heard out loud on a surface that never showed him a list. Now every one of those sites reads dialogueIDOf(ctx), which handleText and the voice path already set. resolveCandidate also wrote resolved_by "tap:voice" for every pick, including a typed one. It takes the turn's source now. A row that lies about where it came from is worse than no row. Anaphora across surfaces was the other reading — one continuous conversation with her, any surface. Rejected: a phone open while he talks is the case this box hits, and two clients sharing one slot trample each other.
83 lines
3.1 KiB
Go
83 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// The list she read at the mic is not the list a browser is looking at
|
|
// (Vikunja #45 step 3). The clarify store was keyed per reach in #466; the
|
|
// dialogue session was still one slot for the box, so "второй" typed on the web
|
|
// closed the second task she had recited out loud.
|
|
func TestCandidatesDoNotCrossReaches(t *testing.T) {
|
|
h, st, _ := newClarifyHandler(t)
|
|
voiceCtx := withDialogueID(context.Background(), dialogueIDFor(sourceVoice, ""))
|
|
webCtx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
|
|
|
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
|
putCandidates(h, voiceCtx, ids, "купить хлеб", "позвонить маме")
|
|
|
|
if reply, handled := h.resolveCandidate(webCtx, "первую сделал", sourceText); handled {
|
|
t.Fatalf("a web turn picked from the list she read aloud: %q", reply)
|
|
}
|
|
live, err := st.ListTasks(context.Background(), "live")
|
|
if err != nil {
|
|
t.Fatalf("list tasks: %v", err)
|
|
}
|
|
if len(live) != 2 {
|
|
t.Fatalf("%d tasks live, want 2 — the web turn moved one", len(live))
|
|
}
|
|
// The reach that was offered the list still owns it.
|
|
if _, handled := h.resolveCandidate(voiceCtx, "первую сделал", sourceVoice); !handled {
|
|
t.Fatal("the mic lost its own list")
|
|
}
|
|
}
|
|
|
|
// A selection writes a fact, so the fact must name the reach the words arrived
|
|
// on. It said "tap:voice" for a typed turn.
|
|
func TestCandidateProvenanceFollowsTheReach(t *testing.T) {
|
|
h, st, _ := newClarifyHandler(t)
|
|
ctx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
|
ids := seedTasks(t, st, "купить хлеб")
|
|
putCandidates(h, ctx, ids, "купить хлеб")
|
|
|
|
if _, handled := h.resolveCandidate(ctx, "первую сделал", sourceText); !handled {
|
|
t.Fatal("the pick was not acted on")
|
|
}
|
|
done, err := st.ListTasks(context.Background(), "done")
|
|
if err != nil {
|
|
t.Fatalf("list tasks: %v", err)
|
|
}
|
|
if len(done) != 1 {
|
|
t.Fatalf("%d tasks done, want 1", len(done))
|
|
}
|
|
if by := done[0].ResolvedBy; by != string(sourceText) {
|
|
t.Errorf("resolved_by = %q, want %q", by, sourceText)
|
|
}
|
|
}
|
|
|
|
// Anaphora is per reach too: an ellipsis typed on the web must not continue the
|
|
// question he asked at the mic. Both surfaces stay usable at once, which is the
|
|
// case a single-owner box actually hits — a phone open while he talks.
|
|
func TestAnaphoraDoesNotCrossReaches(t *testing.T) {
|
|
h, _, _ := newClarifyHandler(t)
|
|
voiceCtx := withDialogueID(context.Background(), dialogueIDFor(sourceVoice, ""))
|
|
webCtx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
|
now := h.now()
|
|
|
|
h.rememberTurn(voiceCtx, nil, router.Decision{
|
|
Intent: router.IntentQuery, Utterance: "во сколько у меня встреча",
|
|
}, now)
|
|
|
|
if sess := h.dialogueSessions.Get(dialogueIDOf(webCtx), now); sess != nil {
|
|
t.Fatalf("the web reach inherited the mic's turn: %+v", sess)
|
|
}
|
|
sess := h.dialogueSessions.Get(dialogueIDOf(voiceCtx), now)
|
|
if sess == nil || !strings.Contains(sess.Slots.Text, "встреча") {
|
|
t.Fatalf("the mic lost its own turn: %+v", sess)
|
|
}
|
|
}
|