Files
lvmh/daemon/store_test.go

188 lines
5.6 KiB
Go

package main
// store_test.go — persistence: events replay, sessions, settings, containers.
import (
"path/filepath"
"reflect"
"testing"
)
func openTestStore(t *testing.T) *Store {
t.Helper()
store, err := OpenStore(filepath.Join(t.TempDir(), "lvmh.db"))
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
return store
}
func TestStoreEventPersistAndReplayAfterSeq(t *testing.T) {
store := openTestStore(t)
events := []Event{
{SessionID: "s1", Seq: 1, TS: 1000, Type: evMessageStart, Payload: []byte(`{"message":{"role":"user","id":"m1"}}`)},
{SessionID: "s1", Seq: 2, TS: 1001, Type: evMessageEnd, Payload: []byte(`{"message":{"role":"user","id":"m1"}}`)},
{SessionID: "s1", Seq: 3, TS: 1002, Type: evAgentSettled, Payload: []byte(`{}`)},
}
for _, e := range events {
if err := store.AppendEvent(e); err != nil {
t.Fatalf("append seq %d: %v", e.Seq, err)
}
}
if last, _ := store.LastSeq("s1"); last != 3 {
t.Fatalf("lastSeq = %d, want 3", last)
}
all, err := store.EventsAfter("s1", 0, 100)
if err != nil {
t.Fatalf("events after 0: %v", err)
}
if len(all) != 3 {
t.Fatalf("after=0 returned %d events, want 3", len(all))
}
for i, e := range all {
if e.Seq != int64(i+1) {
t.Fatalf("event %d has seq %d, want ascending from 1", i, e.Seq)
}
}
tail, err := store.EventsAfter("s1", 2, 100)
if err != nil {
t.Fatalf("events after 2: %v", err)
}
if len(tail) != 1 || tail[0].Seq != 3 {
t.Fatalf("after=2 returned %v, want only seq 3", tail)
}
limited, _ := store.EventsAfter("s1", 0, 2)
if len(limited) != 2 {
t.Fatalf("limit=2 returned %d events", len(limited))
}
// Duplicate (sessionId, seq) is ignored — replay idempotence.
if err := store.AppendEvent(events[0]); err != nil {
t.Fatalf("duplicate append: %v", err)
}
dup, _ := store.EventsAfter("s1", 0, 100)
if len(dup) != 3 {
t.Fatalf("duplicate append created extra row: %d", len(dup))
}
}
func TestStoreSessionsOnlineAndTouch(t *testing.T) {
store := openTestStore(t)
name := "my session"
info := SessionInfo{ID: "s1", Name: &name, Cwd: "/w", Model: "glm-5.3", Provider: "zai-renaud", Agent: true, Repo: nil, StartedAt: 42}
if err := store.UpsertSession(info); err != nil {
t.Fatalf("upsert: %v", err)
}
if err := store.TouchSession("s1", 7, 4242); err != nil {
t.Fatalf("touch: %v", err)
}
if err := store.TouchSession("s1", 3, 9999); err != nil { // lastSeq monotonic
t.Fatalf("touch lower: %v", err)
}
if err := store.SetOnline("s1", true); err != nil {
t.Fatalf("online: %v", err)
}
rows, err := store.Sessions()
if err != nil || len(rows) != 1 {
t.Fatalf("sessions: %v %d", err, len(rows))
}
row := rows[0]
if row.LastSeq != 7 || row.LastEventAt != 9999 || !row.OnlineDB {
t.Fatalf("row = %+v, want lastSeq 7, lastEventAt 9999, online", row)
}
if row.Info.Name == nil || *row.Info.Name != name {
t.Fatalf("name not roundtripped: %+v", row.Info)
}
if last, _ := store.LastSeq("nope"); last != 0 {
t.Fatalf("missing session lastSeq = %d, want 0", last)
}
}
func TestStoreSettingsAndContainers(t *testing.T) {
store := openTestStore(t)
if _, ok, _ := store.GetSetting(settingGitLabToken); ok {
t.Fatal("unset setting should not exist")
}
if err := store.SetSetting(settingGitLabToken, "pat"); err != nil {
t.Fatalf("set: %v", err)
}
if v, ok, _ := store.GetSetting(settingGitLabToken); !ok || v != "pat" {
t.Fatalf("get = %q %v", v, ok)
}
if err := store.DeleteSetting(settingGitLabToken); err != nil {
t.Fatalf("delete: %v", err)
}
if err := store.UpsertContainer("s1", "cid1", "group/proj"); err != nil {
t.Fatalf("upsert container: %v", err)
}
row, ok, err := store.GetContainer("s1")
if err != nil || !ok || row.ContainerID != "cid1" || row.Repo != "group/proj" {
t.Fatalf("get container = %+v %v %v", row, ok, err)
}
if err := store.DeleteContainer("s1"); err != nil {
t.Fatalf("delete container: %v", err)
}
if _, ok, _ := store.GetContainer("s1"); ok {
t.Fatal("container row should be gone")
}
}
func TestStoreRepoImages(t *testing.T) {
store := openTestStore(t)
if _, ok, err := store.GetRepoImage("group/proj"); err != nil || ok {
t.Fatalf("missing repo image = %v %v, want absent no-error", ok, err)
}
if rows, err := store.ListRepoImages(); err != nil || rows != nil {
t.Fatalf("empty list = %v %v, want nil no-error", rows, err)
}
if err := store.SetRepoImage("group/proj", "lvmh-worker-g--p-ab12cd"); err != nil {
t.Fatalf("set: %v", err)
}
if img, ok, _ := store.GetRepoImage("group/proj"); !ok || img != "lvmh-worker-g--p-ab12cd" {
t.Fatalf("get = %q %v", img, ok)
}
// upsert replaces
if err := store.SetRepoImage("group/proj", "lvmh-worker-g--p-ff99ee"); err != nil {
t.Fatalf("upsert: %v", err)
}
if img, _, _ := store.GetRepoImage("group/proj"); img != "lvmh-worker-g--p-ff99ee" {
t.Fatalf("after upsert = %q", img)
}
if err := store.SetRepoImage("other/repo", "lvmh-worker-o--r-11aabb"); err != nil {
t.Fatalf("set second: %v", err)
}
rows, err := store.ListRepoImages()
if err != nil {
t.Fatalf("list: %v", err)
}
want := []RepoImageRow{
{Repo: "group/proj", Image: "lvmh-worker-g--p-ff99ee"},
{Repo: "other/repo", Image: "lvmh-worker-o--r-11aabb"},
}
if !reflect.DeepEqual(rows, want) {
t.Fatalf("list = %+v, want %+v (sorted by repo)", rows, want)
}
if err := store.DeleteRepoImage("group/proj"); err != nil {
t.Fatalf("delete: %v", err)
}
if _, ok, _ := store.GetRepoImage("group/proj"); ok {
t.Fatal("repo image should be gone")
}
// idempotent delete
if err := store.DeleteRepoImage("group/proj"); err != nil {
t.Fatalf("delete absent: %v", err)
}
}