diff --git a/pkg/agentic/commands_forge.go b/pkg/agentic/commands_forge.go index f3296a0..c6ca558 100644 --- a/pkg/agentic/commands_forge.go +++ b/pkg/agentic/commands_forge.go @@ -100,6 +100,8 @@ func (s *PrepSubsystem) registerForgeCommands() { c.Command("issue/list", core.Command{Description: "List Forge issues for a repo", Action: s.cmdIssueList}) c.Command("issue/comment", core.Command{Description: "Comment on a Forge issue", Action: s.cmdIssueComment}) c.Command("issue/create", core.Command{Description: "Create a Forge issue", Action: s.cmdIssueCreate}) + c.Command("issue/update", core.Command{Description: "Update a tracked platform issue", Action: s.cmdIssueUpdate}) + c.Command("issue/archive", core.Command{Description: "Archive a tracked platform issue", Action: s.cmdIssueArchive}) 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}) @@ -228,6 +230,72 @@ func (s *PrepSubsystem) cmdIssueCreate(options core.Options) core.Result { return core.Result{Value: issue.Index, OK: true} } +func (s *PrepSubsystem) cmdIssueUpdate(options core.Options) core.Result { + ctx := context.Background() + id := optionStringValue(options, "id", "slug", "_arg") + if id == "" { + core.Print(nil, "usage: core-agent issue update [--title=\"...\"] [--description=\"...\"] [--type=bug] [--status=open] [--priority=high] [--labels=a,b] [--sprint-id=7|--sprint-slug=phase-1]") + return core.Result{Value: core.E("agentic.cmdIssueUpdate", "slug or id is required", nil), OK: false} + } + + result := s.handleIssueRecordUpdate(ctx, core.NewOptions( + core.Option{Key: "slug", Value: id}, + core.Option{Key: "title", Value: options.String("title")}, + core.Option{Key: "description", Value: options.String("description")}, + core.Option{Key: "type", Value: options.String("type")}, + core.Option{Key: "status", Value: options.String("status")}, + core.Option{Key: "priority", Value: options.String("priority")}, + core.Option{Key: "labels", Value: options.String("labels")}, + core.Option{Key: "sprint_id", Value: options.String("sprint-id")}, + core.Option{Key: "sprint_slug", Value: options.String("sprint-slug")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdIssueUpdate", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(IssueOutput) + if !ok { + err := core.E("agentic.cmdIssueUpdate", "invalid issue update output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "%s", output.Issue.Slug) + core.Print(nil, " status: %s", output.Issue.Status) + core.Print(nil, " title: %s", output.Issue.Title) + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdIssueArchive(options core.Options) core.Result { + ctx := context.Background() + id := optionStringValue(options, "id", "slug", "_arg") + if id == "" { + core.Print(nil, "usage: core-agent issue archive ") + return core.Result{Value: core.E("agentic.cmdIssueArchive", "slug or id is required", nil), OK: false} + } + + result := s.handleIssueRecordArchive(ctx, core.NewOptions( + core.Option{Key: "slug", Value: id}, + )) + if !result.OK { + err := commandResultError("agentic.cmdIssueArchive", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(IssueArchiveOutput) + if !ok { + err := core.E("agentic.cmdIssueArchive", "invalid issue archive output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "archived: %s", output.Archived) + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) cmdPRGet(options core.Options) core.Result { ctx := context.Background() org, repo, num := parseForgeArgs(options) diff --git a/pkg/agentic/commands_forge_test.go b/pkg/agentic/commands_forge_test.go index 2480093..685fed0 100644 --- a/pkg/agentic/commands_forge_test.go +++ b/pkg/agentic/commands_forge_test.go @@ -10,6 +10,7 @@ import ( core "dappco.re/go/core" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // --- parseForgeArgs --- @@ -145,6 +146,82 @@ func TestCommandsforge_CmdIssueCreate_Ugly(t *testing.T) { assert.False(t, r.OK) } +func TestCommandsforge_CmdIssueUpdate_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/issues/fix-auth", r.URL.Path) + require.Equal(t, http.MethodPatch, r.Method) + + bodyResult := core.ReadAll(r.Body) + require.True(t, bodyResult.OK) + + var payload map[string]any + parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload) + require.True(t, parseResult.OK) + require.Equal(t, "Fix auth middleware", payload["title"]) + require.Equal(t, "in_progress", payload["status"]) + + _, _ = w.Write([]byte(`{"data":{"issue":{"slug":"fix-auth","title":"Fix auth middleware","status":"in_progress","priority":"high","labels":["auth","backend"]}}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdIssueUpdate(core.NewOptions( + core.Option{Key: "_arg", Value: "fix-auth"}, + core.Option{Key: "title", Value: "Fix auth middleware"}, + core.Option{Key: "status", Value: "in_progress"}, + core.Option{Key: "priority", Value: "high"}, + core.Option{Key: "labels", Value: "auth,backend"}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(IssueOutput) + require.True(t, ok) + assert.Equal(t, "fix-auth", output.Issue.Slug) + assert.Equal(t, "in_progress", output.Issue.Status) + assert.Equal(t, []string{"auth", "backend"}, output.Issue.Labels) +} + +func TestCommandsforge_CmdIssueUpdate_Bad_MissingSlug(t *testing.T) { + subsystem := testPrepWithPlatformServer(t, nil, "secret-token") + result := subsystem.cmdIssueUpdate(core.NewOptions()) + assert.False(t, result.OK) + assert.EqualError(t, result.Value.(error), "agentic.cmdIssueUpdate: slug or id is required") +} + +func TestCommandsforge_CmdIssueArchive_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/issues/fix-auth", r.URL.Path) + require.Equal(t, http.MethodDelete, r.Method) + + _, _ = w.Write([]byte(`{"data":{"result":{"slug":"fix-auth","success":true}}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdIssueArchive(core.NewOptions( + core.Option{Key: "slug", Value: "fix-auth"}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(IssueArchiveOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, "fix-auth", output.Archived) +} + +func TestCommandsforge_CmdIssueArchive_Ugly_ServerError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdIssueArchive(core.NewOptions( + core.Option{Key: "_arg", Value: "fix-auth"}, + )) + assert.False(t, result.OK) +} + func TestCommandsforge_CmdPRGet_Ugly(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) })) t.Cleanup(srv.Close) @@ -244,4 +321,6 @@ func TestCommandsforge_RegisterForgeCommands_Good_RepoSyncRegistered(t *testing. s, c := testPrepWithCore(t, nil) s.registerForgeCommands() assert.Contains(t, c.Commands(), "repo/sync") + assert.Contains(t, c.Commands(), "issue/update") + assert.Contains(t, c.Commands(), "issue/archive") }