86817d6d06
"что я говорил про бэкапы?" is his data by definition, and nothing outside the box has ever heard him say anything. The boundary matched possession words only, so the question walked past it into SearXNG and came back answered out of a Habr article about somebody else's backups. A speech-verb marker class was written first and dropped. Russian gives every verb a dozen surface forms and the "как я говорил, ..." preamble list has no end, so each form the lexicon missed was one more question reaching the world, and a missing verb looks exactly like no bug. The boundary now embeds two frozen seed sets and scores the turn's own query vector, already computed upstream, against both. Nearest side wins. The possession markers stay as the offline floor for a handler with no embedder. 19/19 held-out utterances correct against multilingual-e5-small; see docs/evals/2026-08-03-personal-boundary.md. The live probe on the deployed box is not done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.1 KiB
Go
118 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|