|
1
|
package topology |
|
2
|
|
|
3
|
import ( |
|
4
|
"testing" |
|
5
|
"time" |
|
6
|
|
|
7
|
"github.com/conflicthq/scuttlebot/internal/config" |
|
8
|
) |
|
9
|
|
|
10
|
func testPolicy() *Policy { |
|
11
|
return NewPolicy(config.TopologyConfig{ |
|
12
|
Channels: []config.StaticChannelConfig{ |
|
13
|
{ |
|
14
|
Name: "#general", |
|
15
|
Topic: "Fleet coordination", |
|
16
|
Autojoin: []string{"bridge", "oracle", "scribe"}, |
|
17
|
}, |
|
18
|
{ |
|
19
|
Name: "#alerts", |
|
20
|
Autojoin: []string{"bridge", "sentinel", "steward"}, |
|
21
|
}, |
|
22
|
}, |
|
23
|
Types: []config.ChannelTypeConfig{ |
|
24
|
{ |
|
25
|
Name: "task", |
|
26
|
Prefix: "task.", |
|
27
|
Autojoin: []string{"bridge", "scribe"}, |
|
28
|
Supervision: "#general", |
|
29
|
Ephemeral: true, |
|
30
|
TTL: config.Duration{Duration: 72 * time.Hour}, |
|
31
|
}, |
|
32
|
{ |
|
33
|
Name: "sprint", |
|
34
|
Prefix: "sprint.", |
|
35
|
Autojoin: []string{"bridge", "oracle", "herald"}, |
|
36
|
Supervision: "", |
|
37
|
}, |
|
38
|
{ |
|
39
|
Name: "incident", |
|
40
|
Prefix: "incident.", |
|
41
|
Autojoin: []string{"bridge", "sentinel", "steward", "oracle"}, |
|
42
|
Supervision: "#alerts", |
|
43
|
Ephemeral: true, |
|
44
|
TTL: config.Duration{Duration: 168 * time.Hour}, |
|
45
|
}, |
|
46
|
{ |
|
47
|
Name: "experiment", |
|
48
|
Prefix: "experiment.", |
|
49
|
}, |
|
50
|
}, |
|
51
|
}) |
|
52
|
} |
|
53
|
|
|
54
|
func TestPolicyMatch(t *testing.T) { |
|
55
|
p := testPolicy() |
|
56
|
|
|
57
|
cases := []struct { |
|
58
|
channel string |
|
59
|
wantType string |
|
60
|
}{ |
|
61
|
{"#task.gh-42", "task"}, |
|
62
|
{"#task.JIRA-99", "task"}, |
|
63
|
{"#sprint.2026-q2", "sprint"}, |
|
64
|
{"#incident.p1", "incident"}, |
|
65
|
{"#experiment.llm-v3", "experiment"}, |
|
66
|
{"#general", ""}, // static, no type |
|
67
|
{"#unknown", ""}, // no match |
|
68
|
{"#taskforce", ""}, // prefix must match exactly (task. not task) |
|
69
|
} |
|
70
|
|
|
71
|
for _, tc := range cases { |
|
72
|
t.Run(tc.channel, func(t *testing.T) { |
|
73
|
got := p.TypeName(tc.channel) |
|
74
|
if got != tc.wantType { |
|
75
|
t.Errorf("TypeName(%q) = %q, want %q", tc.channel, got, tc.wantType) |
|
76
|
} |
|
77
|
}) |
|
78
|
} |
|
79
|
} |
|
80
|
|
|
81
|
func TestPolicyAutojoinFor(t *testing.T) { |
|
82
|
p := testPolicy() |
|
83
|
|
|
84
|
cases := []struct { |
|
85
|
channel string |
|
86
|
wantBots []string |
|
87
|
}{ |
|
88
|
{"#task.gh-42", []string{"bridge", "scribe"}}, |
|
89
|
{"#sprint.2026-q2", []string{"bridge", "oracle", "herald"}}, |
|
90
|
{"#incident.p1", []string{"bridge", "sentinel", "steward", "oracle"}}, |
|
91
|
{"#general", []string{"bridge", "oracle", "scribe"}}, // static channel |
|
92
|
{"#alerts", []string{"bridge", "sentinel", "steward"}}, // static channel |
|
93
|
{"#unknown", nil}, |
|
94
|
{"#experiment.llm-v3", nil}, // type exists but no autojoin configured |
|
95
|
} |
|
96
|
|
|
97
|
for _, tc := range cases { |
|
98
|
t.Run(tc.channel, func(t *testing.T) { |
|
99
|
got := p.AutojoinFor(tc.channel) |
|
100
|
if len(got) != len(tc.wantBots) { |
|
101
|
t.Fatalf("AutojoinFor(%q) = %v, want %v", tc.channel, got, tc.wantBots) |
|
102
|
} |
|
103
|
for i, nick := range tc.wantBots { |
|
104
|
if got[i] != nick { |
|
105
|
t.Errorf("AutojoinFor(%q)[%d] = %q, want %q", tc.channel, i, got[i], nick) |
|
106
|
} |
|
107
|
} |
|
108
|
}) |
|
109
|
} |
|
110
|
} |
|
111
|
|
|
112
|
func TestPolicySupervisionFor(t *testing.T) { |
|
113
|
p := testPolicy() |
|
114
|
|
|
115
|
cases := []struct { |
|
116
|
channel string |
|
117
|
want string |
|
118
|
}{ |
|
119
|
{"#task.gh-42", "#general"}, |
|
120
|
{"#incident.p1", "#alerts"}, |
|
121
|
{"#sprint.2026-q2", ""}, |
|
122
|
{"#general", ""}, |
|
123
|
{"#unknown", ""}, |
|
124
|
} |
|
125
|
|
|
126
|
for _, tc := range cases { |
|
127
|
t.Run(tc.channel, func(t *testing.T) { |
|
128
|
got := p.SupervisionFor(tc.channel) |
|
129
|
if got != tc.want { |
|
130
|
t.Errorf("SupervisionFor(%q) = %q, want %q", tc.channel, got, tc.want) |
|
131
|
} |
|
132
|
}) |
|
133
|
} |
|
134
|
} |
|
135
|
|
|
136
|
func TestPolicyEphemeral(t *testing.T) { |
|
137
|
p := testPolicy() |
|
138
|
|
|
139
|
if !p.IsEphemeral("#task.gh-42") { |
|
140
|
t.Error("#task.gh-42 should be ephemeral") |
|
141
|
} |
|
142
|
if p.IsEphemeral("#sprint.2026-q2") { |
|
143
|
t.Error("#sprint.2026-q2 should not be ephemeral") |
|
144
|
} |
|
145
|
if p.IsEphemeral("#general") { |
|
146
|
t.Error("#general should not be ephemeral") |
|
147
|
} |
|
148
|
|
|
149
|
if got := p.TTLFor("#task.gh-42"); got != 72*time.Hour { |
|
150
|
t.Errorf("TTLFor #task.gh-42 = %v, want 72h", got) |
|
151
|
} |
|
152
|
if got := p.TTLFor("#incident.p1"); got != 168*time.Hour { |
|
153
|
t.Errorf("TTLFor #incident.p1 = %v, want 168h", got) |
|
154
|
} |
|
155
|
if got := p.TTLFor("#sprint.2026-q2"); got != 0 { |
|
156
|
t.Errorf("TTLFor #sprint.2026-q2 = %v, want 0", got) |
|
157
|
} |
|
158
|
} |
|
159
|
|
|
160
|
func TestPolicyStaticChannels(t *testing.T) { |
|
161
|
p := testPolicy() |
|
162
|
statics := p.StaticChannels() |
|
163
|
if len(statics) != 2 { |
|
164
|
t.Fatalf("want 2 static channels, got %d", len(statics)) |
|
165
|
} |
|
166
|
if statics[0].Name != "#general" { |
|
167
|
t.Errorf("statics[0].Name = %q, want #general", statics[0].Name) |
|
168
|
} |
|
169
|
} |
|
170
|
|
|
171
|
func TestPolicyTypes(t *testing.T) { |
|
172
|
p := testPolicy() |
|
173
|
types := p.Types() |
|
174
|
if len(types) != 4 { |
|
175
|
t.Fatalf("want 4 types, got %d", len(types)) |
|
176
|
} |
|
177
|
} |
|
178
|
|
|
179
|
func TestNewPolicyEmpty(t *testing.T) { |
|
180
|
p := NewPolicy(config.TopologyConfig{}) |
|
181
|
if p.Match("#anything") != nil { |
|
182
|
t.Error("empty policy should not match") |
|
183
|
} |
|
184
|
if p.AutojoinFor("#general") != nil { |
|
185
|
t.Error("empty policy should return nil autojoin") |
|
186
|
} |
|
187
|
} |
|
188
|
|