P4b: skill noun + contrib/store (SQLite for budget/persona/skill/audit)
executus CI / test (push) Has been cancelled

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>
This commit is contained in:
2026-06-27 00:15:00 -04:00
parent d82cef46b4
commit c8559676ed
25 changed files with 3141 additions and 10 deletions
+72
View File
@@ -0,0 +1,72 @@
package store
import (
"context"
"testing"
"time"
"gitea.stevedudenhoeffer.com/steve/executus/skill"
)
func TestSQLiteSkillStore(t *testing.T) {
ctx := context.Background()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
st := db.Skills()
if err := st.Initialize(ctx); err != nil {
t.Fatal(err)
}
pub := &skill.Skill{ID: "a", Name: "pub", OwnerID: "o1", Visibility: skill.VisibilityPublic,
Tools: []string{"summarize"}, ExposeAsChatbotTool: true}
shared := &skill.Skill{ID: "b", Name: "shr", OwnerID: "o1", Visibility: skill.VisibilityShared, SharedWith: []string{"bob"}}
if err := st.Save(ctx, pub); err != nil {
t.Fatal(err)
}
if err := st.Save(ctx, shared); err != nil {
t.Fatal(err)
}
got, err := st.Get(ctx, "a")
if err != nil || len(got.Tools) != 1 || !got.ExposeAsChatbotTool {
t.Fatalf("round-trip: %v %+v", err, got)
}
if ps, _ := st.ListPublic(ctx); len(ps) != 1 || ps[0].ID != "a" {
t.Errorf("ListPublic = %+v", ps)
}
if ss, _ := st.ListSharedWith(ctx, "bob"); len(ss) != 1 || ss[0].ID != "b" {
t.Errorf("ListSharedWith(bob) = %+v", ss)
}
if ss, _ := st.ListSharedWith(ctx, "carol"); len(ss) != 0 {
t.Errorf("ListSharedWith(carol) should be empty: %+v", ss)
}
if ce, _ := st.ListChatbotExposed(ctx); len(ce) != 1 {
t.Errorf("ListChatbotExposed = %d, want 1", len(ce))
}
// Versions newest-first + by id.
st.AppendVersion(ctx, skill.SkillVersion{ID: "v1", SkillID: "a", Version: "1.0.0"})
st.AppendVersion(ctx, skill.SkillVersion{ID: "v2", SkillID: "a", Version: "1.1.0"})
vs, _ := st.ListVersionsBySkill(ctx, "a", 10)
if len(vs) != 2 || vs[0].ID != "v2" {
t.Errorf("versions newest-first: %+v", vs)
}
if gv, err := st.GetVersionByID(ctx, "v1"); err != nil || gv.Version != "1.0.0" {
t.Errorf("GetVersionByID: %v %+v", err, gv)
}
// Scheduling.
now := time.Now().UTC()
cron := &skill.Skill{ID: "c", Name: "cron", OwnerID: "o1", Schedule: "0 * * * *", NextRunAt: now.Add(-time.Minute)}
st.Save(ctx, cron)
if due, _ := st.ListDueScheduled(ctx, now); len(due) != 1 || due[0].ID != "c" {
t.Fatalf("ListDueScheduled = %+v", due)
}
st.MarkScheduledRun(ctx, "c", now, now.Add(time.Hour))
if due, _ := st.ListDueScheduled(ctx, now); len(due) != 0 {
t.Errorf("after MarkScheduledRun nothing due: %+v", due)
}
}