package main import ( "context" "testing" "time" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/router" ) func TestIsPersonalQuery(t *testing.T) { for _, s := range []string{ "во сколько у меня встреча", "что у меня сегодня", "когда мой следующий отпуск", "где моя книга", "сколько моих задач висит", "when is my meeting", "do i have anything today", "did i take my vitamins", // Speech, but only the forms possession already covers ("did i"). // The verb forms the floor cannot see are the seeds' job, scored in // TestONNXPersonalBoundary. "what did i say about backups", } { if !isPersonalQuery(s) { t.Errorf("isPersonalQuery(%q) = false, want true", s) } } for _, s := range []string{ // First person without possession. These are questions about the // world that merely mention the asker, and refusing them would be the // opposite mistake. "как мне сварить борщ", "что я могу посмотреть вечером", "почему небо синее", "столица франции", "how do i boil an egg", // The floor is possession-only by design: a speech verb it cannot see // passes here and is caught by the seeds instead. "что я говорил про бэкапы?", "как я говорил, почему небо синее", "", } { if isPersonalQuery(s) { t.Errorf("isPersonalQuery(%q) = true, want false", s) } } } // kiwixTrapAPI stands in for the world. Nothing below the personal boundary // should be consulted for a question about him, so the test asserts on the // reply rather than on a call: reaching Kiwix or general knowledge produces a // phrased answer, and refusing produces the honest one. func personalHandler() *reactiveHandler { return &reactiveHandler{ api: ipc.UnimplementedCoreAPI{}, now: func() time.Time { return contNow }, // No phraser and no kiwix wiring: if the walk gets past the personal // source it reaches queryGeneral, which returns "не знаю." with a nil // phraser — a different string from the one this guard produces, so // the two cases stay distinguishable. } } // The regression: "во сколько у меня встреча" reached Kiwix, Wikipedia matched // an article on the 2015 CPISRA World Games, and the phraser reported it back // as his meeting. Seen on the deployed daemon, 01-08-2026. func TestPersonalQuestionIsNotSentToTheWorld(t *testing.T) { h := personalHandler() reply, ok := h.queryPersonal(context.Background(), &queryTurn{ dec: router.Decision{Intent: router.IntentQuery, Utterance: "во сколько у меня встреча"}, }) if !ok { t.Fatal("queryPersonal passed on a question about him") } if reply == "" { t.Fatal("empty reply") } } func TestWorldQuestionsPassThroughTheBoundary(t *testing.T) { h := personalHandler() for _, u := range []string{"почему небо синее", "столица франции"} { if _, ok := h.queryPersonal(context.Background(), &queryTurn{ dec: router.Decision{Intent: router.IntentQuery, Utterance: u}, }); ok { t.Errorf("queryPersonal claimed %q, want it to pass to the encyclopedia", u) } } } // The boundary must sit above kiwix and general-knowledge and below every // source that reads his own data. Asserted on the table itself: an ordering // bug here is silent, because both arrangements answer, just from the wrong // place. func TestPersonalBoundarySitsBetweenHisDataAndTheWorld(t *testing.T) { idx := map[string]int{} for i, s := range querySources { idx[s.name] = i } boundary, ok := idx["personal"] if !ok { t.Fatal("no personal source in the chain") } for _, his := range []string{"fact-by-key", "day-plan", "tasks", "calendar", "memory", "notes"} { if i, ok := idx[his]; !ok || i > boundary { t.Errorf("%q reads his own data and must run before the personal boundary", his) } } for _, world := range []string{"search", "kiwix", "web", "general-knowledge"} { if i, ok := idx[world]; !ok || i < boundary { t.Errorf("%q reads the world and must run after the personal boundary", world) } } }