package main import ( "context" "testing" "time" "github.com/kami/maven/internal/ipc" ) // quietFakeAPI records the WriteFact the toggle performs. type quietFakeAPI struct { ipc.UnimplementedCoreAPI got ipc.WriteFactReq call int } func (a *quietFakeAPI) WriteFact(_ context.Context, req ipc.WriteFactReq) (int64, error) { a.got, a.call = req, a.call+1 return 1, nil } // quietVerdict — what a phrase should do to the setting. type quietVerdict int const ( quietNone quietVerdict = iota quietOn quietOff ) func TestResolveQuietToggle(t *testing.T) { cases := []struct { text string want quietVerdict }{ // ON vocabulary. {"quiet on", quietOn}, {"quiet mode", quietOn}, {"тихий режим", quietOn}, {"тихий", quietOn}, {"не шуми", quietOn}, {"не беспокоить", quietOn}, {"тихо", quietOn}, // ON, inflected / embedded in a sentence. {"включи тихий режим", quietOn}, {"побудь в тихом режиме", quietOn}, {"Тихий Режим!", quietOn}, {"тихая", quietOn}, // The noun form and the comparative. {"включи режим тишины", quietOn}, {"режим тишины", quietOn}, {"сделай потише", quietOn}, {"сделай тише", quietOn}, {"потише", quietOn}, // English, as the fixture phrases it. {"turn quiet mode back on", quietOn}, {"enable quiet mode", quietOn}, // OFF vocabulary — all seven, incl. the three that used to say ON. {"quiet off", quietOff}, {"quiet end", quietOff}, {"громкий режим", quietOff}, {"шумный режим", quietOff}, {"отмени тихий", quietOff}, {"выключи тихий", quietOff}, {"не тихо", quietOff}, // OFF wins over the ON words it contains. {"выключи тихий режим", quietOff}, {"отмени тихий режим пожалуйста", quietOff}, {"верни громкий режим", quietOff}, {"выключи режим тишины", quietOff}, {"хватит тишины", quietOff}, {"turn off quiet mode", quietOff}, {"quiet mode off", quietOff}, {"stop quiet mode", quietOff}, {"disable quiet mode", quietOff}, // False positives: "тихо"/"тихий" as ordinary Russian. {"очень тихий сегодня день", quietNone}, {"в комнате тихо", quietNone}, {"тихонько напомни", quietNone}, {"потихоньку", quietNone}, {"тихонько", quietNone}, {"он говорил тихим голосом весь вечер", quietNone}, {"в тишине лучше думается", quietNone}, {"на улице стало потише", quietNone}, // Unrelated. {"напомни завтра позвонить маме", quietNone}, {"какая погода", quietNone}, {"", quietNone}, } for _, tc := range cases { t.Run(tc.text, func(t *testing.T) { api := &quietFakeAPI{} h := &reactiveHandler{api: api, now: func() time.Time { return time.Unix(0, 0).UTC() }} reply, handled := h.resolveQuietToggle(context.Background(), tc.text, sourceVoice) if tc.want == quietNone { if handled || reply != "" { t.Fatalf("%q: got (%q, %v), want no match", tc.text, reply, handled) } if api.call != 0 { t.Fatalf("%q: wrote a fact on a non-match", tc.text) } return } if !handled { t.Fatalf("%q: not handled, want %v", tc.text, tc.want) } wantReply, wantVal := "тихий режим выключен.", "false" if tc.want == quietOn { wantReply, wantVal = "тихий режим включён. буду реже напоминать.", "true" } if reply != wantReply { t.Errorf("%q: reply = %q, want %q", tc.text, reply, wantReply) } if api.call != 1 { t.Fatalf("%q: WriteFact called %d times, want 1", tc.text, api.call) } if api.got.Kind != "config" || api.got.Key != "quiet_hours" || api.got.Source != "tap:voice" || api.got.Confidence != 1.0 { t.Errorf("%q: request shape = %+v", tc.text, api.got) } if api.got.Value != wantVal { t.Errorf("%q: value = %q, want %q", tc.text, api.got.Value, wantVal) } }) } } // TestQuietToggleNegationIsNotAdjacency — negation used to be an adjacency // pattern ({"не","тих"} in the OFF list), so any word between the negator and // the quiet word made the ON pattern win and asking for quiet mode to STOP // turned it on. Negation is scanned over the whole utterance now. func TestQuietToggleNegationIsNotAdjacency(t *testing.T) { off := []string{ "не надо тихий режим", "не хочу тихий режим", "тихий режим выключи", "убери тихий режим", "хватит тихого режима", "прекрати тихий режим", "тихий режим отмени пожалуйста", } for _, text := range off { t.Run(text, func(t *testing.T) { on, isOff := classifyQuietToggle(text) if on || !isOff { t.Fatalf("%q: want OFF, got on=%v off=%v", text, on, isOff) } }) } // The two ON phrases that are themselves built on "не" must stay ON: they // are requests FOR quiet, not negations of one. for _, text := range []string{"не шуми", "не беспокоить"} { t.Run(text, func(t *testing.T) { on, isOff := classifyQuietToggle(text) if !on || isOff { t.Fatalf("%q: want ON, got on=%v off=%v", text, on, isOff) } }) } } // TestQuietToggleRecordsTheChannelItArrivedOn — the toggle is reachable from // mavweb /api/chat and telegram, not only the microphone. Every write used to // be stamped "tap:voice", so a toggle typed into the web UI claimed a mic wrote // it and the provenance column lied about a daemon-wide setting. func TestQuietToggleRecordsTheChannelItArrivedOn(t *testing.T) { for _, src := range []turnSource{sourceVoice, sourceText} { t.Run(string(src), func(t *testing.T) { api := &quietFakeAPI{} h := &reactiveHandler{api: api, now: func() time.Time { return time.Unix(0, 0).UTC() }} if _, handled := h.resolveQuietToggle(context.Background(), "тихий режим", src); !handled { t.Fatal("expected the toggle to match") } if api.got.Source != string(src) { t.Errorf("source = %q, want %q", api.got.Source, src) } }) } }