feat(agentic): add forge PR close action

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 19:18:58 +00:00
parent 57ee930717
commit 24bb3b26c6
5 changed files with 92 additions and 0 deletions

View file

@ -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"},

View file

@ -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 <repo> --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)

View file

@ -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{

View file

@ -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"

View file

@ -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", "")