ScuttleBot

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

Keyboard Shortcuts

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