|
9 | 9 | "time" |
10 | 10 |
|
11 | 11 | "github.com/BackendStack21/odek/internal/config" |
| 12 | + "github.com/BackendStack21/odek/internal/danger" |
12 | 13 | "github.com/BackendStack21/odek/internal/schedule" |
13 | 14 | "github.com/BackendStack21/odek/internal/telegram" |
14 | 15 | ) |
@@ -218,3 +219,132 @@ func TestAppendScheduleLog_RedactsSecrets(t *testing.T) { |
218 | 219 | t.Errorf("log should contain [REDACTED] markers: %q", string(data)) |
219 | 220 | } |
220 | 221 | } |
| 222 | + |
| 223 | +func TestMergeScheduleDangerous(t *testing.T) { |
| 224 | + base := danger.DangerousConfig{ |
| 225 | + Classes: map[danger.RiskClass]danger.Action{ |
| 226 | + danger.SystemWrite: danger.Prompt, |
| 227 | + }, |
| 228 | + Allowlist: []string{"base-allow"}, |
| 229 | + Denylist: []string{"base-deny"}, |
| 230 | + } |
| 231 | + schedule := danger.DangerousConfig{ |
| 232 | + Classes: map[danger.RiskClass]danger.Action{ |
| 233 | + danger.NetworkEgress: danger.Allow, |
| 234 | + danger.SystemWrite: danger.Allow, // overrides base |
| 235 | + }, |
| 236 | + Allowlist: []string{"schedule-allow"}, |
| 237 | + Denylist: []string{"schedule-deny"}, |
| 238 | + } |
| 239 | + mergeScheduleDangerous(&base, schedule) |
| 240 | + |
| 241 | + if base.Classes[danger.NetworkEgress] != danger.Allow { |
| 242 | + t.Errorf("network_egress not added from schedule: %s", base.Classes[danger.NetworkEgress]) |
| 243 | + } |
| 244 | + if base.Classes[danger.SystemWrite] != danger.Allow { |
| 245 | + t.Errorf("system_write not overridden from schedule: %s", base.Classes[danger.SystemWrite]) |
| 246 | + } |
| 247 | + if len(base.Allowlist) != 2 || base.Allowlist[0] != "base-allow" || base.Allowlist[1] != "schedule-allow" { |
| 248 | + t.Errorf("allowlist not merged: %v", base.Allowlist) |
| 249 | + } |
| 250 | + if len(base.Denylist) != 2 || base.Denylist[0] != "base-deny" || base.Denylist[1] != "schedule-deny" { |
| 251 | + t.Errorf("denylist not merged: %v", base.Denylist) |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | +func TestMergeScheduleDangerous_NilBaseClasses(t *testing.T) { |
| 256 | + base := danger.DangerousConfig{} |
| 257 | + schedule := danger.DangerousConfig{ |
| 258 | + Classes: map[danger.RiskClass]danger.Action{ |
| 259 | + danger.NetworkEgress: danger.Allow, |
| 260 | + }, |
| 261 | + } |
| 262 | + mergeScheduleDangerous(&base, schedule) |
| 263 | + if base.Classes[danger.NetworkEgress] != danger.Allow { |
| 264 | + t.Errorf("network_egress not added when base.Classes is nil: %s", base.Classes[danger.NetworkEgress]) |
| 265 | + } |
| 266 | +} |
| 267 | + |
| 268 | +func TestMergeScheduleDangerous_NilScheduleClasses(t *testing.T) { |
| 269 | + base := danger.DangerousConfig{ |
| 270 | + Classes: map[danger.RiskClass]danger.Action{ |
| 271 | + danger.SystemWrite: danger.Allow, |
| 272 | + }, |
| 273 | + } |
| 274 | + schedule := danger.DangerousConfig{ |
| 275 | + Allowlist: []string{"schedule-allow"}, |
| 276 | + } |
| 277 | + mergeScheduleDangerous(&base, schedule) |
| 278 | + if base.Classes[danger.SystemWrite] != danger.Allow { |
| 279 | + t.Errorf("base classes mutated when schedule.Classes is nil") |
| 280 | + } |
| 281 | + if len(base.Allowlist) != 1 || base.Allowlist[0] != "schedule-allow" { |
| 282 | + t.Errorf("allowlist not merged when schedule.Classes is nil: %v", base.Allowlist) |
| 283 | + } |
| 284 | +} |
| 285 | + |
| 286 | +func TestMergeScheduleDangerous_ActionAndNonInteractive(t *testing.T) { |
| 287 | + base := danger.DangerousConfig{} |
| 288 | + schedule := danger.DangerousConfig{ |
| 289 | + DefaultAction: strPtr("allow"), |
| 290 | + NonInteractive: strPtr("prompt"), |
| 291 | + } |
| 292 | + mergeScheduleDangerous(&base, schedule) |
| 293 | + if base.DefaultAction == nil || *base.DefaultAction != "allow" { |
| 294 | + t.Errorf("DefaultAction not overridden") |
| 295 | + } |
| 296 | + if base.NonInteractive == nil || *base.NonInteractive != "prompt" { |
| 297 | + t.Errorf("NonInteractive not overridden") |
| 298 | + } |
| 299 | +} |
| 300 | + |
| 301 | +func TestBuildHeadlessDangerConfig_Defaults(t *testing.T) { |
| 302 | + resolved := config.ResolvedConfig{} |
| 303 | + cfg := buildHeadlessDangerConfig(resolved) |
| 304 | + |
| 305 | + if cfg.NonInteractive == nil || *cfg.NonInteractive != "deny" { |
| 306 | + t.Errorf("non_interactive should be forced to deny, got %v", cfg.NonInteractive) |
| 307 | + } |
| 308 | + if cfg.Classes[danger.Destructive] != danger.Deny { |
| 309 | + t.Errorf("destructive should be denied, got %s", cfg.Classes[danger.Destructive]) |
| 310 | + } |
| 311 | + if cfg.Classes[danger.Blocked] != danger.Deny { |
| 312 | + t.Errorf("blocked should be denied, got %s", cfg.Classes[danger.Blocked]) |
| 313 | + } |
| 314 | +} |
| 315 | + |
| 316 | +func TestBuildHeadlessDangerConfig_ScheduleOverridesAllowed(t *testing.T) { |
| 317 | + resolved := config.ResolvedConfig{ |
| 318 | + Dangerous: danger.DangerousConfig{ |
| 319 | + Classes: map[danger.RiskClass]danger.Action{ |
| 320 | + danger.SystemWrite: danger.Prompt, |
| 321 | + }, |
| 322 | + }, |
| 323 | + Schedules: config.ScheduleConfig{ |
| 324 | + Dangerous: danger.DangerousConfig{ |
| 325 | + Classes: map[danger.RiskClass]danger.Action{ |
| 326 | + danger.NetworkEgress: danger.Allow, |
| 327 | + danger.SystemWrite: danger.Allow, |
| 328 | + danger.Destructive: danger.Allow, // should be floored back to deny |
| 329 | + }, |
| 330 | + }, |
| 331 | + }, |
| 332 | + } |
| 333 | + cfg := buildHeadlessDangerConfig(resolved) |
| 334 | + |
| 335 | + if cfg.Classes[danger.NetworkEgress] != danger.Allow { |
| 336 | + t.Errorf("network_egress should be allow via schedule override, got %s", cfg.Classes[danger.NetworkEgress]) |
| 337 | + } |
| 338 | + if cfg.Classes[danger.SystemWrite] != danger.Allow { |
| 339 | + t.Errorf("system_write should be allow via schedule override, got %s", cfg.Classes[danger.SystemWrite]) |
| 340 | + } |
| 341 | + if cfg.Classes[danger.Destructive] != danger.Deny { |
| 342 | + t.Errorf("destructive should remain denied by safety floor, got %s", cfg.Classes[danger.Destructive]) |
| 343 | + } |
| 344 | + if cfg.Classes[danger.Blocked] != danger.Deny { |
| 345 | + t.Errorf("blocked should remain denied by safety floor, got %s", cfg.Classes[danger.Blocked]) |
| 346 | + } |
| 347 | + if *cfg.NonInteractive != "deny" { |
| 348 | + t.Errorf("non_interactive should remain denied by safety floor, got %s", *cfg.NonInteractive) |
| 349 | + } |
| 350 | +} |
0 commit comments