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) } } // TestMarkAgentScheduledRunBlobRoundTrips guards the json_set atomic update: // the JSON blob must stay parseable and reflect the new scheduled times. func TestMarkAgentScheduledRunBlobRoundTrips(t *testing.T) { ctx := context.Background() db, _ := Open(":memory:") defer db.Close() st := db.Personas() st.InitializeAgentStorage(ctx) start := time.Now().UTC() a := &persona.Agent{ID: "m1", Name: "n", OwnerID: "o", Schedule: "0 * * * *"} a.NextRunAt = &start if err := st.SaveAgent(ctx, a); err != nil { t.Fatal(err) } ran := start next := start.Add(time.Hour) if err := st.MarkAgentScheduledRun(ctx, "m1", ran, next); err != nil { t.Fatal(err) } got, err := st.GetAgent(ctx, "m1") // blob must still unmarshal if err != nil { t.Fatalf("GetAgent after json_set Mark failed (blob corrupt?): %v", err) } if got.NextRunAt == nil || !got.NextRunAt.Equal(next) { t.Errorf("blob NextRunAt = %v, want %v", got.NextRunAt, next) } if got.LastScheduledRunAt == nil || !got.LastScheduledRunAt.Equal(ran) { t.Errorf("blob LastScheduledRunAt = %v, want %v", got.LastScheduledRunAt, ran) } // Unknown id -> ErrNotFound. if err := st.MarkAgentScheduledRun(ctx, "nope", ran, next); err != persona.ErrNotFound { t.Errorf("Mark(unknown) = %v, want ErrNotFound", err) } }