Files
kandev/apps/backend/internal/plugins/state/instance_store_test.go
T
2026-09-05 18:44:43 +00:00

102 lines
3.3 KiB
Go

package state
import (
"context"
"database/sql"
"encoding/json"
"errors"
"path/filepath"
"testing"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/kandev/kandev/internal/db"
)
func newInstanceStoreTest(t *testing.T) *InstanceStore {
t.Helper()
conn, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "instance-state.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
conn.SetMaxOpenConns(1)
pool := db.NewPool(sqlx.NewDb(conn, "sqlite3"), sqlx.NewDb(conn, "sqlite3"))
t.Cleanup(func() { _ = pool.Close() })
store, err := NewInstanceStore(pool)
if err != nil {
t.Fatalf("NewInstanceStore: %v", err)
}
return store
}
func TestInstanceStoreUsesRevisionedConditionalWrites(t *testing.T) {
store := newInstanceStoreTest(t)
ctx := context.Background()
first, err := store.Set(ctx, "instance-1", "filters", json.RawMessage(`{"group":"step"}`), ptrInt64(0), "browser")
if err != nil {
t.Fatalf("initial Set: %v", err)
}
if first.Revision != 1 {
t.Fatalf("initial revision = %d, want 1", first.Revision)
}
second, err := store.Set(ctx, "instance-1", "filters", json.RawMessage(`{"group":"workflow"}`), ptrInt64(1), "agent")
if err != nil {
t.Fatalf("conditional Set: %v", err)
}
if second.Revision != 2 {
t.Fatalf("second revision = %d, want 2", second.Revision)
}
_, err = store.Set(ctx, "instance-1", "filters", json.RawMessage(`{"group":"stale"}`), ptrInt64(1), "browser")
var conflict *ConflictError
if !errors.As(err, &conflict) || conflict.CurrentRevision != 2 {
t.Fatalf("stale Set = %v, conflict = %+v", err, conflict)
}
got, found, err := store.Get(ctx, "instance-1", "filters")
if err != nil || !found || string(got.Value) != `{"group":"workflow"}` || got.Revision != 2 {
t.Fatalf("Get = %+v, found=%v, err=%v", got, found, err)
}
}
func TestInstanceStoreDeleteRetainsTombstoneRevision(t *testing.T) {
store := newInstanceStoreTest(t)
ctx := context.Background()
if _, err := store.Set(ctx, "instance-1", "draft", json.RawMessage(`1`), ptrInt64(0), "browser"); err != nil {
t.Fatalf("Set: %v", err)
}
revision, err := store.Delete(ctx, "instance-1", "draft", ptrInt64(1), "browser")
if err != nil || revision != 2 {
t.Fatalf("Delete = %d, %v; want revision 2", revision, err)
}
entry, found, err := store.Get(ctx, "instance-1", "draft")
if err != nil || found || entry.Revision != 2 {
t.Fatalf("Get tombstone = %+v, found=%v, err=%v", entry, found, err)
}
var conflict *ConflictError
if _, err := store.Set(ctx, "instance-1", "draft", json.RawMessage(`2`), ptrInt64(1), "browser"); !errors.As(err, &conflict) {
t.Fatalf("stale recreate = %v, want conflict", err)
}
}
func TestInstanceStoreDeleteInstanceRemovesAllState(t *testing.T) {
store := newInstanceStoreTest(t)
ctx := context.Background()
for _, key := range []string{"one", "two"} {
if _, err := store.Set(ctx, "instance-1", key, json.RawMessage(`{"ok":true}`), ptrInt64(0), "agent"); err != nil {
t.Fatalf("Set(%s): %v", key, err)
}
}
if err := store.DeleteInstance(ctx, "instance-1"); err != nil {
t.Fatalf("DeleteInstance: %v", err)
}
entries, err := store.List(ctx, "instance-1")
if err != nil {
t.Fatalf("List after DeleteInstance: %v", err)
}
if len(entries) != 0 {
t.Fatalf("state after DeleteInstance = %+v, want empty", entries)
}
}
func ptrInt64(value int64) *int64 { return &value }