ScuttleBot

scuttlebot / pkg / protocol / protocol_test.go
Blame History Raw 240 lines
1
package protocol_test
2
3
import (
4
"encoding/json"
5
"testing"
6
7
"github.com/conflicthq/scuttlebot/pkg/protocol"
8
)
9
10
func TestRoundTrip(t *testing.T) {
11
type testPayload struct {
12
Task string `json:"task"`
13
}
14
15
env, err := protocol.New(protocol.TypeTaskCreate, "claude-01", testPayload{Task: "write tests"})
16
if err != nil {
17
t.Fatalf("New: %v", err)
18
}
19
20
data, err := protocol.Marshal(env)
21
if err != nil {
22
t.Fatalf("Marshal: %v", err)
23
}
24
25
got, err := protocol.Unmarshal(data)
26
if err != nil {
27
t.Fatalf("Unmarshal: %v", err)
28
}
29
30
if got.V != protocol.Version {
31
t.Errorf("V: got %d, want %d", got.V, protocol.Version)
32
}
33
if got.Type != protocol.TypeTaskCreate {
34
t.Errorf("Type: got %q, want %q", got.Type, protocol.TypeTaskCreate)
35
}
36
if got.ID == "" {
37
t.Error("ID is empty")
38
}
39
if got.From != "claude-01" {
40
t.Errorf("From: got %q, want %q", got.From, "claude-01")
41
}
42
if got.TS == 0 {
43
t.Error("TS is zero")
44
}
45
46
var p testPayload
47
if err := protocol.UnmarshalPayload(got, &p); err != nil {
48
t.Fatalf("UnmarshalPayload: %v", err)
49
}
50
if p.Task != "write tests" {
51
t.Errorf("payload.Task: got %q, want %q", p.Task, "write tests")
52
}
53
}
54
55
func TestUnmarshalInvalid(t *testing.T) {
56
cases := []struct {
57
name string
58
json string
59
}{
60
{"not json", `not json`},
61
{"wrong version", `{"v":99,"type":"task.create","id":"01HX","from":"agent","ts":1}`},
62
{"missing type", `{"v":1,"id":"01HX","from":"agent","ts":1}`},
63
{"missing id", `{"v":1,"type":"task.create","from":"agent","ts":1}`},
64
{"missing from", `{"v":1,"type":"task.create","id":"01HX","ts":1}`},
65
}
66
67
for _, tc := range cases {
68
t.Run(tc.name, func(t *testing.T) {
69
_, err := protocol.Unmarshal([]byte(tc.json))
70
if err == nil {
71
t.Error("expected error, got nil")
72
}
73
})
74
}
75
}
76
77
func TestNewGeneratesUniqueIDs(t *testing.T) {
78
seen := make(map[string]bool)
79
for i := 0; i < 100; i++ {
80
env, err := protocol.New(protocol.TypeAgentHello, "agent", nil)
81
if err != nil {
82
t.Fatalf("New: %v", err)
83
}
84
if seen[env.ID] {
85
t.Errorf("duplicate ID: %s", env.ID)
86
}
87
seen[env.ID] = true
88
}
89
}
90
91
func TestNilPayload(t *testing.T) {
92
env, err := protocol.New(protocol.TypeAgentBye, "agent-01", nil)
93
if err != nil {
94
t.Fatalf("New: %v", err)
95
}
96
97
data, err := protocol.Marshal(env)
98
if err != nil {
99
t.Fatalf("Marshal: %v", err)
100
}
101
102
got, err := protocol.Unmarshal(data)
103
if err != nil {
104
t.Fatalf("Unmarshal: %v", err)
105
}
106
107
if len(got.Payload) != 0 {
108
t.Errorf("expected empty payload, got %s", got.Payload)
109
}
110
}
111
112
func TestMatchesRecipient(t *testing.T) {
113
cases := []struct {
114
name string
115
to []string
116
nick string
117
agentType string
118
want bool
119
}{
120
// backwards compat
121
{"empty to matches all", nil, "claude-1", "worker", true},
122
123
// @all
124
{"@all matches worker", []string{"@all"}, "claude-1", "worker", true},
125
{"@all matches operator", []string{"@all"}, "glengoolie", "operator", true},
126
127
// role tokens
128
{"@workers matches worker", []string{"@workers"}, "claude-1", "worker", true},
129
{"@workers no match orchestrator", []string{"@workers"}, "claude-1", "orchestrator", false},
130
{"@operators matches operator", []string{"@operators"}, "glengoolie", "operator", true},
131
{"@orchestrators matches orchestrator", []string{"@orchestrators"}, "claude-1", "orchestrator", true},
132
{"@observers matches observer", []string{"@observers"}, "sentinel", "observer", true},
133
134
// prefix glob
135
{"@claude-* matches claude-1", []string{"@claude-*"}, "claude-1", "worker", true},
136
{"@claude-* matches claude-sonnet", []string{"@claude-*"}, "claude-sonnet", "worker", true},
137
{"@claude-* no match codex-1", []string{"@claude-*"}, "codex-1", "worker", false},
138
{"@gemini-* matches gemini-pro", []string{"@gemini-*"}, "gemini-pro", "worker", true},
139
140
// exact nick
141
{"exact nick match", []string{"codex-7"}, "codex-7", "worker", true},
142
{"exact nick no match", []string{"codex-7"}, "codex-8", "worker", false},
143
144
// OR semantics
145
{"OR: second token matches", []string{"@operators", "codex-7"}, "codex-7", "worker", true},
146
{"OR: first token matches", []string{"@workers", "codex-7"}, "claude-1", "worker", true},
147
{"OR: none match", []string{"@operators", "codex-7"}, "claude-1", "worker", false},
148
}
149
150
for _, tc := range cases {
151
t.Run(tc.name, func(t *testing.T) {
152
env := &protocol.Envelope{
153
V: protocol.Version,
154
Type: protocol.TypeTaskCreate,
155
ID: "test",
156
From: "orchestrator",
157
To: tc.to,
158
TS: 1,
159
}
160
got := protocol.MatchesRecipient(env, tc.nick, tc.agentType)
161
if got != tc.want {
162
t.Errorf("MatchesRecipient(%v, %q, %q) = %v, want %v", tc.to, tc.nick, tc.agentType, got, tc.want)
163
}
164
})
165
}
166
}
167
168
func TestNewTo(t *testing.T) {
169
env, err := protocol.NewTo(protocol.TypeTaskCreate, "orchestrator-1", []string{"@workers", "@claude-*"}, nil)
170
if err != nil {
171
t.Fatalf("NewTo: %v", err)
172
}
173
if len(env.To) != 2 {
174
t.Fatalf("To length: got %d, want 2", len(env.To))
175
}
176
if env.To[0] != "@workers" || env.To[1] != "@claude-*" {
177
t.Errorf("To: got %v", env.To)
178
}
179
180
// round-trip
181
data, err := protocol.Marshal(env)
182
if err != nil {
183
t.Fatalf("Marshal: %v", err)
184
}
185
got, err := protocol.Unmarshal(data)
186
if err != nil {
187
t.Fatalf("Unmarshal: %v", err)
188
}
189
if len(got.To) != 2 || got.To[0] != "@workers" {
190
t.Errorf("round-trip To: got %v", got.To)
191
}
192
}
193
194
func TestToOmittedWhenEmpty(t *testing.T) {
195
env, err := protocol.New(protocol.TypeAgentHello, "agent", nil)
196
if err != nil {
197
t.Fatalf("New: %v", err)
198
}
199
data, err := protocol.Marshal(env)
200
if err != nil {
201
t.Fatalf("Marshal: %v", err)
202
}
203
var raw map[string]any
204
if err := json.Unmarshal(data, &raw); err != nil {
205
t.Fatalf("json.Unmarshal: %v", err)
206
}
207
if _, ok := raw["to"]; ok {
208
t.Error("expected 'to' key to be omitted when empty")
209
}
210
}
211
212
func TestAllMessageTypes(t *testing.T) {
213
types := []string{
214
protocol.TypeTaskCreate,
215
protocol.TypeTaskUpdate,
216
protocol.TypeTaskComplete,
217
protocol.TypeAgentHello,
218
protocol.TypeAgentBye,
219
}
220
for _, msgType := range types {
221
t.Run(msgType, func(t *testing.T) {
222
env, err := protocol.New(msgType, "agent", json.RawMessage(`{"key":"val"}`))
223
if err != nil {
224
t.Fatalf("New: %v", err)
225
}
226
data, err := protocol.Marshal(env)
227
if err != nil {
228
t.Fatalf("Marshal: %v", err)
229
}
230
got, err := protocol.Unmarshal(data)
231
if err != nil {
232
t.Fatalf("Unmarshal: %v", err)
233
}
234
if got.Type != msgType {
235
t.Errorf("Type: got %q, want %q", got.Type, msgType)
236
}
237
})
238
}
239
}
240

Keyboard Shortcuts

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