package store import ( "context" "errors" "testing" "time" ) // markerVec is a recognisable vector: nothing in these tests writes it except // the backfill, so finding it proves the row really was rewritten. var markerVec = []float32{9, 9, 9} func newEmbedder(calls *int) EmbedFunc { return func(_ context.Context, _ string) ([]float32, error) { *calls++ return markerVec, nil } } // seedOldVectors puts one note (notes table + unified index) and one fact // (unified index only) in the DB, both carrying obviously-old vectors. func seedOldVectors(t *testing.T, s *Store) { t.Helper() ctx := context.Background() old := []float32{0.1, 0.2, 0.3} id, err := s.WriteNote(ctx, time.Now(), "молоко в холодильнике", old, "voice") if err != nil { t.Fatalf("WriteNote: %v", err) } mem := s.VectorMemory() if err := mem.Insert(ctx, "note:1", old, map[string]string{ "type": "note", "text": "молоко в холодильнике", }); err != nil { t.Fatalf("Insert note vector: %v", err) } if err := mem.Insert(ctx, "fact:water:1", old, map[string]string{ "type": "fact", "text": "я пил воду", }); err != nil { t.Fatalf("Insert fact vector: %v", err) } _ = id } func noteVec(t *testing.T, s *Store) []float32 { t.Helper() var blob []byte if err := s.db.QueryRow(`SELECT embedding FROM notes LIMIT 1`).Scan(&blob); err != nil { t.Fatalf("read note embedding: %v", err) } return blobToFloats(blob) } func memVec(t *testing.T, s *Store, id string) []float32 { t.Helper() var blob []byte if err := s.db.QueryRow(`SELECT vec FROM memory_vectors WHERE id = ?`, id).Scan(&blob); err != nil { t.Fatalf("read memory vector %s: %v", id, err) } return decodeVec(blob) } func sameVec(a, b []float32) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // The deployed case: old vectors everywhere, no marker. Every vector in both // places must be rewritten and the marker recorded. func TestReembedAllRewritesEveryVector(t *testing.T) { s := newTestStore(t) ctx := context.Background() seedOldVectors(t, s) calls := 0 res, err := s.ReembedAll(ctx, "multilingual-e5-small@384", newEmbedder(&calls)) if err != nil { t.Fatalf("ReembedAll: %v", err) } if res.Skipped { t.Fatal("first run should not skip") } if res.Notes != 1 || res.MemNotes != 1 || res.Facts != 1 { t.Fatalf("counts: notes=%d memNotes=%d facts=%d", res.Notes, res.MemNotes, res.Facts) } if calls != 3 { t.Fatalf("embedder called %d times, want 3", calls) } if !sameVec(noteVec(t, s), markerVec) { t.Fatalf("notes table not rewritten: %v", noteVec(t, s)) } if !sameVec(memVec(t, s, "note:1"), markerVec) { t.Fatal("unified index note row not rewritten") } if !sameVec(memVec(t, s, "fact:water:1"), markerVec) { t.Fatal("unified index fact row not rewritten") } got, err := s.Meta(ctx, metaKeyEmbedderID) if err != nil { t.Fatalf("Meta: %v", err) } if got != "multilingual-e5-small@384" { t.Fatalf("marker = %q", got) } } // Re-running must do nothing at all — not a second pass over the same rows. func TestReembedAllSecondRunIsNoop(t *testing.T) { s := newTestStore(t) ctx := context.Background() seedOldVectors(t, s) calls := 0 if _, err := s.ReembedAll(ctx, "e5@384", newEmbedder(&calls)); err != nil { t.Fatalf("first run: %v", err) } first := calls res, err := s.ReembedAll(ctx, "e5@384", newEmbedder(&calls)) if err != nil { t.Fatalf("second run: %v", err) } if !res.Skipped { t.Fatal("second run should report Skipped") } if calls != first { t.Fatalf("second run embedded %d more rows, want 0", calls-first) } } // A failure partway must leave the DB exactly as it was: no marker, and the old // vectors still in place (one transaction, rolled back). func TestReembedAllPartialFailureLeavesMarkerUnset(t *testing.T) { s := newTestStore(t) ctx := context.Background() seedOldVectors(t, s) before := noteVec(t, s) calls := 0 boom := func(_ context.Context, _ string) ([]float32, error) { calls++ if calls == 2 { return nil, errors.New("onnx blew up") } return markerVec, nil } if _, err := s.ReembedAll(ctx, "e5@384", boom); err == nil { t.Fatal("expected an error") } got, err := s.Meta(ctx, metaKeyEmbedderID) if err != nil { t.Fatalf("Meta: %v", err) } if got != "" { t.Fatalf("marker was set to %q after a failed run", got) } if !sameVec(noteVec(t, s), before) { t.Fatal("a failed run left a partially rewritten notes table") } // And the mismatch warning must still fire, so the user knows to re-run. if _, mismatch, err := s.CheckEmbedder(ctx, "e5@384"); err != nil || !mismatch { t.Fatalf("CheckEmbedder after failed backfill: mismatch=%v err=%v", mismatch, err) } }