diff options
Diffstat (limited to 'internal/storage/events_test.go')
| -rw-r--r-- | internal/storage/events_test.go | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/internal/storage/events_test.go b/internal/storage/events_test.go new file mode 100644 index 0000000..3b342c7 --- /dev/null +++ b/internal/storage/events_test.go @@ -0,0 +1,182 @@ +package storage + +import ( + "encoding/json" + "testing" + "time" + + "github.com/thepeterstone/claudomator/internal/event" + "github.com/thepeterstone/claudomator/internal/task" +) + +// makeTaskForEvent creates a minimal task row so events can satisfy the FK. +func makeTaskForEvent(t *testing.T, db *DB, id string) { + t.Helper() + now := time.Now().UTC().Truncate(time.Second) + tk := &task.Task{ + ID: id, + Name: id, + Agent: task.AgentConfig{Type: "claude", Instructions: "noop"}, + Priority: task.PriorityNormal, + State: task.StatePending, + CreatedAt: now, + UpdatedAt: now, + } + if err := db.CreateTask(tk); err != nil { + t.Fatalf("creating task: %v", err) + } +} + +func TestCreateEvent_AssignsSeqAndDefaults(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + e := &event.Event{ + TaskID: "task-1", + Kind: event.KindStateChange, + Actor: event.ActorSystem, + } + if err := db.CreateEvent(e); err != nil { + t.Fatalf("CreateEvent: %v", err) + } + if e.ID == "" { + t.Error("expected ID populated") + } + if e.Seq != 1 { + t.Errorf("expected seq=1, got %d", e.Seq) + } + if e.Timestamp.IsZero() { + t.Error("expected Timestamp populated") + } + if string(e.Payload) != "{}" { + t.Errorf("expected default payload {}, got %s", e.Payload) + } +} + +func TestCreateEvent_SeqIsMonotonicPerTask(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-A") + makeTaskForEvent(t, db, "task-B") + + // 3 events on task-A, 2 on task-B, interleaved. + for _, taskID := range []string{"task-A", "task-A", "task-B", "task-A", "task-B"} { + e := &event.Event{ + TaskID: taskID, + Kind: event.KindAgentMessage, + Actor: event.ActorAgent, + } + if err := db.CreateEvent(e); err != nil { + t.Fatalf("CreateEvent: %v", err) + } + } + + a, err := db.ListEvents("task-A", 0) + if err != nil { + t.Fatalf("ListEvents A: %v", err) + } + if len(a) != 3 { + t.Fatalf("expected 3 events on task-A, got %d", len(a)) + } + for i, e := range a { + want := int64(i + 1) + if e.Seq != want { + t.Errorf("task-A event %d: seq=%d want %d", i, e.Seq, want) + } + } + + b, err := db.ListEvents("task-B", 0) + if err != nil { + t.Fatalf("ListEvents B: %v", err) + } + if len(b) != 2 { + t.Fatalf("expected 2 events on task-B, got %d", len(b)) + } + for i, e := range b { + want := int64(i + 1) + if e.Seq != want { + t.Errorf("task-B event %d: seq=%d want %d", i, e.Seq, want) + } + } +} + +func TestListEvents_SinceSeq(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + for i := 0; i < 5; i++ { + e := &event.Event{ + TaskID: "task-1", + Kind: event.KindAgentMessage, + Actor: event.ActorAgent, + } + if err := db.CreateEvent(e); err != nil { + t.Fatalf("CreateEvent: %v", err) + } + } + + got, err := db.ListEvents("task-1", 3) + if err != nil { + t.Fatalf("ListEvents: %v", err) + } + if len(got) != 2 { + t.Fatalf("expected 2 events with seq>3, got %d", len(got)) + } + if got[0].Seq != 4 || got[1].Seq != 5 { + t.Errorf("expected seqs 4,5; got %d,%d", got[0].Seq, got[1].Seq) + } +} + +func TestListEvents_PreservesPayloadAndKind(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + payload := json.RawMessage(`{"question":"which env?","options":["dev","prod"]}`) + written := &event.Event{ + TaskID: "task-1", + Kind: event.KindClarificationRequest, + Actor: event.ActorAgent, + Payload: payload, + } + if err := db.CreateEvent(written); err != nil { + t.Fatalf("CreateEvent: %v", err) + } + + got, err := db.ListEvents("task-1", 0) + if err != nil { + t.Fatalf("ListEvents: %v", err) + } + if len(got) != 1 { + t.Fatalf("expected 1 event, got %d", len(got)) + } + e := got[0] + if e.Kind != event.KindClarificationRequest { + t.Errorf("kind: got %v want %v", e.Kind, event.KindClarificationRequest) + } + if e.Actor != event.ActorAgent { + t.Errorf("actor: got %v want %v", e.Actor, event.ActorAgent) + } + if string(e.Payload) != string(payload) { + t.Errorf("payload: got %s want %s", e.Payload, payload) + } +} + +func TestCreateEvent_RequiresFields(t *testing.T) { + db := testDB(t) + + cases := []struct { + name string + e *event.Event + }{ + {"nil", nil}, + {"empty taskID", &event.Event{Kind: event.KindAgentMessage, Actor: event.ActorAgent}}, + {"empty kind", &event.Event{TaskID: "t", Actor: event.ActorAgent}}, + {"empty actor", &event.Event{TaskID: "t", Kind: event.KindAgentMessage}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if err := db.CreateEvent(tc.e); err == nil { + t.Errorf("expected error, got nil") + } + }) + } +} |
