From 24bb3b26c6275b002f84f03cb4eb81940ade70da Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 19:18:58 +0000 Subject: [PATCH] feat(agentic): add forge PR close action Co-Authored-By: Virgil --- pkg/agentic/actions.go | 10 ++++++++ pkg/agentic/commands_forge.go | 22 ++++++++++++++++++ pkg/agentic/commands_test.go | 44 +++++++++++++++++++++++++++++++++++ pkg/agentic/prep.go | 1 + pkg/agentic/prep_test.go | 15 ++++++++++++ 5 files changed, 92 insertions(+) diff --git a/pkg/agentic/actions.go b/pkg/agentic/actions.go index 02ddb9c..51b7ca1 100644 --- a/pkg/agentic/actions.go +++ b/pkg/agentic/actions.go @@ -343,6 +343,16 @@ func (s *PrepSubsystem) handlePRMerge(ctx context.Context, options core.Options) return s.cmdPRMerge(normaliseForgeActionOptions(options)) } +// result := c.Action("agentic.pr.close").Run(ctx, core.NewOptions( +// +// core.Option{Key: "_arg", Value: "go-io"}, +// core.Option{Key: "number", Value: "12"}, +// +// )) +func (s *PrepSubsystem) handlePRClose(ctx context.Context, options core.Options) core.Result { + return s.cmdPRClose(normaliseForgeActionOptions(options)) +} + // result := c.Action("agentic.review-queue").Run(ctx, core.NewOptions( // // core.Option{Key: "workspace", Value: "core/go-io/task-5"}, diff --git a/pkg/agentic/commands_forge.go b/pkg/agentic/commands_forge.go index e2e55e5..67fdfe1 100644 --- a/pkg/agentic/commands_forge.go +++ b/pkg/agentic/commands_forge.go @@ -103,6 +103,7 @@ func (s *PrepSubsystem) registerForgeCommands() { c.Command("pr/get", core.Command{Description: "Get a Forge PR", Action: s.cmdPRGet}) c.Command("pr/list", core.Command{Description: "List Forge PRs for a repo", Action: s.cmdPRList}) c.Command("pr/merge", core.Command{Description: "Merge a Forge PR", Action: s.cmdPRMerge}) + c.Command("pr/close", core.Command{Description: "Close a Forge PR", Action: s.cmdPRClose}) c.Command("repo/get", core.Command{Description: "Get Forge repo info", Action: s.cmdRepoGet}) c.Command("repo/list", core.Command{Description: "List Forge repos for an org", Action: s.cmdRepoList}) } @@ -293,6 +294,27 @@ func (s *PrepSubsystem) cmdPRMerge(options core.Options) core.Result { return core.Result{OK: true} } +func (s *PrepSubsystem) cmdPRClose(options core.Options) core.Result { + ctx := context.Background() + org, repo, num := parseForgeArgs(options) + if repo == "" || num == 0 { + core.Print(nil, "usage: core-agent pr close --number=N [--org=core]") + return core.Result{Value: core.E("agentic.cmdPRClose", "repo and number are required", nil), OK: false} + } + + var pr pullRequestView + err := s.forge.Client().Patch(ctx, core.Sprintf("/api/v1/repos/%s/%s/pulls/%d", org, repo, num), &forge_types.EditPullRequestOption{ + State: "closed", + }, &pr) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "closed %s/%s#%d", org, repo, num) + return core.Result{OK: true} +} + func (s *PrepSubsystem) cmdRepoGet(options core.Options) core.Result { ctx := context.Background() org, repo, _ := parseForgeArgs(options) diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index c233693..9c7f160 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -339,6 +339,50 @@ func TestCommandsforge_CmdPRMerge_Good_CustomMethod(t *testing.T) { assert.True(t, r.OK) } +func TestCommandsforge_CmdPRClose_Bad_MissingArgs(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdPRClose(core.NewOptions()) + assert.False(t, r.OK) +} + +func TestCommandsforge_CmdPRClose_Good_Success(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method) + assert.Equal(t, "/api/v1/repos/core/go-io/pulls/5", r.URL.Path) + + bodyResult := core.ReadAll(r.Body) + assert.True(t, bodyResult.OK) + assert.Contains(t, bodyResult.Value.(string), `"state":"closed"`) + + w.Write([]byte(core.JSONMarshalString(map[string]any{ + "number": 5, + "state": "closed", + }))) + })) + t.Cleanup(srv.Close) + + s, _ := testPrepWithCore(t, srv) + r := s.cmdPRClose(core.NewOptions( + core.Option{Key: "_arg", Value: "go-io"}, + core.Option{Key: "number", Value: "5"}, + )) + assert.True(t, r.OK) +} + +func TestCommandsforge_CmdPRClose_Ugly_APIError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + t.Cleanup(srv.Close) + + s, _ := testPrepWithCore(t, srv) + r := s.cmdPRClose(core.NewOptions( + core.Option{Key: "_arg", Value: "go-io"}, + core.Option{Key: "number", Value: "5"}, + )) + assert.False(t, r.OK) +} + func TestCommandsforge_CmdIssueGet_Good_WithBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(core.JSONMarshalString(map[string]any{ diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 824ec5b..1a67827 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -174,6 +174,7 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { c.Action("agentic.pr.get", s.handlePRGet).Description = "Get a Forge PR by number" c.Action("agentic.pr.list", s.handlePRList).Description = "List Forge PRs for a repo" c.Action("agentic.pr.merge", s.handlePRMerge).Description = "Merge a Forge PR" + c.Action("agentic.pr.close", s.handlePRClose).Description = "Close a Forge PR" c.Action("agentic.review-queue", s.handleReviewQueue).Description = "Run CodeRabbit review on completed workspaces" diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 5cd187c..30dd11d 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -490,6 +490,21 @@ func TestPrep_OnStartup_Good_RegistersSessionActions(t *testing.T) { assert.True(t, c.Action("sprint.archive").Exists()) } +func TestPrep_OnStartup_Good_RegistersForgeActions(t *testing.T) { + t.Setenv("CORE_WORKSPACE", t.TempDir()) + t.Setenv("CORE_AGENT_DISPATCH", "") + + c := core.New(core.WithOption("name", "test")) + s := NewPrep() + s.ServiceRuntime = core.NewServiceRuntime(c, AgentOptions{}) + + require.True(t, s.OnStartup(context.Background()).OK) + assert.True(t, c.Action("agentic.pr.get").Exists()) + assert.True(t, c.Action("agentic.pr.list").Exists()) + assert.True(t, c.Action("agentic.pr.merge").Exists()) + assert.True(t, c.Action("agentic.pr.close").Exists()) +} + func TestPrep_OnStartup_Good_RegistersContentActions(t *testing.T) { t.Setenv("CORE_WORKSPACE", t.TempDir()) t.Setenv("CORE_AGENT_DISPATCH", "")