maven: scheduled routines + persistent long-term memory
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
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package routine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
ok := []Routine{{Name: "morning", Cron: "0 8 * * *", Body: "доброе утро"}}
|
||||
if err := Validate(ok); err != nil {
|
||||
t.Fatalf("valid routine rejected: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
r Routine
|
||||
}{
|
||||
{"missing name", Routine{Cron: "0 8 * * *", Body: "x"}},
|
||||
{"missing body", Routine{Name: "m", Cron: "0 8 * * *"}},
|
||||
{"bad cron", Routine{Name: "m", Cron: "not a cron", Body: "x"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if err := Validate([]Routine{c.r}); err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDue(t *testing.T) {
|
||||
// a routine that fires at 08:00 every day.
|
||||
rs := []Routine{{Name: "morning", Cron: "0 8 * * *", Body: "доброе утро", Severity: 1}}
|
||||
|
||||
t.Run("first sight seeds without firing (cold-start guard)", func(t *testing.T) {
|
||||
last := map[string]time.Time{}
|
||||
now := time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC) // exactly on schedule
|
||||
got := Due(rs, last, now)
|
||||
if len(got) != 0 {
|
||||
t.Errorf("routine fired on first sight: %v", got)
|
||||
}
|
||||
if _, seen := last["morning"]; !seen {
|
||||
t.Error("first sight did not seed the last-fired map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fires once the schedule crosses", func(t *testing.T) {
|
||||
last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 7, 30, 0, 0, time.UTC)}
|
||||
now := time.Date(2026, 7, 6, 8, 0, 30, 0, time.UTC) // just past 08:00
|
||||
got := Due(rs, last, now)
|
||||
if len(got) != 1 || got[0].Name != "morning" {
|
||||
t.Fatalf("expected morning to fire, got %v", got)
|
||||
}
|
||||
if !last["morning"].Equal(now) {
|
||||
t.Errorf("last-fired not advanced to now: %v", last["morning"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not refire before the next crossing", func(t *testing.T) {
|
||||
last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC)}
|
||||
now := time.Date(2026, 7, 6, 8, 5, 0, 0, time.UTC) // same morning, later
|
||||
if got := Due(rs, last, now); len(got) != 0 {
|
||||
t.Errorf("routine refired within the same window: %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missed schedule on restart fires at most once, not per-tick", func(t *testing.T) {
|
||||
// booted at 07:00, seeded then; next tick is 09:00 (08:00 already passed).
|
||||
last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 7, 0, 0, 0, time.UTC)}
|
||||
now := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
|
||||
if got := Due(rs, last, now); len(got) != 1 {
|
||||
t.Fatalf("missed 08:00 should fire once at 09:00, got %v", got)
|
||||
}
|
||||
// immediately after, it must not fire again.
|
||||
if got := Due(rs, last, now.Add(time.Minute)); len(got) != 0 {
|
||||
t.Errorf("routine fired twice for one missed schedule: %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad cron is skipped, not fired every tick", func(t *testing.T) {
|
||||
bad := []Routine{{Name: "broken", Cron: "nonsense", Body: "x"}}
|
||||
last := map[string]time.Time{"broken": time.Date(2026, 7, 6, 7, 0, 0, 0, time.UTC)}
|
||||
now := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
|
||||
if got := Due(bad, last, now); len(got) != 0 {
|
||||
t.Errorf("unparseable cron fired: %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user