1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package event_test
6
7import (
8 "context"
9 "errors"
10 "testing"
11 "time"
12
13 "git.secluded.site/np/internal/event"
14 "git.secluded.site/np/internal/goal"
15 "git.secluded.site/np/internal/task"
16 "git.secluded.site/np/internal/testutil"
17)
18
19func TestStoreAppendAndList(t *testing.T) {
20 ctx := context.Background()
21 db := testutil.OpenDB(t)
22
23 clock := &testutil.SequenceClock{
24 Times: []time.Time{
25 time.Date(2025, time.March, 3, 10, 15, 0, 0, time.FixedZone("A", 3600)),
26 time.Date(2025, time.March, 3, 10, 20, 0, 0, time.FixedZone("B", -3600)),
27 },
28 }
29
30 store := event.NewStore(db, clock)
31 sid := "sid-events"
32
33 goalDoc := goal.Document{
34 Title: "Plan",
35 Description: "do something",
36 UpdatedAt: time.Date(2025, time.March, 3, 9, 0, 0, 0, time.FixedZone("C", 7200)),
37 }
38
39 first, err := store.Append(ctx, sid, event.BuildGoalSet("np g s", " reason ", goalDoc))
40 if err != nil {
41 t.Fatalf("Append goal_set: %v", err)
42 }
43 if first.Seq != 1 {
44 t.Fatalf("Expected seq 1, got %d", first.Seq)
45 }
46 if !first.At.Equal(clock.Times[0].UTC()) {
47 t.Fatalf("At mismatch: want %v got %v", clock.Times[0].UTC(), first.At)
48 }
49 if !first.HasReason() {
50 t.Fatalf("Expected reason")
51 }
52 if want := "reason"; *first.Reason != want {
53 t.Fatalf("Reason mismatch: want %q got %q", want, *first.Reason)
54 }
55 var goalPayload event.GoalSetPayload
56 if err := first.UnmarshalPayload(&goalPayload); err != nil {
57 t.Fatalf("UnmarshalPayload goal_set: %v", err)
58 }
59 if want := event.GoalSnapshotFrom(goalDoc); goalPayload.Goal != want {
60 t.Fatalf("Goal payload mismatch: want %+v got %+v", want, goalPayload.Goal)
61 }
62
63 taskAdded := task.Task{
64 ID: "abc123",
65 Title: "Task",
66 Description: "finish work",
67 Status: task.StatusPending,
68 CreatedAt: time.Date(2025, time.March, 3, 9, 30, 0, 0, time.UTC),
69 UpdatedAt: time.Date(2025, time.March, 3, 9, 30, 0, 0, time.UTC),
70 CreatedSeq: 1,
71 }
72
73 second, err := store.Append(ctx, sid, event.BuildTaskAdded("np t a", "", taskAdded))
74 if err != nil {
75 t.Fatalf("Append task_added: %v", err)
76 }
77 if second.Seq != 2 {
78 t.Fatalf("Expected seq 2, got %d", second.Seq)
79 }
80 if second.Reason != nil {
81 t.Fatalf("Expected nil reason")
82 }
83 if !second.At.Equal(clock.Times[1].UTC()) {
84 t.Fatalf("Second At mismatch: want %v got %v", clock.Times[1].UTC(), second.At)
85 }
86
87 var taskPayload event.TaskAddedPayload
88 if err := second.UnmarshalPayload(&taskPayload); err != nil {
89 t.Fatalf("UnmarshalPayload task_added: %v", err)
90 }
91 if taskPayload.Task != taskAdded {
92 t.Fatalf("Task payload mismatch: want %+v got %+v", taskAdded, taskPayload.Task)
93 }
94
95 all, err := store.List(ctx, sid, event.ListOptions{})
96 if err != nil {
97 t.Fatalf("List: %v", err)
98 }
99 if len(all) != 2 {
100 t.Fatalf("Expected 2 events, got %d", len(all))
101 }
102 if all[0].Seq != 1 || all[1].Seq != 2 {
103 t.Fatalf("Events not ordered: %+v", all)
104 }
105
106 after, err := store.List(ctx, sid, event.ListOptions{After: 1})
107 if err != nil {
108 t.Fatalf("List after=1: %v", err)
109 }
110 if len(after) != 1 || after[0].Seq != 2 {
111 t.Fatalf("After list mismatch: %+v", after)
112 }
113
114 limited, err := store.List(ctx, sid, event.ListOptions{Limit: 1})
115 if err != nil {
116 t.Fatalf("List limit=1: %v", err)
117 }
118 if len(limited) != 1 || limited[0].Seq != 1 {
119 t.Fatalf("Limited list mismatch: %+v", limited)
120 }
121
122 seq, err := store.LatestSequence(ctx, sid)
123 if err != nil {
124 t.Fatalf("LatestSequence: %v", err)
125 }
126 if seq != 2 {
127 t.Fatalf("LatestSequence mismatch: want 2 got %d", seq)
128 }
129
130 emptySeq, err := store.LatestSequence(ctx, "missing")
131 if err != nil {
132 t.Fatalf("LatestSequence missing: %v", err)
133 }
134 if emptySeq != 0 {
135 t.Fatalf("LatestSequence should be 0 for missing, got %d", emptySeq)
136 }
137}
138
139func TestAppendValidation(t *testing.T) {
140 ctx := context.Background()
141 db := testutil.OpenDB(t)
142 store := event.NewStore(db, &testutil.SequenceClock{Times: []time.Time{time.Now()}})
143
144 if _, err := store.Append(ctx, "sid", event.AppendInput{Type: "unknown", Command: "cmd"}); !errors.Is(err, event.ErrInvalidType) {
145 t.Fatalf("expected ErrInvalidType, got %v", err)
146 }
147
148 if _, err := store.Append(ctx, "sid", event.AppendInput{Type: event.TypeGoalSet, Command: " "}); !errors.Is(err, event.ErrEmptyCommand) {
149 t.Fatalf("expected ErrEmptyCommand, got %v", err)
150 }
151}