P4: contrib/store — persona SQLite store (JSON-blob round-trip)
db.Personas() satisfies persona.Storage over SQLite. Each Agent is stored as a JSON blob with extracted indexed columns (owner_id, name, webhook_secret, chatbot_channel_filter, schedule, next_run_at) — so the WHOLE struct round-trips (no domain<->GORM<->DB field-loss footgun) while the lookups stay indexable. Test proves the round-trip preserves nested + map fields (SkillPalette, StateReactEmoji), the owner/name + webhook + chatbot-filter queries, the scheduled-due query, and MarkAgentScheduledRun clearing the due window. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/executus/persona"
|
||||
)
|
||||
|
||||
func TestSQLitePersonaStore(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
st := db.Personas()
|
||||
if err := st.InitializeAgentStorage(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Full struct round-trips through the JSON blob (incl. nested + map fields).
|
||||
a := &persona.Agent{
|
||||
ID: "a1", Name: "helper", OwnerID: "o1", SystemPrompt: "be nice",
|
||||
ModelTier: "fast", SkillPalette: []string{"animate"},
|
||||
StateReactEmoji: map[string]string{"running": "⏳"},
|
||||
ChatbotChannelFilter: "general",
|
||||
}
|
||||
if err := st.SaveAgent(ctx, a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := st.GetAgent(ctx, "a1")
|
||||
if err != nil || got.SystemPrompt != "be nice" || len(got.SkillPalette) != 1 ||
|
||||
got.StateReactEmoji["running"] != "⏳" {
|
||||
t.Fatalf("round-trip lost fields: %+v (err %v)", got, err)
|
||||
}
|
||||
if byName, err := st.GetAgentByName(ctx, "o1", "helper"); err != nil || byName.ID != "a1" {
|
||||
t.Fatalf("GetAgentByName: %v %+v", err, byName)
|
||||
}
|
||||
if cf, _ := st.ListAgentsByChatbotChannelFilter(ctx); len(cf) != 1 {
|
||||
t.Errorf("ListAgentsByChatbotChannelFilter = %d, want 1", len(cf))
|
||||
}
|
||||
|
||||
// Scheduling: due query + MarkAgentScheduledRun round-trip.
|
||||
now := time.Now().UTC()
|
||||
sched := &persona.Agent{ID: "s1", Name: "cron", OwnerID: "o1", Schedule: "0 * * * *"}
|
||||
due := now.Add(-time.Minute)
|
||||
sched.NextRunAt = &due
|
||||
if err := st.SaveAgent(ctx, sched); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dueList, _ := st.ListScheduledAgents(ctx, now)
|
||||
if len(dueList) != 1 || dueList[0].ID != "s1" {
|
||||
t.Fatalf("ListScheduledAgents = %+v", dueList)
|
||||
}
|
||||
next := now.Add(time.Hour)
|
||||
if err := st.MarkAgentScheduledRun(ctx, "s1", now, next); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again, _ := st.ListScheduledAgents(ctx, now); len(again) != 0 {
|
||||
t.Errorf("after MarkAgentScheduledRun, nothing should be due before now: %+v", again)
|
||||
}
|
||||
|
||||
if err := st.DeleteAgent(ctx, "a1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.GetAgent(ctx, "a1"); err != persona.ErrNotFound {
|
||||
t.Errorf("GetAgent after delete = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user