05594113a2
agentpr/watchpr already propagated command errors to a non-zero exit, but that behaviour had no regression coverage and the root command was not constructible outside main(). watchpr also fired a spurious conflict alert because Gitea computes mergeability asynchronously and can briefly report mergeable=false right after a push. The docs additionally printed the AppRole role_id literal UUID. - Extract newRootCmd() in both cmd/agentpr and cmd/watchpr so main() only runs Execute and exits non-zero on error; add tests asserting Execute returns an error for a bad PR ref / malformed --repo / no args. - Debounce mergeability loss in MeaningfulChange: only alert when mergeable=false persists across two consecutive polls (both prev and cur false, still open); update the table test for one-poll-false (benign), false-persisting (alert), and recovered false->true (benign). - Refer to AGENT_APPROLE_ROLE_ID by env var in README.md/AGENTS.md without printing the literal role_id; keep the code default and env override.
138 lines
3.7 KiB
Go
138 lines
3.7 KiB
Go
package agent
|
|
|
|
import "testing"
|
|
|
|
func base() PRState {
|
|
return PRState{
|
|
Ref: PRRef{Owner: "unkin", Repo: "repo", Number: 1},
|
|
State: "open",
|
|
Merged: false,
|
|
HeadSHA: "abc123",
|
|
Mergeable: true,
|
|
CIStatus: "pending",
|
|
NonAgentComments: 0,
|
|
}
|
|
}
|
|
|
|
func TestMeaningfulChange(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutatePrev func(s *PRState)
|
|
mutate func(s *PRState)
|
|
wantChange bool
|
|
}{
|
|
{
|
|
name: "no change",
|
|
mutate: func(s *PRState) {},
|
|
wantChange: false,
|
|
},
|
|
{
|
|
name: "CI pending to success is benign",
|
|
mutate: func(s *PRState) { s.CIStatus = "success" },
|
|
wantChange: false,
|
|
},
|
|
{
|
|
name: "open to merged alerts",
|
|
mutate: func(s *PRState) { s.Merged = true; s.State = "closed" },
|
|
wantChange: true,
|
|
},
|
|
{
|
|
name: "open to closed without merge alerts",
|
|
mutate: func(s *PRState) { s.State = "closed" },
|
|
wantChange: true,
|
|
},
|
|
{
|
|
name: "new non-agent comment alerts",
|
|
mutate: func(s *PRState) { s.NonAgentComments = 1 },
|
|
wantChange: true,
|
|
},
|
|
{
|
|
name: "CI to failure alerts",
|
|
mutate: func(s *PRState) { s.CIStatus = "failure" },
|
|
wantChange: true,
|
|
},
|
|
{
|
|
name: "CI to error alerts",
|
|
mutate: func(s *PRState) { s.CIStatus = "error" },
|
|
wantChange: true,
|
|
},
|
|
{
|
|
// A single mergeable=false poll is debounced: Gitea often reports
|
|
// this transiently right after a push.
|
|
name: "mergeable true to false for one poll is benign",
|
|
mutate: func(s *PRState) { s.Mergeable = false },
|
|
wantChange: false,
|
|
},
|
|
{
|
|
// mergeable=false persisting into a second consecutive poll is a
|
|
// real conflict and alerts.
|
|
name: "mergeable false persisting a second poll alerts",
|
|
mutatePrev: func(s *PRState) { s.Mergeable = false },
|
|
mutate: func(s *PRState) { s.Mergeable = false },
|
|
wantChange: true,
|
|
},
|
|
{
|
|
// mergeable recovered (false then true) must not alert.
|
|
name: "mergeable recovered false to true is benign",
|
|
mutatePrev: func(s *PRState) { s.Mergeable = false },
|
|
mutate: func(s *PRState) {},
|
|
wantChange: false,
|
|
},
|
|
{
|
|
name: "new head sha alone is benign",
|
|
mutate: func(s *PRState) { s.HeadSHA = "def456" },
|
|
wantChange: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
prev := base()
|
|
if tt.mutatePrev != nil {
|
|
tt.mutatePrev(&prev)
|
|
}
|
|
cur := base()
|
|
tt.mutate(&cur)
|
|
got, reason := MeaningfulChange(prev, cur)
|
|
if got != tt.wantChange {
|
|
t.Errorf("MeaningfulChange() = %v (%q), want %v", got, reason, tt.wantChange)
|
|
}
|
|
if got && reason == "" {
|
|
t.Errorf("change reported without a reason")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A comment that only the agent posts must not alert: the non-agent count is
|
|
// unchanged, so MeaningfulChange sees nothing.
|
|
func TestMeaningfulChangeAgentCommentIgnored(t *testing.T) {
|
|
prev := base()
|
|
cur := base() // agent commented, but NonAgentComments stayed 0
|
|
if got, _ := MeaningfulChange(prev, cur); got {
|
|
t.Errorf("agent-only comment should not alert")
|
|
}
|
|
}
|
|
|
|
// Once CI is already failing, staying failed must not re-alert.
|
|
func TestMeaningfulChangeStaysFailed(t *testing.T) {
|
|
prev := base()
|
|
prev.CIStatus = "failure"
|
|
cur := base()
|
|
cur.CIStatus = "failure"
|
|
if got, _ := MeaningfulChange(prev, cur); got {
|
|
t.Errorf("CI staying failed should not re-alert")
|
|
}
|
|
}
|
|
|
|
func TestCountNonAgentComments(t *testing.T) {
|
|
comments := []Comment{
|
|
{User: User{Login: "unkin-agent"}},
|
|
{User: User{Login: "ben"}},
|
|
{User: User{Login: "unkin-agent"}},
|
|
{User: User{Login: "reviewer"}},
|
|
}
|
|
if n := countNonAgentComments(comments, "unkin-agent"); n != 2 {
|
|
t.Errorf("countNonAgentComments = %d, want 2", n)
|
|
}
|
|
}
|