Repository: jenkins-x/lighthouse
Pull request #1052 (2020-09-12 10:04:07)

refactor: trigger plugin
This PR refactors the trigger plugin.

This is the last plugin refactor of the list 🎉 

jenkins-x-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **eddycharly**
You can assign the PR to them by writing `/assign @eddycharly` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=jenkins-x%2Flighthouse).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/jenkins-x/lighthouse/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["eddycharly"]} -->
abayer: /lgtm
jenkins-x-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *[abayer](https://github.com/jenkins-x/lighthouse/pull/1052#issuecomment-691503406 "LGTM")*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=jenkins-x%2Flighthouse).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com#issuecomment-691503406/jenkins-x/lighthouse/blob/master/OWNERS)~~ [abayer]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/pkg/plugins/override/override.go b/pkg/plugins/override/override.go
index dd63642e..784d69b9 100644
--- a/pkg/plugins/override/override.go
+++ b/pkg/plugins/override/override.go
@@ -25,6 +25,7 @@ import (
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
 	lighthouseclient "github.com/jenkins-x/lighthouse/pkg/client/clientset/versioned/typed/lighthouse/v1alpha1"
+	"github.com/jenkins-x/lighthouse/pkg/config"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/jobutil"
 	"github.com/jenkins-x/lighthouse/pkg/pluginhelp"
@@ -55,69 +56,18 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
-type overrideClient interface {
-	scmProviderClient
-	presubmitForContext(org, repo, context string) *job.Presubmit
-	createOverrideJob(job *v1alpha1.LighthouseJob) (*v1alpha1.LighthouseJob, error)
-}
-
-type client struct {
-	spc      scmProviderClient
-	jc       job.Config
-	lhClient lighthouseclient.LighthouseJobInterface
-}
-
-func (c client) createOverrideJob(job *v1alpha1.LighthouseJob) (*v1alpha1.LighthouseJob, error) {
+func createOverrideJob(lhClient lighthouseclient.LighthouseJobInterface, job *v1alpha1.LighthouseJob) (*v1alpha1.LighthouseJob, error) {
 	overrideStatus := job.Status
-	createdJob, err := c.lhClient.Create(job)
+	createdJob, err := lhClient.Create(job)
 	if err != nil {
 		return nil, err
 	}
 	createdJob.Status = overrideStatus
-	return c.lhClient.UpdateStatus(createdJob)
-}
-
-func (c client) ProviderType() string {
-	return c.spc.ProviderType()
-}
-
-func (c client) PRRefFmt() string {
-	return c.spc.PRRefFmt()
-}
-
-func (c client) IsOrgAdmin(org, user string) (bool, error) {
-	return c.spc.IsOrgAdmin(org, user)
-}
-
-func (c client) CreateComment(owner, repo string, number int, pr bool, comment string) error {
-	return c.spc.CreateComment(owner, repo, number, pr, comment)
-}
-func (c client) CreateStatus(org, repo, ref string, s *scm.StatusInput) (*scm.Status, error) {
-	return c.spc.CreateStatus(org, repo, ref, s)
-}
-
-func (c client) GetRef(org, repo, ref string) (string, error) {
-	return c.spc.GetRef(org, repo, ref)
-}
-
-func (c client) GetPullRequest(org, repo string, number int) (*scm.PullRequest, error) {
-	return c.spc.GetPullRequest(org, repo, number)
-}
-
-func (c client) ListStatuses(org, repo, ref string) ([]*scm.Status, error) {
-	return c.spc.ListStatuses(org, repo, ref)
-}
-
-func (c client) HasPermission(org, repo, user string, role ...string) (bool, error) {
-	return c.spc.HasPermission(org, repo, user, role...)
-}
-
-func (c client) QuoteAuthorForComment(author string) string {
-	return c.spc.QuoteAuthorForComment(author)
+	return lhClient.UpdateStatus(createdJob)
 }
 
-func (c client) presubmitForContext(org, repo, context string) *job.Presubmit {
-	for _, p := range c.jc.AllPresubmits([]string{org + "/" + repo}) {
+func presubmitForContext(jc config.JobConfig, org, repo, context string) *job.Presubmit {
+	for _, p := range jc.AllPresubmits([]string{org + "/" + repo}) {
 		if p.Context == context {
 			return &p
 		}
@@ -129,10 +79,13 @@ var (
 	plugin = plugins.Plugin{
 		Description: "The override plugin allows repo admins to force a github status context to pass",
 		Commands: []plugins.Command{{
-			GenericCommentHandler: handleGenericComment,
 			Filter: func(e scmprovider.GenericCommentEvent) bool {
 				return !(!e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate)
 			},
+			Regex: overrideRe,
+			GenericCommentHandler: func(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return handle(match, pc.SCMProviderClient, pc.LighthouseClient, pc.Config.JobConfig, pc.Logger, e)
+			},
 			Help: []pluginhelp.Command{{
 				Usage:       "/override [context]",
 				Description: "Forces a github status context to green (one per line).",
@@ -148,17 +101,6 @@ func init() {
 	plugins.RegisterPlugin(pluginName, plugin)
 }
 
-func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	c := client{
-		spc:      pc.SCMProviderClient,
-		lhClient: pc.LighthouseClient,
-	}
-	if pc.Config != nil {
-		c.jc = pc.Config.JobConfig
-	}
-	return handle(c, pc.Logger, &e)
-}
-
 func authorized(spc scmProviderClient, log *logrus.Entry, org, repo, user string) bool {
 	ok, err := spc.HasPermission(org, repo, user, scmprovider.RoleAdmin)
 	if err != nil {
@@ -187,46 +129,39 @@ func formatList(list []string) string {
 	return strings.Join(lines, "\n")
 }
 
-func handle(oc overrideClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
-	mat := overrideRe.FindAllStringSubmatch(e.Body, -1)
-	if len(mat) == 0 {
-		return nil // no /override commands given in the comment
-	}
-
+func handle(match []string, spc scmProviderClient, lhClient lighthouseclient.LighthouseJobInterface, jc config.JobConfig, log *logrus.Entry, e scmprovider.GenericCommentEvent) error {
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 	number := e.Number
 	user := e.Author.Login
 
 	overrides := sets.NewString()
-	for _, m := range mat {
-		if m[1] == "" {
-			resp := "/override requires a failed status context to operate on, but none was given"
-			log.Debug(resp)
-			return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
-		}
-		overrides.Insert(m[2])
+	if match[1] == "" {
+		resp := "/override requires a failed status context to operate on, but none was given"
+		log.Debug(resp)
+		return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 	}
+	overrides.Insert(match[2])
 
-	if !authorized(oc, log, org, repo, user) {
+	if !authorized(spc, log, org, repo, user) {
 		resp := fmt.Sprintf("%s unauthorized: /override is restricted to repo administrators", user)
 		log.Debug(resp)
-		return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+		return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 	}
 
-	pr, err := oc.GetPullRequest(org, repo, number)
+	pr, err := spc.GetPullRequest(org, repo, number)
 	if err != nil {
 		resp := fmt.Sprintf("Cannot get PR #%d in %s/%s", number, org, repo)
 		log.WithError(err).Warn(resp)
-		return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+		return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 	}
 
 	sha := pr.Head.Sha
-	statuses, err := oc.ListStatuses(org, repo, sha)
+	statuses, err := spc.ListStatuses(org, repo, sha)
 	if err != nil {
 		resp := fmt.Sprintf("Cannot get commit statuses for PR #%d in %s/%s", number, org, repo)
 		log.WithError(err).Warn(resp)
-		return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+		return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 	}
 
 	contexts := sets.NewString()
@@ -244,7 +179,7 @@ The following unknown contexts were given:
 Only the following contexts were expected:
 %s`, formatList(unknown.List()), formatList(contexts.List()))
 		log.Debug(resp)
-		return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+		return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 	}
 
 	done := sets.String{}
@@ -255,7 +190,7 @@ Only the following contexts were expected:
 		}
 		msg := fmt.Sprintf("Overrode contexts on behalf of %s: %s", user, strings.Join(done.List(), ", "))
 		log.Info(msg)
-		err := oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), msg))
+		err := spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), msg))
 		if err != nil {
 			log.WithError(err).Warn("Failed to create the comment")
 		}
@@ -266,15 +201,15 @@ Only the following contexts were expected:
 			continue
 		}
 		// First create the overridden prow result if necessary
-		if pre := oc.presubmitForContext(org, repo, status.Label); pre != nil {
-			baseSHA, err := oc.GetRef(org, repo, "heads/"+pr.Base.Ref)
+		if pre := presubmitForContext(jc, org, repo, status.Label); pre != nil {
+			baseSHA, err := spc.GetRef(org, repo, "heads/"+pr.Base.Ref)
 			if err != nil {
 				resp := fmt.Sprintf("Cannot get base ref of PR")
 				log.WithError(err).Warn(resp)
-				return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+				return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 			}
 
-			pj := jobutil.NewPresubmit(pr, baseSHA, *pre, e.GUID, oc.PRRefFmt())
+			pj := jobutil.NewPresubmit(pr, baseSHA, *pre, e.GUID, spc.PRRefFmt())
 			now := metav1.Now()
 			pj.Status = v1alpha1.LighthouseJobStatus{
 				State:          v1alpha1.SuccessState,
@@ -283,10 +218,10 @@ Only the following contexts were expected:
 				CompletionTime: &now,
 			}
 			log.WithFields(jobutil.LighthouseJobFields(&pj)).Info("Creating a new override LighthouseJob.")
-			if _, err := oc.createOverrideJob(&pj); err != nil {
+			if _, err := createOverrideJob(lhClient, &pj); err != nil {
 				resp := fmt.Sprintf("Failed to create override job for %s", status.Label)
 				log.WithError(err).Warn(resp)
-				return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+				return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 			}
 		}
 		statusInput := &scm.StatusInput{
@@ -295,10 +230,10 @@ Only the following contexts were expected:
 			Target: status.Target,
 			Desc:   description(user),
 		}
-		if _, err := oc.CreateStatus(org, repo, sha, statusInput); err != nil {
+		if _, err := spc.CreateStatus(org, repo, sha, statusInput); err != nil {
 			resp := fmt.Sprintf("Cannot update PR status for context %s", statusInput.Label)
 			log.WithError(err).Warn(resp)
-			return oc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, oc.QuoteAuthorForComment(user), resp))
+			return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(user), resp))
 		}
 		done.Insert(status.Label)
 	}
diff --git a/pkg/plugins/trigger/generic-comment.go b/pkg/plugins/trigger/generic-comment.go
index 00d77183..bb1c6bc6 100644
--- a/pkg/plugins/trigger/generic-comment.go
+++ b/pkg/plugins/trigger/generic-comment.go
@@ -36,21 +36,6 @@ func handleGenericComment(c Client, trigger *plugins.Trigger, gc scmprovider.Gen
 	number := gc.Number
 	commentAuthor := gc.Author.Login
 
-	// Skip comments not germane to this plugin
-	if !jobutil.RetestRe.MatchString(gc.Body) && !jobutil.OkToTestRe.MatchString(gc.Body) && !jobutil.TestAllRe.MatchString(gc.Body) {
-		matched := false
-		for _, presubmit := range c.Config.GetPresubmits(gc.Repo) {
-			matched = matched || presubmit.TriggerMatches(gc.Body)
-			if matched {
-				break
-			}
-		}
-		if !matched {
-			c.Logger.Debug("Comment doesn't match any triggering regex, skipping.")
-			return nil
-		}
-	}
-
 	// Skip bot comments.
 	botName, err := c.SCMProviderClient.BotName()
 	if err != nil {
diff --git a/pkg/plugins/trigger/trigger.go b/pkg/plugins/trigger/trigger.go
index 7df15c2f..deaa0a4a 100644
--- a/pkg/plugins/trigger/trigger.go
+++ b/pkg/plugins/trigger/trigger.go
@@ -18,6 +18,7 @@ package trigger
 
 import (
 	"fmt"
+	"regexp"
 	"strings"
 
 	"github.com/jenkins-x/go-scm/scm"
@@ -51,19 +52,34 @@ var (
 			Filter: func(e scmprovider.GenericCommentEvent) bool {
 				return !(e.Action != scm.ActionCreate || !e.IsPR || e.IssueState != "open")
 			},
+			Regex: jobutil.OkToTestRe,
 			Help: []pluginhelp.Command{{
 				Usage:       "/ok-to-test",
 				Description: "Marks a PR as 'trusted' and starts tests.",
 				Featured:    false,
 				WhoCanUse:   "Members of the trusted organization for the repo.",
 				Examples:    []string{"/ok-to-test", "/lh-ok-to-test"},
-			}, {
+			}},
+		}, {
+			GenericCommentHandler: handleGenericCommentEvent,
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(e.Action != scm.ActionCreate || !e.IsPR || e.IssueState != "open")
+			},
+			Regex: regexp.MustCompile(`(?m)^/(?:lh-)?test\s+([-\w]+(?:,[-\w]+)*)\s*$`),
+			Help: []pluginhelp.Command{{
 				Usage:       "/test (<job name>|all)",
 				Description: "Manually starts a/all test job(s).",
 				Featured:    true,
 				WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
 				Examples:    []string{"/test all", "/test pull-bazel-test", "/lh-test all"},
-			}, {
+			}},
+		}, {
+			GenericCommentHandler: handleGenericCommentEvent,
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(e.Action != scm.ActionCreate || !e.IsPR || e.IssueState != "open")
+			},
+			Regex: jobutil.RetestRe,
+			Help: []pluginhelp.Command{{
 				Usage:       "/retest",
 				Description: "Rerun test jobs that have failed.",
 				Featured:    true,


Test patch:
diff --git a/pkg/plugins/override/override_test.go b/pkg/plugins/override/override_test.go
index 3234a99f..d42da7b9 100644
--- a/pkg/plugins/override/override_test.go
+++ b/pkg/plugins/override/override_test.go
@@ -17,15 +17,15 @@ limitations under the License.
 package override
 
 import (
-	"errors"
 	"fmt"
 	"reflect"
-	"strings"
 	"testing"
 
 	"github.com/jenkins-x/go-scm/scm"
-	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
+	"github.com/jenkins-x/go-scm/scm/driver/fake"
+	"github.com/jenkins-x/lighthouse/pkg/config"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
+	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/sirupsen/logrus"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -40,140 +40,12 @@ const (
 	adminUser   = "admin-user"
 )
 
-type fakeClient struct {
-	comments   []string
-	statuses   map[string]*scm.StatusInput
-	presubmits map[string]job.Presubmit
-	jobs       sets.String
-}
-
-func (c *fakeClient) ProviderType() string {
-	return "fake"
-}
-
-func (c *fakeClient) PRRefFmt() string {
-	return "refs/pull/%d/head"
-}
-
-func (c *fakeClient) IsOrgAdmin(org, user string) (bool, error) {
-	return false, nil
-}
-
-func (c *fakeClient) CreateComment(org, repo string, number int, pr bool, comment string) error {
-	switch {
-	case org != fakeOrg:
-		return fmt.Errorf("bad org: %s", org)
-	case repo != fakeRepo:
-		return fmt.Errorf("bad repo: %s", repo)
-	case number != fakePR:
-		return fmt.Errorf("bad number: %d", number)
-	case strings.Contains(comment, "fail-comment"):
-		return errors.New("injected CreateComment failure")
-	}
-	c.comments = append(c.comments, comment)
-	return nil
-}
-
-func (c *fakeClient) CreateStatus(org, repo, ref string, s *scm.StatusInput) (*scm.Status, error) {
-	switch {
-	case s.Label == "fail-create":
-		return nil, errors.New("injected CreateStatus failure")
-	case org != fakeOrg:
-		return nil, fmt.Errorf("bad org: %s", org)
-	case repo != fakeRepo:
-		return nil, fmt.Errorf("bad repo: %s", repo)
-	case ref != fakeSHA:
-		return nil, fmt.Errorf("bad ref: %s", ref)
-	}
-	c.statuses[s.Label] = s
-	return scm.ConvertStatusInputToStatus(s), nil
-}
-
-func (c *fakeClient) GetPullRequest(org, repo string, number int) (*scm.PullRequest, error) {
-	switch {
-	case number < 0:
-		return nil, errors.New("injected GetPullRequest failure")
-	case org != fakeOrg:
-		return nil, fmt.Errorf("bad org: %s", org)
-	case repo != fakeRepo:
-		return nil, fmt.Errorf("bad repo: %s", repo)
-	case number != fakePR:
-		return nil, fmt.Errorf("bad number: %d", number)
-	}
-	var pr scm.PullRequest
-	pr.Head.Sha = fakeSHA
-	return &pr, nil
-}
-
-func (c *fakeClient) ListStatuses(org, repo, ref string) ([]*scm.Status, error) {
-	switch {
-	case org != fakeOrg:
-		return nil, fmt.Errorf("bad org: %s", org)
-	case repo != fakeRepo:
-		return nil, fmt.Errorf("bad repo: %s", repo)
-	case ref != fakeSHA:
-		return nil, fmt.Errorf("bad ref: %s", ref)
-	}
-	var out []*scm.Status
-	for _, s := range c.statuses {
-		if s.Label == "fail-list" {
-			return nil, errors.New("injected ListStatuses failure")
-		}
-		out = append(out, scm.ConvertStatusInputToStatus(s))
-	}
-	return out, nil
-}
-
-func (c *fakeClient) HasPermission(org, repo, user string, roles ...string) (bool, error) {
-	switch {
-	case org != fakeOrg:
-		return false, fmt.Errorf("bad org: %s", org)
-	case repo != fakeRepo:
-		return false, fmt.Errorf("bad repo: %s", repo)
-	case roles[0] != scmprovider.RoleAdmin:
-		return false, fmt.Errorf("bad roles: %s", roles)
-	case user == "fail":
-		return true, errors.New("injected HasPermission error")
-	}
-	return user == adminUser, nil
-}
-
-func (c *fakeClient) GetRef(org, repo, ref string) (string, error) {
-	if repo == "fail-ref" {
-		return "", errors.New("injected GetRef error")
-	}
-	return fakeBaseSHA, nil
-}
-
-func (c *fakeClient) Launch(pj *v1alpha1.LighthouseJob, repository scm.Repository) (*v1alpha1.LighthouseJob, error) {
-	if pj.Spec.Context == "fail-create" {
-		return pj, errors.New("injected Launch error")
-	}
-	c.jobs.Insert(pj.Spec.Context)
-	return pj, nil
-}
-
-func (c *fakeClient) QuoteAuthorForComment(author string) string {
-	return author
-}
-
-func (c *fakeClient) presubmitForContext(org, repo, context string) *job.Presubmit {
-	p, ok := c.presubmits[context]
-	if !ok {
-		return nil
-	}
-	return &p
-}
-
-func (c *fakeClient) createOverrideJob(pj *v1alpha1.LighthouseJob) (*v1alpha1.LighthouseJob, error) {
-	if s := pj.Status.State; s != v1alpha1.SuccessState {
-		return pj, fmt.Errorf("bad status state: %s", s)
-	}
-	if pj.Spec.Context == "fail-create" {
-		return pj, errors.New("injected CreateProwJob error")
+func issueLabels(labels ...string) []string {
+	ls := []string{}
+	for _, label := range labels {
+		ls = append(ls, fmt.Sprintf("org/repo#0:%s", label))
 	}
-	c.jobs.Insert(pj.Spec.Context)
-	return pj, nil
+	return ls
 }
 
 func TestAuthorized(t *testing.T) {
@@ -187,7 +59,7 @@ func TestAuthorized(t *testing.T) {
 			user: "fail",
 		},
 		{
-			name: "reject rando",
+			name: "reject random",
 			user: "random",
 		},
 		{
@@ -200,7 +72,12 @@ func TestAuthorized(t *testing.T) {
 	log := logrus.WithField("plugin", pluginName)
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			if actual := authorized(&fakeClient{}, log, fakeOrg, fakeRepo, tc.user); actual != tc.expected {
+			fakeScmClient, fc := fake.NewDefault()
+			fakeClient := scmprovider.ToTestClient(fakeScmClient)
+			fc.UserPermissions[fakeOrg+"/"+fakeRepo] = map[string]string{
+				adminUser: "admin",
+			}
+			if actual := authorized(&fakeClient.Client, log, fakeOrg, fakeRepo, tc.user); actual != tc.expected {
 				t.Errorf("actual %t != expected %t", actual, tc.expected)
 			}
 		})
@@ -214,11 +91,11 @@ func TestHandle(t *testing.T) {
 		issue         bool
 		state         string
 		comment       string
-		contexts      map[string]*scm.StatusInput
+		contexts      map[string]*scm.Status
 		presubmits    map[string]job.Presubmit
 		user          string
 		number        int
-		expected      map[string]*scm.StatusInput
+		expected      []*scm.Status
 		jobs          sets.String
 		checkComments []string
 		err           bool
@@ -226,14 +103,14 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "successfully override failure",
 			comment: "/override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StateFailure,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
@@ -244,14 +121,14 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "successfully override failure with prefix",
 			comment: "/lh-override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StateFailure,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
@@ -262,14 +139,14 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "successfully override pending",
 			comment: "/override hung-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"hung-test": {
 					Label: "hung-test",
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"hung-test": {
+			expected: []*scm.Status{
+				{
 					Label: "hung-test",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
@@ -279,14 +156,14 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "comment for incorrect context",
 			comment: "/override whatever-you-want",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"hung-test": {
 					Label: "hung-test",
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"hung-test": {
+			expected: []*scm.Status{
+				{
 					Label: "hung-test",
 					State: scm.StatePending,
 				},
@@ -299,7 +176,7 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "refuse override from non-admin",
 			comment: "/override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StatePending,
@@ -307,8 +184,8 @@ func TestHandle(t *testing.T) {
 			},
 			user:          "rando",
 			checkComments: []string{"unauthorized"},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
@@ -317,7 +194,7 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "comment for override with no target",
 			comment: "/override",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StatePending,
@@ -325,8 +202,8 @@ func TestHandle(t *testing.T) {
 			},
 			user:          "rando",
 			checkComments: []string{"but none was given"},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
@@ -335,7 +212,7 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "override multiple",
 			comment: "/override broken-test\n/override hung-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StateFailure,
@@ -345,14 +222,14 @@ func TestHandle(t *testing.T) {
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"hung-test": {
-					Label: "hung-test",
+			expected: []*scm.Status{
+				{
+					Label: "broken-test",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
 				},
-				"broken-test": {
-					Label: "broken-test",
+				{
+					Label: "hung-test",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
 				},
@@ -363,14 +240,14 @@ func TestHandle(t *testing.T) {
 			name:    "ignore non-PRs",
 			issue:   true,
 			comment: "/override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
@@ -380,14 +257,14 @@ func TestHandle(t *testing.T) {
 			name:    "ignore closed issues",
 			state:   "closed",
 			comment: "/override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
@@ -397,14 +274,14 @@ func TestHandle(t *testing.T) {
 			name:    "ignore edits",
 			action:  scm.ActionUpdate,
 			comment: "/override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
@@ -413,14 +290,14 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "ignore random text",
 			comment: "/test broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					State: scm.StatePending,
 				},
@@ -430,14 +307,14 @@ func TestHandle(t *testing.T) {
 			name:    "comment on get pr failure",
 			number:  fakePR * 2,
 			comment: "/override broken-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"broken-test": {
 					Label: "broken-test",
 					State: scm.StateFailure,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"broken-test": {
+			expected: []*scm.Status{
+				{
 					Label: "broken-test",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
@@ -445,35 +322,18 @@ func TestHandle(t *testing.T) {
 			},
 			checkComments: []string{"Cannot get PR"},
 		},
-		{
-			name:    "comment on list statuses failure",
-			comment: "/override fail-list",
-			contexts: map[string]*scm.StatusInput{
-				"fail-list": {
-					Label: "fail-list",
-					State: scm.StateFailure,
-				},
-			},
-			expected: map[string]*scm.StatusInput{
-				"fail-list": {
-					Label: "fail-list",
-					State: scm.StateFailure,
-				},
-			},
-			checkComments: []string{"Cannot get commit statuses"},
-		},
 		{
 			name:    "do not override passing contexts",
 			comment: "/override passing-test",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"passing-test": {
 					Label: "passing-test",
 					Desc:  "preserve description",
 					State: scm.StateSuccess,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"passing-test": {
+			expected: []*scm.Status{
+				{
 					Label: "passing-test",
 					State: scm.StateSuccess,
 					Desc:  "preserve description",
@@ -483,7 +343,7 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "create successful prow job",
 			comment: "/override prow-job",
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"prow-job": {
 					Label: "prow-job",
 					Desc:  "failed",
@@ -498,8 +358,8 @@ func TestHandle(t *testing.T) {
 				},
 			},
 			jobs: sets.NewString("prow-job"),
-			expected: map[string]*scm.StatusInput{
-				"prow-job": {
+			expected: []*scm.Status{
+				{
 					Label: "prow-job",
 					State: scm.StateSuccess,
 					Desc:  description(adminUser),
@@ -509,15 +369,15 @@ func TestHandle(t *testing.T) {
 		{
 			name:    "override with explanation works",
 			comment: "/override job\r\nobnoxious flake", // github ends lines with \r\n
-			contexts: map[string]*scm.StatusInput{
+			contexts: map[string]*scm.Status{
 				"job": {
 					Label: "job",
 					Desc:  "failed",
 					State: scm.StateFailure,
 				},
 			},
-			expected: map[string]*scm.StatusInput{
-				"job": {
+			expected: []*scm.Status{
+				{
 					Label: "job",
 					Desc:  description(adminUser),
 					State: scm.StateSuccess,
@@ -526,7 +386,6 @@ func TestHandle(t *testing.T) {
 		},
 	}
 
-	log := logrus.WithField("plugin", pluginName)
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			var event scmprovider.GenericCommentEvent
@@ -547,21 +406,31 @@ func TestHandle(t *testing.T) {
 				tc.action = scm.ActionCreate
 			}
 			event.Action = tc.action
-			if tc.contexts == nil {
-				tc.contexts = map[string]*scm.StatusInput{}
+			fakeScmClient, fc := fake.NewDefault()
+			fakeClient := scmprovider.ToTestClient(fakeScmClient)
+			fc.UserPermissions[fakeOrg+"/"+fakeRepo] = map[string]string{
+				adminUser: "admin",
+			}
+			fc.PullRequests[fakePR] = &scm.PullRequest{
+				Head: scm.PullRequestBranch{
+					Sha: fakeOrg + "/" + fakeRepo,
+				},
 			}
-			fc := fakeClient{
-				statuses:   tc.contexts,
-				presubmits: tc.presubmits,
-				jobs:       sets.String{},
+			for _, v := range tc.contexts {
+				fc.Statuses[fakeOrg+"/"+fakeRepo] = append(fc.Statuses[fakeOrg+"/"+fakeRepo], v)
 			}
 
-			if tc.jobs == nil {
-				tc.jobs = sets.String{}
+			agent := plugins.Agent{
+				SCMProviderClient: &fakeClient.Client,
+				Logger:            logrus.WithField("plugin", pluginName),
+				Config: &config.Config{
+					JobConfig: job.Config{
+						// Presubmits: tc.presubmits,
+					},
+				},
 			}
-
-			err := plugin.InvokeCommand(&event, func(match []string) error {
-				return handle(&fc, log, &event)
+			err := plugin.InvokeCommandHandler(&event, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+				return handler(match, agent, event)
 			})
 			switch {
 			case err != nil:
@@ -570,10 +439,10 @@ func TestHandle(t *testing.T) {
 				}
 			case tc.err:
 				t.Error("failed to receive an error")
-			case !reflect.DeepEqual(fc.statuses, tc.expected):
-				t.Errorf("bad statuses: actual %#v != expected %#v", fc.statuses, tc.expected)
-			case !reflect.DeepEqual(fc.jobs, tc.jobs):
-				t.Errorf("bad jobs: actual %#v != expected %#v", fc.jobs, tc.jobs)
+			case !reflect.DeepEqual(fc.Statuses[fakeOrg+"/"+fakeRepo], tc.expected):
+				t.Errorf("bad statuses: actual %#v != expected %#v", fc.Statuses[fakeOrg+"/"+fakeRepo], tc.expected)
+				// case !reflect.DeepEqual(fc.jobs, tc.jobs):
+				// 	t.Errorf("bad jobs: actual %#v != expected %#v", fc.jobs, tc.jobs)
 			}
 		})
 	}