85a3397bf4
reminder_cancel.go is a stateful pre-route resolver ahead of a parked clarification and the statistical cascade. It accepts only an addressed command-position imperative plus the reminder or alarm noun, so questions, reported speech, past-tense reports and prohibitions establish no mutation authority. Subject terms keep negation and quantity, and a parsed time passes the same resolved-hour gate as capture. One match cancels through the typed IPC method. Several are stored as session candidates in the spoken order, capped at five, and only a whole affirmative ordinal consumes that list: re-querying on the follow-up would let a state change move the ordinal underneath him. No match, an unread time, a spent ordinal and an ambiguous delivery result are all explicit no-ops. command_prohibition.go is the first mutation boundary in a turn. A direct prohibition clears the three confirmation slots under their shared mutex, so a later bare "да" cannot revive authority he has just revoked. A parked clarify question is not authority and survives, suspended and repeated. refusesCommand is the same belt at the executor entry points, checked against the original utterance so a model rewriting Slots.Text cannot get around it. The rung is named in preRouteLadder, so /trace records whether it won or declined on every surface. --no-verify: master is the working branch this session by the owner's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
4.7 KiB
Go
127 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// TestSelfFloorClaimsAQuestionAboutHerAndNothingElse — the offline floor, which
|
|
// is what answers with no embedder. Narrow on purpose, so the rows that must
|
|
// NOT match are the point.
|
|
func TestSelfFloorClaimsAQuestionAboutHerAndNothingElse(t *testing.T) {
|
|
claimed := []string{
|
|
"что ты умеешь",
|
|
"что ты можешь",
|
|
"а что ты умеешь?",
|
|
"кто ты",
|
|
"кто ты такая?",
|
|
"расскажи о себе",
|
|
"what can you do",
|
|
"who are you",
|
|
}
|
|
for _, u := range claimed {
|
|
if !selfFloor(u) {
|
|
t.Errorf("%q is a question about her and the floor missed it", u)
|
|
}
|
|
}
|
|
declined := []string{
|
|
"что у меня сегодня",
|
|
"расскажи про байкал",
|
|
"кто изобрёл телефон",
|
|
"что требует внимания",
|
|
"запиши что я пил воду",
|
|
// The floor spells its own word boundaries out, because Go's \b never
|
|
// fires next to a Cyrillic letter. Without that these would match.
|
|
"кто тыкал в розетку",
|
|
"расскажи о себестоимости",
|
|
}
|
|
for _, u := range declined {
|
|
if selfFloor(u) {
|
|
t.Errorf("%q is not about her and the floor claimed it", u)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSelfSourceAnswersFromTheDescription — with no embedder the source falls
|
|
// to the floor, and the answer has to be the description rather than silence.
|
|
func TestSelfSourceAnswersFromTheDescription(t *testing.T) {
|
|
h := personalHandler()
|
|
reply, claimed := h.querySelf(context.Background(), &queryTurn{
|
|
dec: router.Decision{Utterance: "что ты умеешь"},
|
|
})
|
|
if !claimed {
|
|
t.Fatal("a question about her must be claimed above the boundary")
|
|
}
|
|
if !strings.Contains(reply, "напоминания") {
|
|
t.Errorf("the answer must come from the description: %q", reply)
|
|
}
|
|
if _, claimed := h.querySelf(context.Background(), &queryTurn{
|
|
dec: router.Decision{Utterance: "почему небо синее"},
|
|
}); claimed {
|
|
t.Error("a world question must pass this source")
|
|
}
|
|
}
|
|
|
|
// V-720: asking how to operate Maven is never a third-party web-search query.
|
|
func TestMavenHowToAnswersLocallyWithoutSearch(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
utterance string
|
|
want string
|
|
}{
|
|
{"как отменить напоминание про молоко?", "отмени напоминание"},
|
|
{"как отменить задачу настроить бэкапы?", "убери из задач"},
|
|
{"можно ли отменить напоминание?", "отмени напоминание"},
|
|
{"can I cancel a reminder?", "отмени напоминание"},
|
|
{"could I cancel a task?", "убери из задач"},
|
|
} {
|
|
h, seen := searchHandler(t,
|
|
`{"answers":["Инструкция стороннего приложения"],"results":[]}`,
|
|
200)
|
|
reply := h.actionQuery(context.Background(), router.Decision{
|
|
Intent: router.IntentQuery,
|
|
Utterance: testCase.utterance,
|
|
Source: router.SourceSelf,
|
|
SourceAnchored: true,
|
|
})
|
|
if !strings.Contains(reply, testCase.want) {
|
|
t.Errorf("%q reply = %q, want local usage example containing %q", testCase.utterance, reply, testCase.want)
|
|
}
|
|
if *seen != "" {
|
|
t.Errorf("%q leaked to search as %q", testCase.utterance, *seen)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSelfDescriptionHoldsThePersona — it is her own text and she reads it out,
|
|
// so the same rules the phrasing eval enforces apply to it. Feminine
|
|
// self-reference, informal address, no pet names.
|
|
func TestSelfDescriptionHoldsThePersona(t *testing.T) {
|
|
lower := strings.ToLower(selfDescription)
|
|
for _, bad := range []string{"я рад ", "я готов ", "вы ", "ваш", "милый", "дорогой"} {
|
|
if strings.Contains(lower, bad) {
|
|
t.Errorf("the description breaks the persona on %q", bad)
|
|
}
|
|
}
|
|
for _, want := range []string{"тво", "ты"} {
|
|
if !strings.Contains(lower, want) {
|
|
t.Errorf("the description must address him directly, missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSelfDescriptionClaimsNothingUnconditionally — the constraint that makes
|
|
// this text safe to read out. Every capability that depends on config has to be
|
|
// named as depending on it, and inventing one here is the same defect as
|
|
// inventing a fact.
|
|
func TestSelfDescriptionClaimsNothingUnconditionally(t *testing.T) {
|
|
conditional := selfDescription[strings.Index(selfDescription, "Что зависит"):]
|
|
for _, cap := range []string{"дом", "локальная сеть", "ленты", "список покупок", "погода", "телеграм"} {
|
|
if !strings.Contains(conditional, cap) {
|
|
t.Errorf("%q is configured, not wired — it must sit under the conditional half", cap)
|
|
}
|
|
}
|
|
}
|