ad60e10e95
Automatic rather than a flag, unlike -reembed: only voice-tapped facts are in this index, so it is tens of embeddings rather than thousands of notes. And waiting for an operator to know the repair exists is the failure being fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
5.4 KiB
Go
152 lines
5.4 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The write-path half of #493: recall of a fact must read back the fact, not
|
|
// the sentence he happened to say.
|
|
func TestFactRecallText(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, key, value, want string
|
|
}{
|
|
{"json value", "go_version", `"1.20"`, "go version — 1.20"},
|
|
{"plain value", "water", "выпил", "water — выпил"},
|
|
{"no value", "shower", "", "shower"},
|
|
{"underscores are spoken as spaces", "espresso_machine", `"чистая"`, "espresso machine — чистая"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := FactRecallText(tc.key, tc.value); got != tc.want {
|
|
t.Fatalf("FactRecallText(%q, %q) = %q; want %q", tc.key, tc.value, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A correction left the superseded value in the index, so recall answered with
|
|
// the value he had just corrected (#493).
|
|
func TestCorrectValueDropsMemoryVectors(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
now := time.Now()
|
|
mem := s.VectorMemory()
|
|
|
|
if _, err := s.WriteFact(ctx, now, KindSelf, "go_version", `"1.20"`, "tap:voice", 1.0, sql.NullInt64{}); err != nil {
|
|
t.Fatalf("WriteFact: %v", err)
|
|
}
|
|
if err := mem.Insert(ctx, "fact:go_version:1", []float32{1, 0, 0}, map[string]string{
|
|
"type": "fact", "text": "go version — 1.20",
|
|
}); err != nil {
|
|
t.Fatalf("Insert: %v", err)
|
|
}
|
|
if _, err := s.CorrectValue(ctx, "go_version", "feedback", "1.25", now.Add(time.Minute)); err != nil {
|
|
t.Fatalf("CorrectValue: %v", err)
|
|
}
|
|
got, err := mem.ByPrefix(ctx, "fact:")
|
|
if err != nil {
|
|
t.Fatalf("ByPrefix: %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Fatalf("after the correction the index still holds %+v; the superseded value must not answer", got)
|
|
}
|
|
}
|
|
|
|
// The recovery path a poisoned box had none of (#470 point 4, #493): rows
|
|
// written before the fix hold utterances, voided junk and superseded values,
|
|
// and no write-path change reaches any of them.
|
|
func TestRepairFactVectors(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
now := time.Now()
|
|
mem := s.VectorMemory()
|
|
embed := func(ctx context.Context, text string) ([]float32, error) {
|
|
return []float32{float32(len(text)), 1, 0}, nil
|
|
}
|
|
|
|
// A live fact indexed under the question that wrote it — the defect.
|
|
if _, err := s.WriteFact(ctx, now, KindSelf, "water", `"выпил"`, "tap:voice", 1.0, sql.NullInt64{}); err != nil {
|
|
t.Fatalf("WriteFact water: %v", err)
|
|
}
|
|
if err := mem.Insert(ctx, "fact:water:100", []float32{9, 9, 9}, map[string]string{
|
|
"type": "fact", "source": "voice", "text": "запиши что я пил воду",
|
|
}); err != nil {
|
|
t.Fatalf("Insert water: %v", err)
|
|
}
|
|
// A voided fact whose vector survived the void.
|
|
if _, err := s.WriteFact(ctx, now, KindSelf, "go_version", `"1.20"`, "tap:voice", 1.0, sql.NullInt64{}); err != nil {
|
|
t.Fatalf("WriteFact go_version: %v", err)
|
|
}
|
|
if _, _, err := s.VoidLatestFact(ctx, "go_version", "feedback", now.Add(time.Minute)); err != nil {
|
|
t.Fatalf("VoidLatestFact: %v", err)
|
|
}
|
|
if err := mem.Insert(ctx, "fact:go_version:100", []float32{9, 9, 9}, map[string]string{
|
|
"type": "fact", "text": "какая последняя версия языка Go?",
|
|
}); err != nil {
|
|
t.Fatalf("Insert go_version: %v", err)
|
|
}
|
|
// A key with two vectors: only the newest may answer.
|
|
if _, err := s.WriteFact(ctx, now, KindSelf, "mood", `"устал"`, "tap:voice", 1.0, sql.NullInt64{}); err != nil {
|
|
t.Fatalf("WriteFact mood: %v", err)
|
|
}
|
|
for _, ts := range []string{"100", "200"} {
|
|
if err := mem.Insert(ctx, "fact:mood:"+ts, []float32{9, 9, 9}, map[string]string{
|
|
"type": "fact", "text": "мне грустно",
|
|
}); err != nil {
|
|
t.Fatalf("Insert mood %s: %v", ts, err)
|
|
}
|
|
}
|
|
// A note must be left entirely alone.
|
|
if err := mem.Insert(ctx, "note:7", []float32{5, 5, 5}, map[string]string{
|
|
"type": "note", "text": "сеть тормозит по вечерам",
|
|
}); err != nil {
|
|
t.Fatalf("Insert note: %v", err)
|
|
}
|
|
|
|
res, err := s.RepairFactVectors(ctx, embed)
|
|
if err != nil {
|
|
t.Fatalf("RepairFactVectors: %v", err)
|
|
}
|
|
if res.Rewritten != 2 || res.Dropped != 2 {
|
|
t.Fatalf("repair reported %+v; want 2 rewritten (water, newest mood) and 2 dropped (voided go_version, superseded mood)", res)
|
|
}
|
|
|
|
got, err := mem.ByPrefix(ctx, "fact:")
|
|
if err != nil {
|
|
t.Fatalf("ByPrefix: %v", err)
|
|
}
|
|
texts := map[string]string{}
|
|
for _, r := range got {
|
|
texts[r.ID] = r.Meta["text"]
|
|
}
|
|
if len(texts) != 2 {
|
|
t.Fatalf("the index holds %+v; want only fact:water:100 and fact:mood:200", texts)
|
|
}
|
|
if texts["fact:water:100"] != "water — выпил" {
|
|
t.Fatalf("water reads back %q; want the fact, not the utterance", texts["fact:water:100"])
|
|
}
|
|
if texts["fact:mood:200"] != "mood — устал" {
|
|
t.Fatalf("mood reads back %q", texts["fact:mood:200"])
|
|
}
|
|
// Provenance the row already carried must survive the rewrite.
|
|
for _, r := range got {
|
|
if r.ID == "fact:water:100" && r.Meta["source"] != "voice" {
|
|
t.Fatalf("water lost its source meta: %+v", r.Meta)
|
|
}
|
|
}
|
|
if notes, err := mem.ByPrefix(ctx, "note:"); err != nil || len(notes) != 1 {
|
|
t.Fatalf("the note row was touched: %+v (err %v)", notes, err)
|
|
}
|
|
|
|
// Marker written, so a second run is free and changes nothing.
|
|
again, err := s.RepairFactVectors(ctx, embed)
|
|
if err != nil {
|
|
t.Fatalf("second RepairFactVectors: %v", err)
|
|
}
|
|
if !again.Skipped {
|
|
t.Fatalf("second run did work: %+v; the marker must make it a no-op", again)
|
|
}
|
|
}
|