ScuttleBot

scuttlebot / internal / config / config_test.go
Source Blame History 288 lines
5705614… lmata 1 package config
5705614… lmata 2
5705614… lmata 3 import (
763c873… lmata 4 "encoding/json"
5705614… lmata 5 "os"
5705614… lmata 6 "path/filepath"
5705614… lmata 7 "testing"
5705614… lmata 8 "time"
5705614… lmata 9 )
5705614… lmata 10
5705614… lmata 11 func TestTopologyConfigParse(t *testing.T) {
5705614… lmata 12 yaml := `
5705614… lmata 13 topology:
5705614… lmata 14 channels:
5705614… lmata 15 - name: "#general"
5705614… lmata 16 topic: "Fleet coordination"
5705614… lmata 17 ops: [bridge, oracle]
5705614… lmata 18 autojoin: [bridge, oracle, scribe]
5705614… lmata 19 - name: "#alerts"
5705614… lmata 20 autojoin: [bridge, sentinel]
5705614… lmata 21 types:
5705614… lmata 22 - name: task
5705614… lmata 23 prefix: "task."
5705614… lmata 24 autojoin: [bridge, scribe]
5705614… lmata 25 supervision: "#general"
5705614… lmata 26 ephemeral: true
5705614… lmata 27 ttl: 72h
5705614… lmata 28 - name: sprint
5705614… lmata 29 prefix: "sprint."
5705614… lmata 30 autojoin: [bridge, oracle, herald]
5705614… lmata 31 - name: incident
5705614… lmata 32 prefix: "incident."
5705614… lmata 33 autojoin: [bridge, sentinel, steward]
5705614… lmata 34 supervision: "#alerts"
5705614… lmata 35 ephemeral: true
5705614… lmata 36 ttl: 168h
5705614… lmata 37 `
5705614… lmata 38 f := filepath.Join(t.TempDir(), "scuttlebot.yaml")
5705614… lmata 39 if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
5705614… lmata 40 t.Fatal(err)
5705614… lmata 41 }
5705614… lmata 42
5705614… lmata 43 var cfg Config
5705614… lmata 44 cfg.Defaults()
5705614… lmata 45 if err := cfg.LoadFile(f); err != nil {
5705614… lmata 46 t.Fatalf("LoadFile: %v", err)
5705614… lmata 47 }
5705614… lmata 48
5705614… lmata 49 top := cfg.Topology
5705614… lmata 50
5705614… lmata 51 // static channels
5705614… lmata 52 if len(top.Channels) != 2 {
5705614… lmata 53 t.Fatalf("want 2 static channels, got %d", len(top.Channels))
5705614… lmata 54 }
5705614… lmata 55 general := top.Channels[0]
5705614… lmata 56 if general.Name != "#general" {
5705614… lmata 57 t.Errorf("channel[0].Name = %q, want #general", general.Name)
5705614… lmata 58 }
5705614… lmata 59 if general.Topic != "Fleet coordination" {
5705614… lmata 60 t.Errorf("channel[0].Topic = %q", general.Topic)
5705614… lmata 61 }
5705614… lmata 62 if len(general.Autojoin) != 3 {
5705614… lmata 63 t.Errorf("channel[0].Autojoin len = %d, want 3", len(general.Autojoin))
5705614… lmata 64 }
5705614… lmata 65
5705614… lmata 66 // types
5705614… lmata 67 if len(top.Types) != 3 {
5705614… lmata 68 t.Fatalf("want 3 types, got %d", len(top.Types))
5705614… lmata 69 }
5705614… lmata 70 task := top.Types[0]
5705614… lmata 71 if task.Name != "task" {
5705614… lmata 72 t.Errorf("types[0].Name = %q, want task", task.Name)
5705614… lmata 73 }
5705614… lmata 74 if task.Prefix != "task." {
5705614… lmata 75 t.Errorf("types[0].Prefix = %q, want task.", task.Prefix)
5705614… lmata 76 }
5705614… lmata 77 if task.Supervision != "#general" {
5705614… lmata 78 t.Errorf("types[0].Supervision = %q, want #general", task.Supervision)
5705614… lmata 79 }
5705614… lmata 80 if !task.Ephemeral {
5705614… lmata 81 t.Error("types[0].Ephemeral should be true")
5705614… lmata 82 }
5705614… lmata 83 if task.TTL.Duration != 72*time.Hour {
5705614… lmata 84 t.Errorf("types[0].TTL = %v, want 72h", task.TTL.Duration)
5705614… lmata 85 }
5705614… lmata 86
5705614… lmata 87 incident := top.Types[2]
5705614… lmata 88 if incident.TTL.Duration != 168*time.Hour {
5705614… lmata 89 t.Errorf("types[2].TTL = %v, want 168h", incident.TTL.Duration)
5705614… lmata 90 }
5705614… lmata 91 }
5705614… lmata 92
5705614… lmata 93 func TestTopologyConfigEmpty(t *testing.T) {
5705614… lmata 94 yaml := `bridge:
5705614… lmata 95 enabled: true
5705614… lmata 96 `
5705614… lmata 97 f := filepath.Join(t.TempDir(), "scuttlebot.yaml")
5705614… lmata 98 if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
5705614… lmata 99 t.Fatal(err)
5705614… lmata 100 }
5705614… lmata 101
5705614… lmata 102 var cfg Config
5705614… lmata 103 cfg.Defaults()
5705614… lmata 104 if err := cfg.LoadFile(f); err != nil {
5705614… lmata 105 t.Fatalf("LoadFile: %v", err)
5705614… lmata 106 }
5705614… lmata 107
5705614… lmata 108 // No topology section — should be zero value, not an error.
5705614… lmata 109 if len(cfg.Topology.Channels) != 0 {
5705614… lmata 110 t.Errorf("expected no static channels, got %d", len(cfg.Topology.Channels))
5705614… lmata 111 }
5705614… lmata 112 if len(cfg.Topology.Types) != 0 {
5705614… lmata 113 t.Errorf("expected no types, got %d", len(cfg.Topology.Types))
5705614… lmata 114 }
5705614… lmata 115 }
5705614… lmata 116
5705614… lmata 117 func TestDurationUnmarshal(t *testing.T) {
5705614… lmata 118 cases := []struct {
5705614… lmata 119 input string
5705614… lmata 120 want time.Duration
5705614… lmata 121 }{
5705614… lmata 122 {"72h", 72 * time.Hour},
5705614… lmata 123 {"30m", 30 * time.Minute},
5705614… lmata 124 {"168h", 168 * time.Hour},
5705614… lmata 125 {"0s", 0},
5705614… lmata 126 }
5705614… lmata 127 for _, tc := range cases {
5705614… lmata 128 yaml := `topology:
5705614… lmata 129 types:
5705614… lmata 130 - name: x
5705614… lmata 131 prefix: "x."
5705614… lmata 132 ttl: ` + tc.input + "\n"
5705614… lmata 133 f := filepath.Join(t.TempDir(), "cfg.yaml")
5705614… lmata 134 if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
5705614… lmata 135 t.Fatal(err)
5705614… lmata 136 }
5705614… lmata 137 var cfg Config
5705614… lmata 138 if err := cfg.LoadFile(f); err != nil {
5705614… lmata 139 t.Fatalf("input %q: %v", tc.input, err)
5705614… lmata 140 }
5705614… lmata 141 got := cfg.Topology.Types[0].TTL.Duration
5705614… lmata 142 if got != tc.want {
5705614… lmata 143 t.Errorf("input %q: got %v, want %v", tc.input, got, tc.want)
5705614… lmata 144 }
763c873… lmata 145 }
763c873… lmata 146 }
763c873… lmata 147
763c873… lmata 148 func TestDurationJSONRoundTrip(t *testing.T) {
763c873… lmata 149 cases := []struct {
763c873… lmata 150 dur time.Duration
763c873… lmata 151 want string
763c873… lmata 152 }{
763c873… lmata 153 {72 * time.Hour, `"72h0m0s"`},
763c873… lmata 154 {30 * time.Minute, `"30m0s"`},
763c873… lmata 155 {0, `"0s"`},
763c873… lmata 156 }
763c873… lmata 157 for _, tc := range cases {
763c873… lmata 158 d := Duration{tc.dur}
763c873… lmata 159 b, err := json.Marshal(d)
763c873… lmata 160 if err != nil {
763c873… lmata 161 t.Fatalf("Marshal(%v): %v", tc.dur, err)
763c873… lmata 162 }
763c873… lmata 163 if string(b) != tc.want {
763c873… lmata 164 t.Errorf("Marshal(%v) = %s, want %s", tc.dur, b, tc.want)
763c873… lmata 165 }
763c873… lmata 166 var back Duration
763c873… lmata 167 if err := json.Unmarshal(b, &back); err != nil {
763c873… lmata 168 t.Fatalf("Unmarshal(%s): %v", b, err)
763c873… lmata 169 }
763c873… lmata 170 if back.Duration != tc.dur {
763c873… lmata 171 t.Errorf("round-trip(%v): got %v", tc.dur, back.Duration)
763c873… lmata 172 }
763c873… lmata 173 }
763c873… lmata 174 }
763c873… lmata 175
763c873… lmata 176 func TestDurationJSONUnmarshalErrors(t *testing.T) {
763c873… lmata 177 cases := []struct{ input string }{
8332dd8… lmata 178 {`123`}, // not a quoted string
8332dd8… lmata 179 {`"notadur"`}, // not parseable
8332dd8… lmata 180 {`""`}, // empty string
763c873… lmata 181 }
763c873… lmata 182 for _, tc := range cases {
763c873… lmata 183 var d Duration
763c873… lmata 184 if err := json.Unmarshal([]byte(tc.input), &d); err == nil {
763c873… lmata 185 t.Errorf("Unmarshal(%s): expected error, got nil", tc.input)
763c873… lmata 186 }
763c873… lmata 187 }
763c873… lmata 188 }
763c873… lmata 189
763c873… lmata 190 func TestApplyEnv(t *testing.T) {
763c873… lmata 191 cases := []struct {
763c873… lmata 192 envKey string
763c873… lmata 193 check func(c Config) bool
763c873… lmata 194 }{
763c873… lmata 195 {"SCUTTLEBOT_API_ADDR", func(c Config) bool { return c.APIAddr == ":9999" }},
763c873… lmata 196 {"SCUTTLEBOT_MCP_ADDR", func(c Config) bool { return c.MCPAddr == ":9998" }},
763c873… lmata 197 {"SCUTTLEBOT_DB_DRIVER", func(c Config) bool { return c.Datastore.Driver == "postgres" }},
763c873… lmata 198 {"SCUTTLEBOT_DB_DSN", func(c Config) bool { return c.Datastore.DSN == "postgres://test" }},
763c873… lmata 199 {"SCUTTLEBOT_ERGO_EXTERNAL", func(c Config) bool { return c.Ergo.External }},
763c873… lmata 200 {"SCUTTLEBOT_ERGO_API_ADDR", func(c Config) bool { return c.Ergo.APIAddr == "http://ergo:8089" }},
763c873… lmata 201 {"SCUTTLEBOT_ERGO_API_TOKEN", func(c Config) bool { return c.Ergo.APIToken == "tok123" }},
763c873… lmata 202 {"SCUTTLEBOT_ERGO_IRC_ADDR", func(c Config) bool { return c.Ergo.IRCAddr == "ergo:6667" }},
763c873… lmata 203 {"SCUTTLEBOT_ERGO_NETWORK_NAME", func(c Config) bool { return c.Ergo.NetworkName == "testnet" }},
763c873… lmata 204 {"SCUTTLEBOT_ERGO_SERVER_NAME", func(c Config) bool { return c.Ergo.ServerName == "irc.test.local" }},
763c873… lmata 205 }
763c873… lmata 206
763c873… lmata 207 envValues := map[string]string{
763c873… lmata 208 "SCUTTLEBOT_API_ADDR": ":9999",
763c873… lmata 209 "SCUTTLEBOT_MCP_ADDR": ":9998",
763c873… lmata 210 "SCUTTLEBOT_DB_DRIVER": "postgres",
763c873… lmata 211 "SCUTTLEBOT_DB_DSN": "postgres://test",
763c873… lmata 212 "SCUTTLEBOT_ERGO_EXTERNAL": "true",
763c873… lmata 213 "SCUTTLEBOT_ERGO_API_ADDR": "http://ergo:8089",
763c873… lmata 214 "SCUTTLEBOT_ERGO_API_TOKEN": "tok123",
763c873… lmata 215 "SCUTTLEBOT_ERGO_IRC_ADDR": "ergo:6667",
763c873… lmata 216 "SCUTTLEBOT_ERGO_NETWORK_NAME": "testnet",
763c873… lmata 217 "SCUTTLEBOT_ERGO_SERVER_NAME": "irc.test.local",
763c873… lmata 218 }
763c873… lmata 219
763c873… lmata 220 for _, tc := range cases {
763c873… lmata 221 t.Run(tc.envKey, func(t *testing.T) {
763c873… lmata 222 t.Setenv(tc.envKey, envValues[tc.envKey])
763c873… lmata 223 var c Config
763c873… lmata 224 c.Defaults()
763c873… lmata 225 c.ApplyEnv()
763c873… lmata 226 if !tc.check(c) {
763c873… lmata 227 t.Errorf("%s=%q did not apply correctly", tc.envKey, envValues[tc.envKey])
763c873… lmata 228 }
763c873… lmata 229 })
763c873… lmata 230 }
763c873… lmata 231 }
763c873… lmata 232
763c873… lmata 233 func TestApplyEnvErgoExternalFalseByDefault(t *testing.T) {
763c873… lmata 234 // SCUTTLEBOT_ERGO_EXTERNAL absent — should not force External=true.
763c873… lmata 235 var c Config
763c873… lmata 236 c.Defaults()
763c873… lmata 237 c.ApplyEnv()
763c873… lmata 238 if c.Ergo.External {
763c873… lmata 239 t.Error("Ergo.External should be false when env var is absent")
763c873… lmata 240 }
763c873… lmata 241 }
763c873… lmata 242
763c873… lmata 243 func TestConfigSaveAndLoad(t *testing.T) {
763c873… lmata 244 dir := t.TempDir()
763c873… lmata 245 path := filepath.Join(dir, "scuttlebot.yaml")
763c873… lmata 246
763c873… lmata 247 var orig Config
763c873… lmata 248 orig.Defaults()
763c873… lmata 249 orig.Bridge.WebUserTTLMinutes = 42
763c873… lmata 250 orig.AgentPolicy.RequireCheckin = true
763c873… lmata 251 orig.AgentPolicy.CheckinChannel = "#fleet"
763c873… lmata 252 orig.Logging.Enabled = true
763c873… lmata 253 orig.Logging.Format = "jsonl"
763c873… lmata 254
763c873… lmata 255 if err := orig.Save(path); err != nil {
763c873… lmata 256 t.Fatalf("Save: %v", err)
763c873… lmata 257 }
763c873… lmata 258
763c873… lmata 259 var loaded Config
763c873… lmata 260 loaded.Defaults()
763c873… lmata 261 if err := loaded.LoadFile(path); err != nil {
763c873… lmata 262 t.Fatalf("LoadFile: %v", err)
763c873… lmata 263 }
763c873… lmata 264
763c873… lmata 265 if loaded.Bridge.WebUserTTLMinutes != 42 {
763c873… lmata 266 t.Errorf("WebUserTTLMinutes = %d, want 42", loaded.Bridge.WebUserTTLMinutes)
763c873… lmata 267 }
763c873… lmata 268 if !loaded.AgentPolicy.RequireCheckin {
763c873… lmata 269 t.Error("AgentPolicy.RequireCheckin should be true")
763c873… lmata 270 }
763c873… lmata 271 if loaded.AgentPolicy.CheckinChannel != "#fleet" {
763c873… lmata 272 t.Errorf("CheckinChannel = %q, want #fleet", loaded.AgentPolicy.CheckinChannel)
763c873… lmata 273 }
763c873… lmata 274 if !loaded.Logging.Enabled {
763c873… lmata 275 t.Error("Logging.Enabled should be true")
763c873… lmata 276 }
763c873… lmata 277 if loaded.Logging.Format != "jsonl" {
763c873… lmata 278 t.Errorf("Logging.Format = %q, want jsonl", loaded.Logging.Format)
763c873… lmata 279 }
763c873… lmata 280 }
763c873… lmata 281
763c873… lmata 282 func TestLoadFileMissingIsNotError(t *testing.T) {
763c873… lmata 283 var c Config
763c873… lmata 284 c.Defaults()
763c873… lmata 285 if err := c.LoadFile("/nonexistent/path/scuttlebot.yaml"); err != nil {
763c873… lmata 286 t.Errorf("LoadFile on missing file should return nil, got %v", err)
5705614… lmata 287 }
5705614… lmata 288 }

Keyboard Shortcuts

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