diff --git a/pkg/agentic/commands_forge.go b/pkg/agentic/commands_forge.go index 0ac9bce..3e8c93e 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/assign", core.Command{Description: "Assign a Forge issue", Action: s.cmdIssueAssign}) + c.Command("issue/report", core.Command{Description: "Post a structured report to a Forge issue", Action: s.cmdIssueReport}) 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}) @@ -268,6 +270,69 @@ func (s *PrepSubsystem) cmdIssueUpdate(options core.Options) core.Result { return core.Result{Value: output, OK: true} } +func (s *PrepSubsystem) cmdIssueAssign(options core.Options) core.Result { + ctx := context.Background() + id := optionStringValue(options, "id", "slug", "_arg") + if id == "" || optionStringValue(options, "assignee", "agent", "agent_type") == "" { + core.Print(nil, "usage: core-agent issue assign --assignee=codex [--org=core]") + return core.Result{Value: core.E("agentic.cmdIssueAssign", "slug or id and assignee are required", nil), OK: false} + } + + result := s.handleIssueRecordAssign(ctx, core.NewOptions( + core.Option{Key: "slug", Value: id}, + core.Option{Key: "assignee", Value: optionStringValue(options, "assignee", "agent", "agent_type")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdIssueAssign", 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.cmdIssueAssign", "invalid issue assign 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, " assignee: %s", output.Issue.Assignee) + core.Print(nil, " status: %s", output.Issue.Status) + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdIssueReport(options core.Options) core.Result { + ctx := context.Background() + id := optionStringValue(options, "id", "slug", "_arg") + if id == "" { + core.Print(nil, "usage: core-agent issue report --report=\"...\" [--org=core]") + return core.Result{Value: core.E("agentic.cmdIssueReport", "slug or id is required", nil), OK: false} + } + + result := s.handleIssueRecordReport(ctx, core.NewOptions( + core.Option{Key: "slug", Value: id}, + core.Option{Key: "report", Value: optionAnyValue(options, "report", "body")}, + core.Option{Key: "author", Value: options.String("author")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdIssueReport", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(IssueReportOutput) + if !ok { + err := core.E("agentic.cmdIssueReport", "invalid issue report output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "comment: %d", output.Comment.ID) + core.Print(nil, " author: %s", output.Comment.Author) + core.Print(nil, " body: %s", output.Comment.Body) + 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") diff --git a/pkg/agentic/commands_forge_test.go b/pkg/agentic/commands_forge_test.go index 685fed0..b025ada 100644 --- a/pkg/agentic/commands_forge_test.go +++ b/pkg/agentic/commands_forge_test.go @@ -188,6 +188,86 @@ func TestCommandsforge_CmdIssueUpdate_Bad_MissingSlug(t *testing.T) { assert.EqualError(t, result.Value.(error), "agentic.cmdIssueUpdate: slug or id is required") } +func TestCommandsforge_CmdIssueAssign_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, "codex", payload["assignee"]) + + _, _ = w.Write([]byte(`{"data":{"issue":{"slug":"fix-auth","title":"Fix auth middleware","status":"open","assignee":"codex"}}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdIssueAssign(core.NewOptions( + core.Option{Key: "slug", Value: "fix-auth"}, + core.Option{Key: "assignee", Value: "codex"}, + )) + 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, "codex", output.Issue.Assignee) +} + +func TestCommandsforge_CmdIssueAssign_Bad_MissingAssignee(t *testing.T) { + subsystem := testPrepWithPlatformServer(t, nil, "secret-token") + result := subsystem.cmdIssueAssign(core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"})) + assert.False(t, result.OK) + assert.EqualError(t, result.Value.(error), "agentic.cmdIssueAssign: slug or id and assignee are required") +} + +func TestCommandsforge_CmdIssueReport_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/issues/fix-auth/comments", r.URL.Path) + require.Equal(t, http.MethodPost, 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) + + reportBody := core.JSONMarshalString(map[string]any{ + "summary": "Build failed", + }) + require.Equal(t, "codex", payload["author"]) + require.Equal(t, core.Concat("```json\n", reportBody, "\n```"), payload["body"]) + + _, _ = w.Write([]byte("{\"data\":{\"comment\":{\"id\":7,\"author\":\"codex\",\"body\":\"report received\"}}}")) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdIssueReport(core.NewOptions( + core.Option{Key: "slug", Value: "fix-auth"}, + core.Option{Key: "report", Value: map[string]any{"summary": "Build failed"}}, + core.Option{Key: "author", Value: "codex"}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(IssueReportOutput) + require.True(t, ok) + assert.Equal(t, 7, output.Comment.ID) + assert.Equal(t, "codex", output.Comment.Author) +} + +func TestCommandsforge_CmdIssueReport_Bad_MissingSlug(t *testing.T) { + subsystem := testPrepWithPlatformServer(t, nil, "secret-token") + result := subsystem.cmdIssueReport(core.NewOptions()) + assert.False(t, result.OK) + assert.EqualError(t, result.Value.(error), "agentic.cmdIssueReport: 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) @@ -321,6 +401,8 @@ 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/assign") + assert.Contains(t, c.Commands(), "issue/report") assert.Contains(t, c.Commands(), "issue/update") assert.Contains(t, c.Commands(), "issue/archive") }