22edc3cdfb
DefaultFactParser matched Russian by hand-written stem substring: "вод", "пил", "душ", "еда" and eleven more, with a helper whose own comment said it would use a morphology lib "until misfires actually bite". That is the fourth mechanism CLAUDE.md says does not exist, and it ran on every fact turn through both wirings in cmd/mavend/voicewire.go. Five closed classes move to internal/lexicon — water nouns and drink verbs, meal words, shower, break, sleep — and internal/morph does the inflection. Three dictionary quirks are carried as data rather than worked around in code, each with its reason in the set's note: "вода" and "водой" lemmatise to two different lemmas, "пил" lemmatises to the saw, and "спал" to "спасть". Shower is matched exactly rather than by lemma, because the dictionary makes "душ" and "душа" one word and only one of them is washing. The accusative of an inanimate noun is its nominative, so exact matching costs nothing he says. NOT behaviour-preserving, deliberately. Rejected now: "пилот", "водитель", "заводить", "душа", "душно", "беда", "победа". "есть" and "ел" are left out of the meal set on purpose — "есть новости по бэкапу" is a question. The vestigial "ate"/"backup" guard goes with the substring era that needed it. Measured on the RU routing fixture, classifier+ONNX arm (91 cases): 64/91 (70.3%) before and after, same failing cases. The LLM arm was not measured — no llama-server reachable from here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
3.1 KiB
Go
80 lines
3.1 KiB
Go
package router
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Russian capture must produce the same keys the loop's care rules read, or a
|
|
// ru voice test silently writes nothing and the nudges look broken.
|
|
func TestDefaultFactParserRU(t *testing.T) {
|
|
p := DefaultFactParser{}
|
|
cases := []struct {
|
|
utterance string
|
|
key string
|
|
ok bool
|
|
}{
|
|
{"выпил воды", "water", true},
|
|
{"попил воду", "water", true},
|
|
{"я поел", "meal", true},
|
|
{"пообедал", "meal", true},
|
|
{"принял душ", "shower", true},
|
|
{"сделал перерыв", "break", true},
|
|
{"немного отдохнул", "break", true},
|
|
{"поспал шесть часов", "sleep", true},
|
|
{"перезапусти nginx", "", false}, // an act, not a fact
|
|
// Inflections the substring stems caught and a lemma test must keep.
|
|
{"только что выпил кружку воды", "water", true},
|
|
{"воды попил наконец", "water", true},
|
|
{"пью воду", "water", true},
|
|
{"поужинал", "meal", true},
|
|
{"отметь что я позавтракал овсянкой", "meal", true},
|
|
{"сходил в душ", "shower", true},
|
|
{"отдыхал час", "break", true},
|
|
{"выспался", "sleep", true},
|
|
// Misfires the substring stems accepted. Rejecting them is the point of
|
|
// V-586: none of these is a fact about his day.
|
|
{"я пилот", "", false},
|
|
{"водитель приехал", "", false},
|
|
{"надо заводить машину", "", false},
|
|
{"у меня душа болит", "", false},
|
|
{"в комнате душно", "", false},
|
|
{"это была беда", "", false},
|
|
{"победа наша", "", false},
|
|
}
|
|
for _, c := range cases {
|
|
k, _, ok := p.Parse(c.utterance)
|
|
if ok != c.ok || k != c.key {
|
|
t.Errorf("Parse(%q) = (%q, %v), want (%q, %v)", c.utterance, k, ok, c.key, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCalendarDate(t *testing.T) {
|
|
now := time.Date(2026, 7, 6, 14, 30, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
text string
|
|
want time.Time
|
|
ok bool
|
|
}{
|
|
{"что у меня сегодня по плану", time.Date(2026, 7, 6, 0, 0, 0, 0, time.UTC), true},
|
|
{"что завтра", time.Date(2026, 7, 7, 0, 0, 0, 0, time.UTC), true},
|
|
{"планы на сегодня", time.Date(2026, 7, 6, 0, 0, 0, 0, time.UTC), true},
|
|
{"расписание на завтра", time.Date(2026, 7, 7, 0, 0, 0, 0, time.UTC), true},
|
|
{"what's today", time.Date(2026, 7, 6, 0, 0, 0, 0, time.UTC), true},
|
|
{"tomorrow plans", time.Date(2026, 7, 7, 0, 0, 0, 0, time.UTC), true},
|
|
{"какое число послезавтра", time.Date(2026, 7, 8, 0, 0, 0, 0, time.UTC), true},
|
|
{"что было вчера", time.Date(2026, 7, 5, 0, 0, 0, 0, time.UTC), true},
|
|
{"yesterday plans", time.Date(2026, 7, 5, 0, 0, 0, 0, time.UTC), true},
|
|
{"какая погода", time.Time{}, false},
|
|
{"сколько времени", time.Time{}, false},
|
|
{"", time.Time{}, false},
|
|
}
|
|
for _, c := range cases {
|
|
got, ok := ParseCalendarDate(c.text, now)
|
|
if ok != c.ok || !got.Equal(c.want) {
|
|
t.Errorf("ParseCalendarDate(%q) = %v, %v; want %v, %v", c.text, got, ok, c.want, c.ok)
|
|
}
|
|
}
|
|
}
|