Files
Maven/cmd/mavend/notefragment_test.go
T
claude b705a786ef a note stores his words, not the model's (V-576)
The note body was already the utterance. Two other holes were not.

The LLM router filled Slots.Text for a note from the model's own text
field, and that slot is what the replier reads out. So the confirmation
he heard named things he never said, twice over, differently each time.
The note payload is now the utterance and the model cannot touch it.

A correction with no referent is also not a note. 'нет, не маме, а папе'
names no intent, so parseRepair declines it and it routed as a fresh
note. actionNote now declines it and asks instead of filing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 01:39:38 +04:00

118 lines
3.6 KiB
Go

package main
import (
"context"
"testing"
"time"
"github.com/kami/maven/internal/ipc"
"github.com/kami/maven/internal/memory"
"github.com/kami/maven/internal/router"
"github.com/kami/maven/internal/store"
"github.com/kami/maven/internal/tool"
"github.com/kami/maven/internal/voice"
)
func TestCorrectionFragment(t *testing.T) {
cases := []struct {
utterance string
want bool
}{
{"нет, не маме, а папе", true},
{"Нет, не маме — а папе", true},
{"no, not mom, but dad", true},
// States something of its own, so it is his to keep.
{"нет, я не поеду, а останусь дома", false},
{"нет", false},
{"не маме, а папе", false}, // no refusal word opening it
{"нет, маме и папе", false}, // nothing negated
{"нет, не маме", false}, // nothing put in its place
{"запомни что кофе закончился", false},
}
for _, c := range cases {
if got := correctionFragment(c.utterance); got != c.want {
t.Errorf("correctionFragment(%q) = %v, want %v", c.utterance, got, c.want)
}
}
}
func newNoteHandler(t *testing.T) (*reactiveHandler, *store.Store) {
t.Helper()
st := newTestStore(t)
api := ipc.NewStoreAPI(st)
now := time.Now()
emb := router.NewHashEmbedder(1024)
h := &reactiveHandler{
api: api,
recall: recallWiring{embedder: emb, memStore: memory.NewInMemoryStore()},
router: buildRouter(emb, tool.NewMatcher(api), 0.55, nil),
replier: voice.NewStubReplier(),
now: func() time.Time { return now },
dataStore: st,
}
return h, st
}
// TestNoteBodyIsTheUtterance — the stored body comes from the utterance, never
// from Slots.Text, which the LLM router is free to write anything into (V-576).
func TestNoteBodyIsTheUtterance(t *testing.T) {
ctx := context.Background()
h, st := newNoteHandler(t)
dec := router.Decision{
Intent: router.IntentNote,
Utterance: "купил хлеб и молоко",
Slots: router.Slots{Text: "ты поедешь на дачу"},
}
if reply := h.applyAction(ctx, dec); reply != "" {
t.Fatalf("applyAction = %q, want empty", reply)
}
notes, err := st.RecentNotes(ctx, 10)
if err != nil {
t.Fatalf("RecentNotes: %v", err)
}
if len(notes) != 1 || notes[0].Text != dec.Utterance {
t.Fatalf("stored note = %+v, want body %q", notes, dec.Utterance)
}
}
// TestNoteBodyIsStable — the same utterance twice stores the same text.
func TestNoteBodyIsStable(t *testing.T) {
ctx := context.Background()
h, st := newNoteHandler(t)
dec := router.Decision{Intent: router.IntentNote, Utterance: "кофе закончился"}
h.applyAction(ctx, dec)
h.applyAction(ctx, dec)
notes, err := st.RecentNotes(ctx, 10)
if err != nil {
t.Fatalf("RecentNotes: %v", err)
}
if len(notes) != 2 {
t.Fatalf("notes = %d, want 2", len(notes))
}
if notes[0].Text != notes[1].Text || notes[0].Text != dec.Utterance {
t.Fatalf("bodies differ: %q vs %q", notes[0].Text, notes[1].Text)
}
}
// TestCorrectionFragmentWritesNoNote — a correction with nothing behind it is
// not a note, and she says so instead of filing it (V-576).
func TestCorrectionFragmentWritesNoNote(t *testing.T) {
ctx := context.Background()
h, st := newNoteHandler(t)
dec := router.Decision{Intent: router.IntentNote, Utterance: "нет, не маме, а папе"}
if reply := h.applyAction(ctx, dec); reply != nothingToCorrectReply {
t.Fatalf("reply = %q, want %q", reply, nothingToCorrectReply)
}
notes, err := st.RecentNotes(ctx, 10)
if err != nil {
t.Fatalf("RecentNotes: %v", err)
}
if len(notes) != 0 {
t.Fatalf("notes = %+v, want none", notes)
}
}