Files
executus/contrib/store/persona_store_test.go
steve c8559676ed
executus CI / test (push) Has been cancelled
P4b: skill noun + contrib/store (SQLite for budget/persona/skill/audit)
Merges the skill half of the persona/skill pair plus the second nested module.
(Squashed onto main from phase-4b-skill; the audit/budget/persona batteries it
was stacked on already landed via the P4 merge.)

- skill/: clean-redesign Skill noun + LEAN SkillStore (lifecycle/versions/
  schedule only) + ToRunnable + Memory default.
- contrib/store/: separate go.mod carrying modernc.org/sqlite, so the driver
  never enters the core go.sum. db.Budget()/Personas()/Skills()/Audit() back
  all four store seams (JSON-blob + indexed columns; round-trip tested).
  Includes the verified gadfly #5 fixes (AppendVersion tx+UNIQUE+error,
  Mark*ScheduledRun atomic json_set, busy_timeout, NaN guard).
- CI: builds + tests the nested module and asserts it owns the sqlite driver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:15:00 -04:00

107 lines
3.4 KiB
Go

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)
}
}