feat(agentic): add issue report action
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
1cc318e2e8
commit
238be05fa8
4 changed files with 152 additions and 0 deletions
|
|
@ -122,6 +122,22 @@ type IssueCommentOutput struct {
|
|||
Comment IssueComment `json:"comment"`
|
||||
}
|
||||
|
||||
// input := agentic.IssueReportInput{Slug: "fix-auth", Report: map[string]any{"summary": "Build failed"}}
|
||||
type IssueReportInput struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
IssueID string `json:"issue_id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Report any `json:"report,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.IssueReportOutput{Success: true, Comment: agentic.IssueComment{Author: "codex"}}
|
||||
type IssueReportOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Comment IssueComment `json:"comment"`
|
||||
}
|
||||
|
||||
// out := agentic.IssueArchiveOutput{Success: true, Archived: "fix-auth"}
|
||||
type IssueArchiveOutput struct {
|
||||
Success bool `json:"success"`
|
||||
|
|
@ -229,6 +245,27 @@ func (s *PrepSubsystem) handleIssueRecordComment(ctx context.Context, options co
|
|||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("issue.report").Run(ctx, core.NewOptions(
|
||||
//
|
||||
// core.Option{Key: "slug", Value: "fix-auth"},
|
||||
// core.Option{Key: "report", Value: map[string]any{"summary": "Build failed"}},
|
||||
//
|
||||
// ))
|
||||
func (s *PrepSubsystem) handleIssueRecordReport(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueReport(ctx, nil, IssueReportInput{
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
IssueID: optionStringValue(options, "issue_id", "issue-id"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
Report: optionAnyValue(options, "report", "body"),
|
||||
Author: optionStringValue(options, "author"),
|
||||
Metadata: optionAnyMapValue(options, "metadata"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("issue.archive").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"}))
|
||||
func (s *PrepSubsystem) handleIssueRecordArchive(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueArchive(ctx, nil, IssueArchiveInput{
|
||||
|
|
@ -272,6 +309,11 @@ func (s *PrepSubsystem) registerIssueTools(server *mcp.Server) {
|
|||
Description: "Add a comment to a tracked platform issue.",
|
||||
}, s.issueComment)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "issue_report",
|
||||
Description: "Post a structured report comment to a tracked platform issue.",
|
||||
}, s.issueReport)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "issue_archive",
|
||||
Description: "Archive a tracked platform issue by slug.",
|
||||
|
|
@ -454,6 +496,38 @@ func (s *PrepSubsystem) issueComment(ctx context.Context, _ *mcp.CallToolRequest
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) issueReport(ctx context.Context, _ *mcp.CallToolRequest, input IssueReportInput) (*mcp.CallToolResult, IssueReportOutput, error) {
|
||||
identifier := issueRecordIdentifier(input.Slug, input.IssueID, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, IssueReportOutput{}, core.E("issueReport", "issue_id, id, or slug is required", nil)
|
||||
}
|
||||
|
||||
body, err := issueReportBody(input.Report)
|
||||
if err != nil {
|
||||
return nil, IssueReportOutput{}, err
|
||||
}
|
||||
if body == "" {
|
||||
return nil, IssueReportOutput{}, core.E("issueReport", "report is required", nil)
|
||||
}
|
||||
|
||||
_, commentOutput, err := s.issueComment(ctx, nil, IssueCommentInput{
|
||||
ID: input.ID,
|
||||
IssueID: input.IssueID,
|
||||
Slug: input.Slug,
|
||||
Body: body,
|
||||
Author: input.Author,
|
||||
Metadata: input.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, IssueReportOutput{}, err
|
||||
}
|
||||
|
||||
return nil, IssueReportOutput{
|
||||
Success: true,
|
||||
Comment: commentOutput.Comment,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) issueArchive(ctx context.Context, _ *mcp.CallToolRequest, input IssueArchiveInput) (*mcp.CallToolResult, IssueArchiveOutput, error) {
|
||||
identifier := issueRecordIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
|
|
@ -519,6 +593,25 @@ func parseIssueComment(values map[string]any) IssueComment {
|
|||
}
|
||||
}
|
||||
|
||||
func issueReportBody(report any) (string, error) {
|
||||
switch value := report.(type) {
|
||||
case nil:
|
||||
return "", nil
|
||||
case string:
|
||||
return core.Trim(value), nil
|
||||
}
|
||||
|
||||
if text := stringValue(report); text != "" {
|
||||
return text, nil
|
||||
}
|
||||
|
||||
if jsonText := core.JSONMarshalString(report); core.Trim(jsonText) != "" {
|
||||
return core.Concat("```json\n", jsonText, "\n```"), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func parseIssueListOutput(payload map[string]any) IssueListOutput {
|
||||
issuesData := payloadDataSlice(payload, "issues")
|
||||
issues := make([]Issue, 0, len(issuesData))
|
||||
|
|
|
|||
|
|
@ -129,6 +129,61 @@ func TestIssue_HandleIssueRecordAssign_Ugly_MissingIdentifier(t *testing.T) {
|
|||
assert.EqualError(t, result.Value.(error), "issueAssign: id or slug is required")
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordReport_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)
|
||||
assert.Equal(t, "QA failed: build output changed", payload["body"])
|
||||
assert.Equal(t, "codex", payload["author"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"comment":{"id":88,"issue_id":42,"author":"codex","body":"QA failed: build output changed","metadata":{"source":"qa"}}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleIssueRecordReport(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: "fix-auth"},
|
||||
core.Option{Key: "report", Value: "QA failed: build output changed"},
|
||||
core.Option{Key: "author", Value: "codex"},
|
||||
core.Option{Key: "metadata", Value: map[string]any{"source": "qa"}},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(IssueReportOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, 88, output.Comment.ID)
|
||||
assert.Equal(t, "QA failed: build output changed", output.Comment.Body)
|
||||
assert.Equal(t, "codex", output.Comment.Author)
|
||||
assert.Equal(t, map[string]any{"source": "qa"}, output.Comment.Metadata)
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordReport_Bad_MissingReport(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
|
||||
result := subsystem.handleIssueRecordReport(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: "fix-auth"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
assert.EqualError(t, result.Value.(error), "issueReport: report is required")
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordReport_Ugly_MissingIdentifier(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
|
||||
result := subsystem.handleIssueRecordReport(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "report", Value: "QA failed: build output changed"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
assert.EqualError(t, result.Value.(error), "issueReport: issue_id, id, or slug is required")
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordList_Ugly_NestedEnvelope(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/issues", r.URL.Path)
|
||||
|
|
|
|||
|
|
@ -223,8 +223,10 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
c.Action("issue.update", s.handleIssueRecordUpdate).Description = "Update a tracked platform issue by slug"
|
||||
c.Action("issue.assign", s.handleIssueRecordAssign).Description = "Assign an agent or user to a tracked platform issue"
|
||||
c.Action("issue.comment", s.handleIssueRecordComment).Description = "Add a comment to a tracked platform issue"
|
||||
c.Action("issue.report", s.handleIssueRecordReport).Description = "Post a structured report comment to a tracked platform issue"
|
||||
c.Action("issue.archive", s.handleIssueRecordArchive).Description = "Archive a tracked platform issue by slug"
|
||||
c.Action("agentic.issue.assign", s.handleIssueRecordAssign).Description = "Assign an agent or user to a tracked platform issue"
|
||||
c.Action("agentic.issue.report", s.handleIssueRecordReport).Description = "Post a structured report comment to a tracked platform issue"
|
||||
c.Action("sprint.create", s.handleSprintCreate).Description = "Create a tracked platform sprint"
|
||||
c.Action("sprint.get", s.handleSprintGet).Description = "Read a tracked platform sprint by slug"
|
||||
c.Action("sprint.list", s.handleSprintList).Description = "List tracked platform sprints with optional filters"
|
||||
|
|
|
|||
|
|
@ -514,10 +514,12 @@ func TestPrep_OnStartup_Good_RegistersSessionActions(t *testing.T) {
|
|||
assert.True(t, c.Action("issue.update").Exists())
|
||||
assert.True(t, c.Action("issue.assign").Exists())
|
||||
assert.True(t, c.Action("issue.comment").Exists())
|
||||
assert.True(t, c.Action("issue.report").Exists())
|
||||
assert.True(t, c.Action("issue.archive").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.update").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.assign").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.comment").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.report").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.archive").Exists())
|
||||
assert.True(t, c.Action("sprint.create").Exists())
|
||||
assert.True(t, c.Action("sprint.get").Exists())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue