990a4a99e9
Two defects in one logged line, both of which put a working capability out of reach of every utterance. The resident model rewrites as it routes, and on the way it transliterates: "перезапусти muzick indexer" came back as "перезагрузить музик индексер", so Nexus was asked to resolve a service nobody has ever named. entityReferenceText takes the longest Latin run out of his own words, but only when the Text slot has lost every Latin letter the utterance had — an English turn and a Russian entity name are both left alone, and reversing the transliteration is not attempted. The second half: the stage-3 gate thins an act that matched no allowlisted fn, and that question was the whole turn, so handleHexisAct never ran. Hexis is where an act with no local fn belongs, so it gets one chance before she asks, and a "" back still leaves her asking. With no ecosystem wired nothing changes. Capability matching reads the phrase as the haystack when there is no fn, because no capability name contains "restart status muzick indexer". Authority is untouched: ambiguity still stops, a mutating capability still goes through the spoken confirm.
134 lines
4.6 KiB
Go
134 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// TestEntityReferenceText pins when his own words win over the model's.
|
|
func TestEntityReferenceText(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
utterance string
|
|
text string
|
|
want string
|
|
}{
|
|
{
|
|
name: "the model transliterated the name",
|
|
utterance: "перезапусти muzick indexer",
|
|
text: "перезагрузить музик индексер",
|
|
want: "muzick indexer",
|
|
},
|
|
{
|
|
name: "it kept the name, so nothing to repair",
|
|
utterance: "перезапусти muzick indexer",
|
|
text: "перезагрузить muzick indexer",
|
|
want: "перезагрузить muzick indexer",
|
|
},
|
|
{
|
|
name: "an all-Russian entity name is not a rewrite",
|
|
utterance: "перезапусти домашний сервер",
|
|
text: "перезагрузить домашний сервер",
|
|
want: "перезагрузить домашний сервер",
|
|
},
|
|
{
|
|
name: "an English turn never enters the recovery",
|
|
utterance: "restart muzick indexer",
|
|
text: "restart muzick indexer",
|
|
want: "restart muzick indexer",
|
|
},
|
|
{
|
|
name: "the longest Latin run is the name",
|
|
utterance: "а перезапусти-ка nginx на muzick-indexer, пожалуйста",
|
|
text: "перезагрузить нгинкс",
|
|
want: "muzick-indexer",
|
|
},
|
|
{
|
|
name: "one stray letter is not a name",
|
|
utterance: "перезапусти сервер a",
|
|
text: "перезагрузить сервер",
|
|
want: "перезагрузить сервер",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dec := router.Decision{Utterance: tc.utterance, Slots: router.Slots{Text: tc.text}}
|
|
if got := entityReferenceText(dec); got != tc.want {
|
|
t.Fatalf("entityReferenceText = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNexusIsAskedForTheNameHeSaid — the defect end to end (Vikunja #476): the
|
|
// router hands over a transliterated Text, and Nexus must still be asked about
|
|
// the service that exists.
|
|
func TestNexusIsAskedForTheNameHeSaid(t *testing.T) {
|
|
ctx := context.Background()
|
|
nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", "Muzick indexer", "service"))
|
|
hexis := newFakeHexis(t, restartCaps(), fixtureHexisExecuted("exec_1", "succeeded"))
|
|
h := ecoHandler(t, nexus, nil, hexis)
|
|
|
|
dec := router.Decision{
|
|
Utterance: "перезапусти muzick indexer",
|
|
Intent: router.IntentAct,
|
|
Slots: router.Slots{Text: "перезагрузить музик индексер", Fn: "restart", HasFn: true},
|
|
}
|
|
h.handleHexisAct(ctx, dec)
|
|
|
|
reqs := nexus.Requests()
|
|
if len(reqs) == 0 {
|
|
t.Fatal("nexus was never asked")
|
|
}
|
|
body := string(reqs[0].Body)
|
|
if !strings.Contains(body, "muzick indexer") {
|
|
t.Fatalf("nexus resolve body = %s, want the name he said", body)
|
|
}
|
|
}
|
|
|
|
// TestAnEntityActReachesHexisInsteadOfAsking — the second half of #476. The
|
|
// stage-3 gate thins an act with no allowlisted fn, and that question used to
|
|
// be the whole turn, so the Hexis path was unreachable from voice or chat.
|
|
func TestAnEntityActReachesHexisInsteadOfAsking(t *testing.T) {
|
|
ctx := context.Background()
|
|
nexus := newFakeNexus(t, fixtureNexusResolved("ent_muzick", "Muzick indexer", "service"))
|
|
hexis := newFakeHexis(t, restartCaps(), fixtureHexisExecuted("exec_1", "succeeded"))
|
|
h := ecoHandler(t, nexus, nil, hexis)
|
|
|
|
dec := router.Decision{
|
|
Utterance: "перезапусти muzick indexer",
|
|
Intent: router.IntentAct,
|
|
Stage: 3,
|
|
Clarify: true,
|
|
Slots: router.Slots{Text: "restart status muzick indexer"},
|
|
}
|
|
reply := h.hexisBeforeClarify(ctx, dec)
|
|
if reply == "" {
|
|
t.Fatal("a resolvable entity act must reach hexis rather than fall through to the question")
|
|
}
|
|
if hexis.Count("", "/api/v1") == 0 {
|
|
t.Fatal("hexis was never contacted")
|
|
}
|
|
}
|
|
|
|
// TestClarifyStillAsksWithoutHexis — the narrowing. No ecosystem, no change:
|
|
// she asks exactly what she asked before.
|
|
func TestClarifyStillAsksWithoutHexis(t *testing.T) {
|
|
h, _, _ := newClarifyHandler(t)
|
|
dec := router.Decision{
|
|
Utterance: "перезапусти muzick indexer",
|
|
Intent: router.IntentAct,
|
|
Stage: 3,
|
|
Clarify: true,
|
|
Slots: router.Slots{Text: "перезагрузить музик индексер"},
|
|
}
|
|
if reply := h.hexisBeforeClarify(context.Background(), dec); reply != "" {
|
|
t.Fatalf("no hexis must mean no reply, got %q", reply)
|
|
}
|
|
if _, asked := h.askClarify(voiceCtx(), dec); !asked {
|
|
t.Fatal("she must still ask what to do")
|
|
}
|
|
}
|