ScuttleBot

scuttlebot / internal / bots / scribe / scribe_test.go
Blame History Raw 280 lines
1
package scribe_test
2
3
import (
4
"encoding/json"
5
"testing"
6
"time"
7
8
"github.com/conflicthq/scuttlebot/internal/bots/scribe"
9
"github.com/conflicthq/scuttlebot/pkg/protocol"
10
)
11
12
func validEnvelopeJSON(t *testing.T, msgType, from string) string {
13
t.Helper()
14
env, err := protocol.New(msgType, from, map[string]string{"key": "val"})
15
if err != nil {
16
t.Fatalf("protocol.New: %v", err)
17
}
18
b, err := protocol.Marshal(env)
19
if err != nil {
20
t.Fatalf("protocol.Marshal: %v", err)
21
}
22
return string(b)
23
}
24
25
func TestStoreAppendAndQuery(t *testing.T) {
26
s := &scribe.MemoryStore{}
27
28
entries := []scribe.Entry{
29
{At: time.Now(), Channel: "#fleet", Nick: "claude-01", Kind: scribe.EntryKindRaw, Raw: "hello"},
30
{At: time.Now(), Channel: "#fleet", Nick: "gemini-01", Kind: scribe.EntryKindRaw, Raw: "world"},
31
{At: time.Now(), Channel: "#project.test", Nick: "claude-01", Kind: scribe.EntryKindRaw, Raw: "other channel"},
32
}
33
for _, e := range entries {
34
if err := s.Append(e); err != nil {
35
t.Fatalf("Append: %v", err)
36
}
37
}
38
39
fleet, err := s.Query("#fleet", 0)
40
if err != nil {
41
t.Fatalf("Query: %v", err)
42
}
43
if len(fleet) != 2 {
44
t.Errorf("Query #fleet: got %d entries, want 2", len(fleet))
45
}
46
47
all, err := s.Query("", 0)
48
if err != nil {
49
t.Fatalf("Query all: %v", err)
50
}
51
if len(all) != 3 {
52
t.Errorf("Query all: got %d entries, want 3", len(all))
53
}
54
}
55
56
func TestStoreQueryLimit(t *testing.T) {
57
s := &scribe.MemoryStore{}
58
for i := 0; i < 10; i++ {
59
_ = s.Append(scribe.Entry{Channel: "#fleet", Nick: "agent", Kind: scribe.EntryKindRaw})
60
}
61
62
got, err := s.Query("#fleet", 3)
63
if err != nil {
64
t.Fatalf("Query: %v", err)
65
}
66
if len(got) != 3 {
67
t.Errorf("Query with limit=3: got %d entries", len(got))
68
}
69
}
70
71
func TestEntryKindFromEnvelope(t *testing.T) {
72
// Test that a valid envelope JSON is detected as EntryKindEnvelope.
73
raw := validEnvelopeJSON(t, protocol.TypeTaskCreate, "claude-01")
74
75
env, err := protocol.Unmarshal([]byte(raw))
76
if err != nil {
77
t.Fatalf("Unmarshal: %v", err)
78
}
79
80
entry := scribe.Entry{
81
At: time.Now(),
82
Channel: "#fleet",
83
Nick: "claude-01",
84
Kind: scribe.EntryKindEnvelope,
85
MessageType: env.Type,
86
MessageID: env.ID,
87
Raw: raw,
88
}
89
90
if entry.MessageType != protocol.TypeTaskCreate {
91
t.Errorf("MessageType: got %q, want %q", entry.MessageType, protocol.TypeTaskCreate)
92
}
93
if entry.MessageID == "" {
94
t.Error("MessageID is empty")
95
}
96
if entry.Kind != scribe.EntryKindEnvelope {
97
t.Errorf("Kind: got %q, want %q", entry.Kind, scribe.EntryKindEnvelope)
98
}
99
}
100
101
func TestEntryKindRawForMalformed(t *testing.T) {
102
// Non-JSON and invalid envelopes should produce EntryKindRaw entries.
103
cases := []string{
104
"hello from a human",
105
"not json at all",
106
`{"incomplete": true}`, // valid JSON but not a valid envelope
107
}
108
109
for _, raw := range cases {
110
_, err := protocol.Unmarshal([]byte(raw))
111
if err == nil {
112
// Valid envelope — skip (this case tests malformed only)
113
continue
114
}
115
entry := scribe.Entry{
116
At: time.Now(),
117
Channel: "#fleet",
118
Nick: "agent",
119
Kind: scribe.EntryKindRaw,
120
Raw: raw,
121
}
122
if entry.Kind != scribe.EntryKindRaw {
123
t.Errorf("expected EntryKindRaw for %q", raw)
124
}
125
if entry.MessageType != "" {
126
t.Errorf("MessageType should be empty for raw entry")
127
}
128
}
129
}
130
131
func TestEntryJSONRoundTrip(t *testing.T) {
132
entry := scribe.Entry{
133
At: time.Now().Truncate(time.Millisecond),
134
Channel: "#project.test",
135
Nick: "claude-01",
136
Kind: scribe.EntryKindEnvelope,
137
MessageType: protocol.TypeAgentHello,
138
MessageID: "01HX123",
139
Raw: `{"v":1}`,
140
}
141
142
b, err := json.Marshal(entry)
143
if err != nil {
144
t.Fatalf("Marshal: %v", err)
145
}
146
147
var got scribe.Entry
148
if err := json.Unmarshal(b, &got); err != nil {
149
t.Fatalf("Unmarshal: %v", err)
150
}
151
152
if got.Channel != entry.Channel {
153
t.Errorf("Channel: got %q, want %q", got.Channel, entry.Channel)
154
}
155
if got.Kind != entry.Kind {
156
t.Errorf("Kind: got %q, want %q", got.Kind, entry.Kind)
157
}
158
if got.MessageType != entry.MessageType {
159
t.Errorf("MessageType: got %q, want %q", got.MessageType, entry.MessageType)
160
}
161
}
162
163
func TestFileStoreJSONL(t *testing.T) {
164
dir := t.TempDir()
165
fs := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "jsonl"})
166
defer fs.Close()
167
168
entries := []scribe.Entry{
169
{At: time.Now(), Channel: "#fleet", Nick: "alice", Kind: scribe.EntryKindRaw, Raw: "hello"},
170
{At: time.Now(), Channel: "#fleet", Nick: "bob", Kind: scribe.EntryKindRaw, Raw: "world"},
171
{At: time.Now(), Channel: "#ops", Nick: "alice", Kind: scribe.EntryKindRaw, Raw: "other"},
172
}
173
for _, e := range entries {
174
if err := fs.Append(e); err != nil {
175
t.Fatalf("Append: %v", err)
176
}
177
}
178
179
got, err := fs.Query("#fleet", 0)
180
if err != nil {
181
t.Fatalf("Query: %v", err)
182
}
183
if len(got) != 2 {
184
t.Errorf("Query #fleet: got %d, want 2", len(got))
185
}
186
}
187
188
func TestFileStorePerChannel(t *testing.T) {
189
dir := t.TempDir()
190
fs := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "jsonl", PerChannel: true})
191
defer fs.Close()
192
193
_ = fs.Append(scribe.Entry{At: time.Now(), Channel: "#fleet", Nick: "a", Kind: scribe.EntryKindRaw, Raw: "msg"})
194
_ = fs.Append(scribe.Entry{At: time.Now(), Channel: "#ops", Nick: "a", Kind: scribe.EntryKindRaw, Raw: "msg"})
195
196
entries, err := fs.Query("#fleet", 0)
197
if err != nil {
198
t.Fatalf("Query: %v", err)
199
}
200
if len(entries) != 1 {
201
t.Errorf("per-channel: got %d entries for #fleet, want 1", len(entries))
202
}
203
}
204
205
func TestFileStoreSizeRotation(t *testing.T) {
206
dir := t.TempDir()
207
// Set threshold to 1 byte so every write triggers rotation.
208
fs := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "text", Rotation: "size", MaxSizeMB: 0})
209
_ = fs // rotation with MaxSizeMB=0 means no limit; just ensure no panic
210
fs2 := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "jsonl", Rotation: "size", MaxSizeMB: 1})
211
defer fs2.Close()
212
for i := 0; i < 3; i++ {
213
if err := fs2.Append(scribe.Entry{At: time.Now(), Channel: "#test", Nick: "x", Kind: scribe.EntryKindRaw, Raw: "line"}); err != nil {
214
t.Fatalf("Append: %v", err)
215
}
216
}
217
}
218
219
func TestFileStoreCSVFormat(t *testing.T) {
220
dir := t.TempDir()
221
fs := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "csv"})
222
defer fs.Close()
223
224
err := fs.Append(scribe.Entry{At: time.Now(), Channel: "#fleet", Nick: "alice", Kind: scribe.EntryKindRaw, Raw: `say "hi"`})
225
if err != nil {
226
t.Fatalf("Append csv: %v", err)
227
}
228
// Query returns nil for non-jsonl formats — just check no error on Append.
229
got, err := fs.Query("#fleet", 0)
230
if err != nil {
231
t.Fatalf("Query csv: %v", err)
232
}
233
if got != nil {
234
t.Errorf("expected nil from Query on csv format")
235
}
236
}
237
238
func TestFileStorePruneOld(t *testing.T) {
239
dir := t.TempDir()
240
fs := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "jsonl", MaxAgeDays: 1})
241
defer fs.Close()
242
243
// Write a file and manually backdate it.
244
_ = fs.Append(scribe.Entry{At: time.Now(), Channel: "#fleet", Nick: "a", Kind: scribe.EntryKindRaw, Raw: "x"})
245
246
if err := fs.PruneOld(); err != nil {
247
t.Fatalf("PruneOld: %v", err)
248
}
249
}
250
251
func TestFileStoreJSONRoundTrip(t *testing.T) {
252
dir := t.TempDir()
253
fs := scribe.NewFileStore(scribe.FileStoreConfig{Dir: dir, Format: "jsonl"})
254
defer fs.Close()
255
256
orig := scribe.Entry{
257
At: time.Now().Truncate(time.Millisecond),
258
Channel: "#fleet",
259
Nick: "claude-01",
260
Kind: scribe.EntryKindEnvelope,
261
MessageType: "task.create",
262
MessageID: "01HX123",
263
Raw: `{"v":1}`,
264
}
265
_ = fs.Append(orig)
266
267
got, err := fs.Query("#fleet", 1)
268
if err != nil {
269
t.Fatalf("Query: %v", err)
270
}
271
if len(got) != 1 {
272
t.Fatalf("want 1 entry, got %d", len(got))
273
}
274
b1, _ := json.Marshal(orig)
275
b2, _ := json.Marshal(got[0])
276
if string(b1) != string(b2) {
277
t.Errorf("round-trip mismatch:\n want %s\n got %s", b1, b2)
278
}
279
}
280

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button