59a4e06615
Two additive proactive/recall features. Routines (internal/routine): a third proactive class beside reminders (user-stated) and care rules (world-state) — operator-declared clockwork. config.routines[] (cron + literal RU body + severity) fire through the normal dispatcher on schedule. Bodies are literal, not LLM-phrased (can't hallucinate); rule name routine:<name> keeps them out of the care autotuner; a cold-start guard seeds on first sight so a restart never replays a missed schedule. Pure routine.Due + config validation, unit- tested; the tick driver holds the last-fired map and calls fireRoutines. Persistent memory (internal/store/memory.go): store.MemoryStore backs the memory.Store interface with the SAME encrypted sqlite db — survives restarts and recall text inherits at-rest encryption (no plaintext sidecar). float32-blob vectors, brute-force cosine (ANN is a later swap behind the interface), upsert-by-id. The daemon wires st.VectorMemory() into wireVoice; the in-memory impl stays the test/no-store floor. Closes the "in-memory only, lost on restart" gap (PROGRESS #8). Gate green: gofmt/vet clean, -race across routine/config/store/mavend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2PNdwDj2Gt8YW294J7oSc
104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func newMemTestStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "mem_test.db")
|
|
st, err := Open(context.Background(), path)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
return st
|
|
}
|
|
|
|
func TestMemoryStoreInsertSearch(t *testing.T) {
|
|
ctx := context.Background()
|
|
m := newMemTestStore(t).VectorMemory()
|
|
|
|
// three orthonormal-ish vectors; a query aligned with the second must rank it top.
|
|
if err := m.Insert(ctx, "a", []float32{1, 0, 0}, map[string]string{"text": "вода"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Insert(ctx, "b", []float32{0, 1, 0}, map[string]string{"text": "сон", "type": "fact"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Insert(ctx, "c", []float32{0, 0, 1}, map[string]string{"text": "еда"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := m.Search(ctx, []float32{0, 1, 0}, 2)
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("topK=2 returned %d results", len(got))
|
|
}
|
|
if got[0].ID != "b" {
|
|
t.Errorf("top hit = %q, want b", got[0].ID)
|
|
}
|
|
if got[0].Meta["text"] != "сон" || got[0].Meta["type"] != "fact" {
|
|
t.Errorf("meta not round-tripped: %v", got[0].Meta)
|
|
}
|
|
if got[0].Score < 0.99 {
|
|
t.Errorf("aligned vector score = %v, want ~1.0", got[0].Score)
|
|
}
|
|
}
|
|
|
|
func TestMemoryStoreUpsertReplaces(t *testing.T) {
|
|
ctx := context.Background()
|
|
m := newMemTestStore(t).VectorMemory()
|
|
|
|
if err := m.Insert(ctx, "x", []float32{1, 0}, map[string]string{"text": "старое"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Insert(ctx, "x", []float32{0, 1}, map[string]string{"text": "новое"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := m.Search(ctx, []float32{0, 1}, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("re-inserting the same id produced %d rows, want 1 (upsert)", len(got))
|
|
}
|
|
if got[0].Meta["text"] != "новое" {
|
|
t.Errorf("upsert kept the old value: %q", got[0].Meta["text"])
|
|
}
|
|
}
|
|
|
|
func TestMemoryStorePersistsAcrossReopen(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(t.TempDir(), "persist.db")
|
|
|
|
st, err := Open(ctx, path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.VectorMemory().Insert(ctx, "k", []float32{1, 0, 0}, map[string]string{"text": "запомни"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// reopen the same file — the in-memory floor would have lost this.
|
|
st2, err := Open(ctx, path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = st2.Close() })
|
|
got, err := st2.VectorMemory().Search(ctx, []float32{1, 0, 0}, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 || got[0].Meta["text"] != "запомни" {
|
|
t.Fatalf("memory did not survive reopen: %v", got)
|
|
}
|
|
}
|