- Add ErrVersionConflict error type and VersionConflictError for detailed conflict information - Implement version validation in InMemoryEventStore.SaveEvent that rejects events with version <= current latest version - Implement version validation in JetStreamEventStore.SaveEvent with version caching for performance - Add comprehensive tests for version conflict detection including concurrent writes to same actor - Document versioning semantics in EventStore interface and CLAUDE.md This ensures events have monotonically increasing versions per actor and provides clear error messages for version conflicts, enabling optimistic concurrency control patterns. Closes #6 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1689 lines
42 KiB
Go
1689 lines
42 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.flowmade.one/flowmade-one/aether"
|
|
)
|
|
|
|
// === Event Store Tests ===
|
|
|
|
func TestNewInMemoryEventStore(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
if store == nil {
|
|
t.Fatal("NewInMemoryEventStore returned nil")
|
|
}
|
|
if store.events == nil {
|
|
t.Error("events map is nil")
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_SingleEvent(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
event := &aether.Event{
|
|
ID: "evt-123",
|
|
EventType: "OrderPlaced",
|
|
ActorID: "order-456",
|
|
Version: 1,
|
|
Data: map[string]interface{}{
|
|
"total": 100.50,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
err := store.SaveEvent(event)
|
|
if err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
// Verify event was persisted
|
|
events, err := store.GetEvents("order-456", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Fatalf("expected 1 event, got %d", len(events))
|
|
}
|
|
if events[0].ID != "evt-123" {
|
|
t.Errorf("event ID mismatch: got %q, want %q", events[0].ID, "evt-123")
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_MultipleEvents(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
for i := 1; i <= 5; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "OrderUpdated",
|
|
ActorID: "order-456",
|
|
Version: int64(i),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed for event %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
events, err := store.GetEvents("order-456", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) != 5 {
|
|
t.Errorf("expected 5 events, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_MultipleActors(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save events for different actors
|
|
actors := []string{"actor-1", "actor-2", "actor-3"}
|
|
for _, actorID := range actors {
|
|
for i := 1; i <= 3; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%s-%d", actorID, i),
|
|
EventType: "TestEvent",
|
|
ActorID: actorID,
|
|
Version: int64(i),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify each actor has its own events
|
|
for _, actorID := range actors {
|
|
events, err := store.GetEvents(actorID, 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed for %s: %v", actorID, err)
|
|
}
|
|
if len(events) != 3 {
|
|
t.Errorf("expected 3 events for %s, got %d", actorID, len(events))
|
|
}
|
|
for _, event := range events {
|
|
if event.ActorID != actorID {
|
|
t.Errorf("event has wrong ActorID: got %q, want %q", event.ActorID, actorID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_PreservesAllFields(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
ts := time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC)
|
|
event := &aether.Event{
|
|
ID: "evt-123",
|
|
EventType: "OrderPlaced",
|
|
ActorID: "order-456",
|
|
CommandID: "cmd-789",
|
|
Version: 42,
|
|
Data: map[string]interface{}{
|
|
"total": 100.50,
|
|
"currency": "USD",
|
|
},
|
|
Timestamp: ts,
|
|
}
|
|
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
events, err := store.GetEvents("order-456", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
|
|
retrieved := events[0]
|
|
if retrieved.ID != event.ID {
|
|
t.Errorf("ID mismatch: got %q, want %q", retrieved.ID, event.ID)
|
|
}
|
|
if retrieved.EventType != event.EventType {
|
|
t.Errorf("EventType mismatch: got %q, want %q", retrieved.EventType, event.EventType)
|
|
}
|
|
if retrieved.ActorID != event.ActorID {
|
|
t.Errorf("ActorID mismatch: got %q, want %q", retrieved.ActorID, event.ActorID)
|
|
}
|
|
if retrieved.CommandID != event.CommandID {
|
|
t.Errorf("CommandID mismatch: got %q, want %q", retrieved.CommandID, event.CommandID)
|
|
}
|
|
if retrieved.Version != event.Version {
|
|
t.Errorf("Version mismatch: got %d, want %d", retrieved.Version, event.Version)
|
|
}
|
|
if !retrieved.Timestamp.Equal(event.Timestamp) {
|
|
t.Errorf("Timestamp mismatch: got %v, want %v", retrieved.Timestamp, event.Timestamp)
|
|
}
|
|
if retrieved.Data["total"] != event.Data["total"] {
|
|
t.Errorf("Data.total mismatch: got %v, want %v", retrieved.Data["total"], event.Data["total"])
|
|
}
|
|
if retrieved.Data["currency"] != event.Data["currency"] {
|
|
t.Errorf("Data.currency mismatch: got %v, want %v", retrieved.Data["currency"], event.Data["currency"])
|
|
}
|
|
}
|
|
|
|
func TestGetEvents_RetrievesInOrder(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save events in order
|
|
for i := 1; i <= 10; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: int64(i),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
|
|
// Verify events are returned in insertion order
|
|
for i, event := range events {
|
|
expectedID := fmt.Sprintf("evt-%d", i+1)
|
|
if event.ID != expectedID {
|
|
t.Errorf("event %d: got ID %q, want %q", i, event.ID, expectedID)
|
|
}
|
|
expectedVersion := int64(i + 1)
|
|
if event.Version != expectedVersion {
|
|
t.Errorf("event %d: got Version %d, want %d", i, event.Version, expectedVersion)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetEvents_FromVersionFilters(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save events with versions 1-10
|
|
for i := 1; i <= 10; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: int64(i),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
fromVersion int64
|
|
expected int
|
|
minVersion int64
|
|
}{
|
|
{"from version 0", 0, 10, 1},
|
|
{"from version 1", 1, 10, 1},
|
|
{"from version 5", 5, 6, 5},
|
|
{"from version 10", 10, 1, 10},
|
|
{"from version 11", 11, 0, 0},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
events, err := store.GetEvents("actor-123", tc.fromVersion)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
|
|
if len(events) != tc.expected {
|
|
t.Errorf("expected %d events, got %d", tc.expected, len(events))
|
|
}
|
|
|
|
// Verify all returned events have version >= fromVersion
|
|
for _, event := range events {
|
|
if event.Version < tc.fromVersion {
|
|
t.Errorf("event version %d is less than fromVersion %d", event.Version, tc.fromVersion)
|
|
}
|
|
}
|
|
|
|
// Verify minimum version if events exist
|
|
if len(events) > 0 && events[0].Version != tc.minVersion {
|
|
t.Errorf("first event version: got %d, want %d", events[0].Version, tc.minVersion)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetEvents_FromVersionZero(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
event := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 1,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
// fromVersion 0 should return all events
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Errorf("expected 1 event with fromVersion 0, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestGetLatestVersion_ReturnsCorrectVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save events with strictly increasing versions
|
|
versions := []int64{1, 2, 3, 4, 5}
|
|
for i, version := range versions {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: version,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
|
|
latestVersion, err := store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion failed: %v", err)
|
|
}
|
|
|
|
// Should return the highest version (5)
|
|
if latestVersion != 5 {
|
|
t.Errorf("expected latest version 5, got %d", latestVersion)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestVersion_SingleEvent(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
event := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 42,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
latestVersion, err := store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion failed: %v", err)
|
|
}
|
|
|
|
if latestVersion != 42 {
|
|
t.Errorf("expected latest version 42, got %d", latestVersion)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestVersion_UpdatesAfterNewEvent(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save first event
|
|
event1 := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 1,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event1); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
version1, err := store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion failed: %v", err)
|
|
}
|
|
if version1 != 1 {
|
|
t.Errorf("expected version 1, got %d", version1)
|
|
}
|
|
|
|
// Save second event with higher version
|
|
event2 := &aether.Event{
|
|
ID: "evt-2",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 10,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event2); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
version2, err := store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion failed: %v", err)
|
|
}
|
|
if version2 != 10 {
|
|
t.Errorf("expected version 10, got %d", version2)
|
|
}
|
|
}
|
|
|
|
func TestGetEvents_NonExistentActor(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save event for one actor
|
|
event := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 1,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
// Get events for non-existent actor
|
|
events, err := store.GetEvents("non-existent-actor", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents should not error for non-existent actor: %v", err)
|
|
}
|
|
|
|
if events == nil {
|
|
t.Error("GetEvents should return non-nil slice for non-existent actor")
|
|
}
|
|
if len(events) != 0 {
|
|
t.Errorf("expected 0 events for non-existent actor, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestGetLatestVersion_NonExistentActor(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Get latest version for non-existent actor
|
|
version, err := store.GetLatestVersion("non-existent-actor")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion should not error for non-existent actor: %v", err)
|
|
}
|
|
|
|
if version != 0 {
|
|
t.Errorf("expected version 0 for non-existent actor, got %d", version)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestVersion_EmptyStore(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
version, err := store.GetLatestVersion("any-actor")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion should not error for empty store: %v", err)
|
|
}
|
|
|
|
if version != 0 {
|
|
t.Errorf("expected version 0 for empty store, got %d", version)
|
|
}
|
|
}
|
|
|
|
func TestGetEvents_EmptyStore(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
events, err := store.GetEvents("any-actor", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents should not error for empty store: %v", err)
|
|
}
|
|
|
|
if events == nil {
|
|
t.Error("GetEvents should return non-nil slice for empty store")
|
|
}
|
|
if len(events) != 0 {
|
|
t.Errorf("expected 0 events for empty store, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestConcurrentSaveEvent(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
numGoroutines := 100
|
|
eventsPerGoroutine := 10
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(numGoroutines)
|
|
|
|
for g := 0; g < numGoroutines; g++ {
|
|
go func(goroutineID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < eventsPerGoroutine; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d-%d", goroutineID, i),
|
|
EventType: "TestEvent",
|
|
ActorID: fmt.Sprintf("actor-%d", goroutineID),
|
|
Version: int64(i + 1),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Errorf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
}(g)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify each actor has the correct number of events
|
|
for g := 0; g < numGoroutines; g++ {
|
|
actorID := fmt.Sprintf("actor-%d", g)
|
|
events, err := store.GetEvents(actorID, 0)
|
|
if err != nil {
|
|
t.Errorf("GetEvents failed for %s: %v", actorID, err)
|
|
continue
|
|
}
|
|
if len(events) != eventsPerGoroutine {
|
|
t.Errorf("expected %d events for %s, got %d", eventsPerGoroutine, actorID, len(events))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcurrentSaveAndGet(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Pre-populate with some events
|
|
for i := 1; i <= 10; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: int64(i),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
numReaders := 50
|
|
numWriters := 10
|
|
readsPerReader := 100
|
|
writesPerWriter := 10
|
|
|
|
// Start readers
|
|
wg.Add(numReaders)
|
|
for r := 0; r < numReaders; r++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < readsPerReader; i++ {
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Errorf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) < 10 {
|
|
t.Errorf("expected at least 10 events, got %d", len(events))
|
|
}
|
|
|
|
_, err = store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Errorf("GetLatestVersion failed: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Start writers - each writer gets their own actor to avoid version conflicts
|
|
wg.Add(numWriters)
|
|
for w := 0; w < numWriters; w++ {
|
|
go func(writerID int) {
|
|
defer wg.Done()
|
|
actorID := fmt.Sprintf("writer-actor-%d", writerID)
|
|
for i := 0; i < writesPerWriter; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-writer-%d-%d", writerID, i),
|
|
EventType: "TestEvent",
|
|
ActorID: actorID,
|
|
Version: int64(i + 1),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Errorf("SaveEvent failed: %v", err)
|
|
}
|
|
}
|
|
}(w)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify actor-123 still has its original events
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
|
|
if len(events) != 10 {
|
|
t.Errorf("expected 10 events for actor-123, got %d", len(events))
|
|
}
|
|
|
|
// Verify each writer's actor has the correct events
|
|
for w := 0; w < numWriters; w++ {
|
|
actorID := fmt.Sprintf("writer-actor-%d", w)
|
|
events, err := store.GetEvents(actorID, 0)
|
|
if err != nil {
|
|
t.Errorf("GetEvents failed for %s: %v", actorID, err)
|
|
continue
|
|
}
|
|
if len(events) != writesPerWriter {
|
|
t.Errorf("expected %d events for %s, got %d", writesPerWriter, actorID, len(events))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcurrentGetLatestVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save initial event
|
|
event := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 100,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
wg.Add(numGoroutines)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
version, err := store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Errorf("GetLatestVersion failed: %v", err)
|
|
}
|
|
if version < 100 {
|
|
t.Errorf("expected version >= 100, got %d", version)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestEventStoreInterface(t *testing.T) {
|
|
// Verify InMemoryEventStore implements EventStore interface
|
|
var _ aether.EventStore = (*InMemoryEventStore)(nil)
|
|
}
|
|
|
|
func TestSaveEvent_NilData(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
event := &aether.Event{
|
|
ID: "evt-nil",
|
|
EventType: "NilDataEvent",
|
|
ActorID: "actor-123",
|
|
Version: 1,
|
|
Data: nil,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed with nil data: %v", err)
|
|
}
|
|
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
|
|
if events[0].Data != nil {
|
|
t.Errorf("expected nil Data, got %v", events[0].Data)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_EmptyData(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
event := &aether.Event{
|
|
ID: "evt-empty",
|
|
EventType: "EmptyDataEvent",
|
|
ActorID: "actor-123",
|
|
Version: 1,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed with empty data: %v", err)
|
|
}
|
|
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
|
|
if len(events[0].Data) != 0 {
|
|
t.Errorf("expected empty Data map, got %v", events[0].Data)
|
|
}
|
|
}
|
|
|
|
func TestGetEvents_VersionEdgeCases(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save events with edge case versions (strictly increasing)
|
|
versions := []int64{1, 100, 9223372036854775807} // one, 100, MaxInt64
|
|
for i, version := range versions {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: version,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed for version %d: %v", version, err)
|
|
}
|
|
}
|
|
|
|
// Test fromVersion 0 returns all
|
|
events, err := store.GetEvents("actor-123", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) != 3 {
|
|
t.Errorf("expected 3 events, got %d", len(events))
|
|
}
|
|
|
|
// Test fromVersion MaxInt64 returns only that event
|
|
events, err = store.GetEvents("actor-123", 9223372036854775807)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Errorf("expected 1 event, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func TestGetLatestVersion_VersionEdgeCases(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save event with MaxInt64 version
|
|
event := &aether.Event{
|
|
ID: "evt-max",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 9223372036854775807,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Fatalf("SaveEvent failed: %v", err)
|
|
}
|
|
|
|
latestVersion, err := store.GetLatestVersion("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestVersion failed: %v", err)
|
|
}
|
|
|
|
if latestVersion != 9223372036854775807 {
|
|
t.Errorf("expected MaxInt64 version, got %d", latestVersion)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_SpecialActorIDs(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
specialIDs := []string{
|
|
"simple",
|
|
"with-dashes",
|
|
"with_underscores",
|
|
"with.dots",
|
|
"with:colons",
|
|
"with/slashes",
|
|
"user@example.com",
|
|
"unicode-世界",
|
|
"",
|
|
}
|
|
|
|
for _, actorID := range specialIDs {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%s", actorID),
|
|
EventType: "TestEvent",
|
|
ActorID: actorID,
|
|
Version: 1,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event); err != nil {
|
|
t.Errorf("SaveEvent failed for actorID %q: %v", actorID, err)
|
|
continue
|
|
}
|
|
|
|
events, err := store.GetEvents(actorID, 0)
|
|
if err != nil {
|
|
t.Errorf("GetEvents failed for actorID %q: %v", actorID, err)
|
|
continue
|
|
}
|
|
if len(events) != 1 {
|
|
t.Errorf("expected 1 event for actorID %q, got %d", actorID, len(events))
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Version Validation Tests ===
|
|
|
|
func TestSaveEvent_RejectsLowerVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save first event with version 5
|
|
event1 := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 5,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event1); err != nil {
|
|
t.Fatalf("SaveEvent failed for first event: %v", err)
|
|
}
|
|
|
|
// Attempt to save event with lower version (should fail)
|
|
event2 := &aether.Event{
|
|
ID: "evt-2",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 3,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
err := store.SaveEvent(event2)
|
|
if err == nil {
|
|
t.Fatal("expected error when saving event with lower version, got nil")
|
|
}
|
|
|
|
// Verify it's a VersionConflictError
|
|
if !errors.Is(err, aether.ErrVersionConflict) {
|
|
t.Errorf("expected ErrVersionConflict, got %v", err)
|
|
}
|
|
|
|
var versionErr *aether.VersionConflictError
|
|
if !errors.As(err, &versionErr) {
|
|
t.Fatalf("expected VersionConflictError, got %T", err)
|
|
}
|
|
|
|
if versionErr.ActorID != "actor-123" {
|
|
t.Errorf("ActorID mismatch: got %q, want %q", versionErr.ActorID, "actor-123")
|
|
}
|
|
if versionErr.CurrentVersion != 5 {
|
|
t.Errorf("CurrentVersion mismatch: got %d, want %d", versionErr.CurrentVersion, 5)
|
|
}
|
|
if versionErr.AttemptedVersion != 3 {
|
|
t.Errorf("AttemptedVersion mismatch: got %d, want %d", versionErr.AttemptedVersion, 3)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_RejectsEqualVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save first event with version 5
|
|
event1 := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 5,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveEvent(event1); err != nil {
|
|
t.Fatalf("SaveEvent failed for first event: %v", err)
|
|
}
|
|
|
|
// Attempt to save event with equal version (should fail)
|
|
event2 := &aether.Event{
|
|
ID: "evt-2",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: 5,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
err := store.SaveEvent(event2)
|
|
if err == nil {
|
|
t.Fatal("expected error when saving event with equal version, got nil")
|
|
}
|
|
|
|
if !errors.Is(err, aether.ErrVersionConflict) {
|
|
t.Errorf("expected ErrVersionConflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_RejectsZeroVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Version 0 should be rejected (not strictly greater than initial 0)
|
|
event := &aether.Event{
|
|
ID: "evt-1",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-new",
|
|
Version: 0,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
err := store.SaveEvent(event)
|
|
if err == nil {
|
|
t.Fatal("expected error when saving event with version 0, got nil")
|
|
}
|
|
|
|
if !errors.Is(err, aether.ErrVersionConflict) {
|
|
t.Errorf("expected ErrVersionConflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_RejectsNegativeVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
event := &aether.Event{
|
|
ID: "evt-neg",
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-123",
|
|
Version: -1,
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
err := store.SaveEvent(event)
|
|
if err == nil {
|
|
t.Fatal("expected error when saving event with negative version, got nil")
|
|
}
|
|
|
|
if !errors.Is(err, aether.ErrVersionConflict) {
|
|
t.Errorf("expected ErrVersionConflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvent_ConcurrentWritesToSameActor(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
numGoroutines := 100
|
|
var successCount int64
|
|
var conflictCount int64
|
|
var wg sync.WaitGroup
|
|
|
|
// All goroutines try to save version 1
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", id),
|
|
EventType: "TestEvent",
|
|
ActorID: "actor-contested",
|
|
Version: 1,
|
|
Data: map[string]interface{}{"goroutine": id},
|
|
Timestamp: time.Now(),
|
|
}
|
|
err := store.SaveEvent(event)
|
|
if err == nil {
|
|
atomic.AddInt64(&successCount, 1)
|
|
} else if errors.Is(err, aether.ErrVersionConflict) {
|
|
atomic.AddInt64(&conflictCount, 1)
|
|
} else {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Exactly one should succeed, rest should conflict
|
|
if successCount != 1 {
|
|
t.Errorf("expected exactly 1 success, got %d", successCount)
|
|
}
|
|
if conflictCount != int64(numGoroutines-1) {
|
|
t.Errorf("expected %d conflicts, got %d", numGoroutines-1, conflictCount)
|
|
}
|
|
|
|
// Verify only one event was stored
|
|
events, err := store.GetEvents("actor-contested", 0)
|
|
if err != nil {
|
|
t.Fatalf("GetEvents failed: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Errorf("expected 1 event, got %d", len(events))
|
|
}
|
|
}
|
|
|
|
func BenchmarkSaveEvent(b *testing.B) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "BenchmarkEvent",
|
|
ActorID: "actor-123",
|
|
Version: int64(i + 1),
|
|
Data: map[string]interface{}{"value": i},
|
|
Timestamp: time.Now(),
|
|
}
|
|
store.SaveEvent(event)
|
|
}
|
|
}
|
|
|
|
func BenchmarkGetEvents(b *testing.B) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Pre-populate with events
|
|
for i := 0; i < 1000; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "BenchmarkEvent",
|
|
ActorID: "actor-123",
|
|
Version: int64(i + 1),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
store.SaveEvent(event)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
store.GetEvents("actor-123", 0)
|
|
}
|
|
}
|
|
|
|
func BenchmarkGetLatestVersion(b *testing.B) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Pre-populate with events
|
|
for i := 0; i < 1000; i++ {
|
|
event := &aether.Event{
|
|
ID: fmt.Sprintf("evt-%d", i),
|
|
EventType: "BenchmarkEvent",
|
|
ActorID: "actor-123",
|
|
Version: int64(i + 1),
|
|
Data: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
store.SaveEvent(event)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
store.GetLatestVersion("actor-123")
|
|
}
|
|
}
|
|
|
|
// === Snapshot Store Tests ===
|
|
|
|
func TestSaveSnapshot_PersistsCorrectly(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-123",
|
|
Version: 10,
|
|
State: map[string]interface{}{
|
|
"balance": 100.50,
|
|
"status": "active",
|
|
},
|
|
Timestamp: time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
|
|
err := store.SaveSnapshot(snapshot)
|
|
if err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
// Verify snapshot was persisted by retrieving it
|
|
retrieved, err := store.GetLatestSnapshot("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if retrieved == nil {
|
|
t.Fatal("expected snapshot to be persisted, got nil")
|
|
}
|
|
|
|
if retrieved.ActorID != snapshot.ActorID {
|
|
t.Errorf("ActorID mismatch: got %q, want %q", retrieved.ActorID, snapshot.ActorID)
|
|
}
|
|
if retrieved.Version != snapshot.Version {
|
|
t.Errorf("Version mismatch: got %d, want %d", retrieved.Version, snapshot.Version)
|
|
}
|
|
if retrieved.State["balance"] != snapshot.State["balance"] {
|
|
t.Errorf("State.balance mismatch: got %v, want %v", retrieved.State["balance"], snapshot.State["balance"])
|
|
}
|
|
if retrieved.State["status"] != snapshot.State["status"] {
|
|
t.Errorf("State.status mismatch: got %v, want %v", retrieved.State["status"], snapshot.State["status"])
|
|
}
|
|
if !retrieved.Timestamp.Equal(snapshot.Timestamp) {
|
|
t.Errorf("Timestamp mismatch: got %v, want %v", retrieved.Timestamp, snapshot.Timestamp)
|
|
}
|
|
}
|
|
|
|
func TestSaveSnapshot_NilSnapshot(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
err := store.SaveSnapshot(nil)
|
|
if err == nil {
|
|
t.Error("expected error when saving nil snapshot, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSaveSnapshot_MultipleSnapshots(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save multiple snapshots for the same actor
|
|
for i := 1; i <= 5; i++ {
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-multi",
|
|
Version: int64(i * 10),
|
|
State: map[string]interface{}{
|
|
"iteration": i,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed for version %d: %v", i*10, err)
|
|
}
|
|
}
|
|
|
|
// Verify all snapshots were saved by checking the latest
|
|
retrieved, err := store.GetLatestSnapshot("actor-multi")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if retrieved.Version != 50 {
|
|
t.Errorf("expected latest version 50, got %d", retrieved.Version)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestSnapshot_ReturnsMostRecent(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save snapshots in non-sequential order to test version comparison
|
|
versions := []int64{5, 15, 10, 25, 20}
|
|
for _, v := range versions {
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-latest",
|
|
Version: v,
|
|
State: map[string]interface{}{
|
|
"version": v,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed for version %d: %v", v, err)
|
|
}
|
|
}
|
|
|
|
latest, err := store.GetLatestSnapshot("actor-latest")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if latest == nil {
|
|
t.Fatal("expected snapshot, got nil")
|
|
}
|
|
|
|
if latest.Version != 25 {
|
|
t.Errorf("expected latest version 25, got %d", latest.Version)
|
|
}
|
|
|
|
// Verify the state matches the snapshot with version 25
|
|
if latest.State["version"].(int64) != 25 {
|
|
t.Errorf("expected state.version to be 25, got %v", latest.State["version"])
|
|
}
|
|
}
|
|
|
|
func TestGetLatestSnapshot_NoSnapshotExists(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Query for a non-existent actor
|
|
snapshot, err := store.GetLatestSnapshot("non-existent-actor")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if snapshot != nil {
|
|
t.Errorf("expected nil for non-existent actor, got %+v", snapshot)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestSnapshot_EmptyActorID(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save a snapshot with empty actor ID
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "",
|
|
Version: 1,
|
|
State: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
// Retrieve with empty actor ID
|
|
retrieved, err := store.GetLatestSnapshot("")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if retrieved == nil {
|
|
t.Fatal("expected snapshot with empty actorID, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSnapshotVersioning_RespectedAcrossActors(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save snapshots for different actors
|
|
actors := []string{"actor-a", "actor-b", "actor-c"}
|
|
for i, actorID := range actors {
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: actorID,
|
|
Version: int64((i + 1) * 100), // Different versions per actor
|
|
State: map[string]interface{}{
|
|
"actor": actorID,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed for %s: %v", actorID, err)
|
|
}
|
|
}
|
|
|
|
// Verify each actor has their own snapshot with correct version
|
|
for i, actorID := range actors {
|
|
snapshot, err := store.GetLatestSnapshot(actorID)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed for %s: %v", actorID, err)
|
|
}
|
|
|
|
expectedVersion := int64((i + 1) * 100)
|
|
if snapshot.Version != expectedVersion {
|
|
t.Errorf("actor %s: expected version %d, got %d", actorID, expectedVersion, snapshot.Version)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSnapshotVersioning_LowerVersionAfterHigher(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Save a high version first
|
|
highSnapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-order",
|
|
Version: 100,
|
|
State: map[string]interface{}{
|
|
"marker": "high",
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(highSnapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
// Save a lower version after
|
|
lowSnapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-order",
|
|
Version: 50,
|
|
State: map[string]interface{}{
|
|
"marker": "low",
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(lowSnapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
// GetLatestSnapshot should return the higher version (100), not the most recently saved
|
|
latest, err := store.GetLatestSnapshot("actor-order")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if latest.Version != 100 {
|
|
t.Errorf("expected version 100, got %d", latest.Version)
|
|
}
|
|
if latest.State["marker"] != "high" {
|
|
t.Errorf("expected marker 'high', got %v", latest.State["marker"])
|
|
}
|
|
}
|
|
|
|
func TestSnapshotDataIntegrity_ComplexState(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
complexState := map[string]interface{}{
|
|
"string": "hello",
|
|
"integer": 42,
|
|
"float": 3.14159,
|
|
"boolean": true,
|
|
"null": nil,
|
|
"array": []interface{}{"a", "b", "c"},
|
|
"nested": map[string]interface{}{
|
|
"level1": map[string]interface{}{
|
|
"level2": "deep value",
|
|
},
|
|
},
|
|
}
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-complex",
|
|
Version: 1,
|
|
State: complexState,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-complex")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
// Verify all fields
|
|
if retrieved.State["string"] != "hello" {
|
|
t.Errorf("string mismatch: got %v", retrieved.State["string"])
|
|
}
|
|
if retrieved.State["integer"] != 42 {
|
|
t.Errorf("integer mismatch: got %v", retrieved.State["integer"])
|
|
}
|
|
if retrieved.State["float"] != 3.14159 {
|
|
t.Errorf("float mismatch: got %v", retrieved.State["float"])
|
|
}
|
|
if retrieved.State["boolean"] != true {
|
|
t.Errorf("boolean mismatch: got %v", retrieved.State["boolean"])
|
|
}
|
|
if retrieved.State["null"] != nil {
|
|
t.Errorf("null mismatch: got %v", retrieved.State["null"])
|
|
}
|
|
|
|
// Verify array
|
|
arr, ok := retrieved.State["array"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("array is not []interface{}")
|
|
}
|
|
if len(arr) != 3 || arr[0] != "a" || arr[1] != "b" || arr[2] != "c" {
|
|
t.Errorf("array mismatch: got %v", arr)
|
|
}
|
|
|
|
// Verify nested structure
|
|
nested, ok := retrieved.State["nested"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("nested is not map[string]interface{}")
|
|
}
|
|
level1, ok := nested["level1"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("level1 is not map[string]interface{}")
|
|
}
|
|
if level1["level2"] != "deep value" {
|
|
t.Errorf("nested value mismatch: got %v", level1["level2"])
|
|
}
|
|
}
|
|
|
|
func TestSnapshotDataIntegrity_SpecialCharacters(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
specialState := map[string]interface{}{
|
|
"unicode": "Hello, 世界!",
|
|
"emoji": "😀🚀",
|
|
"newlines": "line1\nline2\r\nline3",
|
|
"tabs": "col1\tcol2",
|
|
"quotes": `"double" and 'single'`,
|
|
"backslash": `path\to\file`,
|
|
}
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-special",
|
|
Version: 1,
|
|
State: specialState,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-special")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
for key, expected := range specialState {
|
|
if retrieved.State[key] != expected {
|
|
t.Errorf("State[%q] mismatch: got %q, want %q", key, retrieved.State[key], expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSnapshotDataIntegrity_EmptyState(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-empty",
|
|
Version: 1,
|
|
State: map[string]interface{}{},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-empty")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if len(retrieved.State) != 0 {
|
|
t.Errorf("expected empty state, got %v", retrieved.State)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotDataIntegrity_NilState(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-nil",
|
|
Version: 1,
|
|
State: nil,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-nil")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if retrieved.State != nil {
|
|
t.Errorf("expected nil state, got %v", retrieved.State)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotDataIntegrity_LargeState(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Create a large state with many entries using unique keys
|
|
largeState := make(map[string]interface{})
|
|
for i := 0; i < 1000; i++ {
|
|
largeState[fmt.Sprintf("key-%d", i)] = i
|
|
}
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-large",
|
|
Version: 1,
|
|
State: largeState,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-large")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if len(retrieved.State) != len(largeState) {
|
|
t.Errorf("state size mismatch: got %d, want %d", len(retrieved.State), len(largeState))
|
|
}
|
|
}
|
|
|
|
func TestSnapshotDataIntegrity_TimestampPreserved(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Test various timestamps
|
|
timestamps := []time.Time{
|
|
time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC),
|
|
time.Date(2020, 6, 15, 23, 59, 59, 999999999, time.UTC),
|
|
time.Time{}, // Zero time
|
|
}
|
|
|
|
for i, ts := range timestamps {
|
|
actorID := fmt.Sprintf("actor-ts-%d", i)
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: actorID,
|
|
Version: 1,
|
|
State: map[string]interface{}{},
|
|
Timestamp: ts,
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot(actorID)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if !retrieved.Timestamp.Equal(ts) {
|
|
t.Errorf("timestamp %d mismatch: got %v, want %v", i, retrieved.Timestamp, ts)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSnapshotVersioning_ZeroVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-zero-version",
|
|
Version: 0,
|
|
State: map[string]interface{}{"initial": true},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-zero-version")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if retrieved.Version != 0 {
|
|
t.Errorf("expected version 0, got %d", retrieved.Version)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotVersioning_LargeVersion(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
largeVersion := int64(9223372036854775807) // MaxInt64
|
|
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-large-version",
|
|
Version: largeVersion,
|
|
State: map[string]interface{}{"maxed": true},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
retrieved, err := store.GetLatestSnapshot("actor-large-version")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
|
|
if retrieved.Version != largeVersion {
|
|
t.Errorf("expected version %d, got %d", largeVersion, retrieved.Version)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotStore_ImplementsInterface(t *testing.T) {
|
|
// Verify InMemoryEventStore implements SnapshotStore interface
|
|
var _ aether.SnapshotStore = (*InMemoryEventStore)(nil)
|
|
}
|
|
|
|
func TestConcurrentSaveSnapshot(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
numGoroutines := 100
|
|
snapshotsPerGoroutine := 10
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(numGoroutines)
|
|
|
|
for g := 0; g < numGoroutines; g++ {
|
|
go func(goroutineID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < snapshotsPerGoroutine; i++ {
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: fmt.Sprintf("actor-%d", goroutineID),
|
|
Version: int64(i + 1),
|
|
State: map[string]interface{}{
|
|
"goroutine": goroutineID,
|
|
"iteration": i,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Errorf("SaveSnapshot failed: %v", err)
|
|
}
|
|
}
|
|
}(g)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify each actor has snapshots
|
|
for g := 0; g < numGoroutines; g++ {
|
|
actorID := fmt.Sprintf("actor-%d", g)
|
|
snapshot, err := store.GetLatestSnapshot(actorID)
|
|
if err != nil {
|
|
t.Errorf("GetLatestSnapshot failed for %s: %v", actorID, err)
|
|
continue
|
|
}
|
|
if snapshot == nil {
|
|
t.Errorf("expected snapshot for %s, got nil", actorID)
|
|
continue
|
|
}
|
|
if snapshot.Version != int64(snapshotsPerGoroutine) {
|
|
t.Errorf("expected latest version %d for %s, got %d", snapshotsPerGoroutine, actorID, snapshot.Version)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcurrentSaveAndGetSnapshot(t *testing.T) {
|
|
store := NewInMemoryEventStore()
|
|
|
|
// Pre-populate with initial snapshot
|
|
initialSnapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-123",
|
|
Version: 1,
|
|
State: map[string]interface{}{
|
|
"initial": true,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(initialSnapshot); err != nil {
|
|
t.Fatalf("SaveSnapshot failed: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
numReaders := 50
|
|
numWriters := 10
|
|
readsPerReader := 100
|
|
writesPerWriter := 10
|
|
|
|
// Start readers
|
|
wg.Add(numReaders)
|
|
for r := 0; r < numReaders; r++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < readsPerReader; i++ {
|
|
snapshot, err := store.GetLatestSnapshot("actor-123")
|
|
if err != nil {
|
|
t.Errorf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
if snapshot == nil {
|
|
t.Error("expected snapshot, got nil")
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Start writers
|
|
wg.Add(numWriters)
|
|
for w := 0; w < numWriters; w++ {
|
|
go func(writerID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < writesPerWriter; i++ {
|
|
snapshot := &aether.ActorSnapshot{
|
|
ActorID: "actor-123",
|
|
Version: int64(100 + writerID*writesPerWriter + i),
|
|
State: map[string]interface{}{
|
|
"writer": writerID,
|
|
"index": i,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
if err := store.SaveSnapshot(snapshot); err != nil {
|
|
t.Errorf("SaveSnapshot failed: %v", err)
|
|
}
|
|
}
|
|
}(w)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify final state - should have the highest version
|
|
snapshot, err := store.GetLatestSnapshot("actor-123")
|
|
if err != nil {
|
|
t.Fatalf("GetLatestSnapshot failed: %v", err)
|
|
}
|
|
if snapshot == nil {
|
|
t.Fatal("expected snapshot, got nil")
|
|
}
|
|
// The highest version should be around 100 + (numWriters-1)*writesPerWriter + (writesPerWriter-1)
|
|
// which is 100 + 9*10 + 9 = 199
|
|
expectedMaxVersion := int64(100 + (numWriters-1)*writesPerWriter + (writesPerWriter - 1))
|
|
if snapshot.Version != expectedMaxVersion {
|
|
t.Errorf("expected latest version %d, got %d", expectedMaxVersion, snapshot.Version)
|
|
}
|
|
}
|