feat(agentic): widen RFC compatibility inputs
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b84e5692a2
commit
51f05bf789
9 changed files with 263 additions and 56 deletions
|
|
@ -50,7 +50,8 @@ type IssueCreateInput struct {
|
|||
|
||||
// input := agentic.IssueGetInput{Slug: "fix-auth"}
|
||||
type IssueGetInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.IssueListInput{Status: "open", Type: "bug"}
|
||||
|
|
@ -64,7 +65,8 @@ type IssueListInput struct {
|
|||
|
||||
// input := agentic.IssueUpdateInput{Slug: "fix-auth", Status: "in_progress"}
|
||||
type IssueUpdateInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
|
|
@ -77,7 +79,9 @@ type IssueUpdateInput struct {
|
|||
|
||||
// input := agentic.IssueCommentInput{Slug: "fix-auth", Body: "Ready for review"}
|
||||
type IssueCommentInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
IssueID string `json:"issue_id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Body string `json:"body"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
|
|
@ -85,7 +89,8 @@ type IssueCommentInput struct {
|
|||
|
||||
// input := agentic.IssueArchiveInput{Slug: "fix-auth"}
|
||||
type IssueArchiveInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.IssueOutput{Success: true, Issue: agentic.Issue{Slug: "fix-auth"}}
|
||||
|
|
@ -134,7 +139,8 @@ func (s *PrepSubsystem) handleIssueRecordCreate(ctx context.Context, options cor
|
|||
// result := c.Action("issue.get").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"}))
|
||||
func (s *PrepSubsystem) handleIssueRecordGet(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueGet(ctx, nil, IssueGetInput{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -160,7 +166,8 @@ func (s *PrepSubsystem) handleIssueRecordList(ctx context.Context, options core.
|
|||
// result := c.Action("issue.update").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"}))
|
||||
func (s *PrepSubsystem) handleIssueRecordUpdate(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueUpdate(ctx, nil, IssueUpdateInput{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
Title: optionStringValue(options, "title"),
|
||||
Description: optionStringValue(options, "description"),
|
||||
Type: optionStringValue(options, "type"),
|
||||
|
|
@ -179,7 +186,9 @@ func (s *PrepSubsystem) handleIssueRecordUpdate(ctx context.Context, options cor
|
|||
// result := c.Action("issue.comment").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"}))
|
||||
func (s *PrepSubsystem) handleIssueRecordComment(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueComment(ctx, nil, IssueCommentInput{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
IssueID: optionStringValue(options, "issue_id", "issue-id"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
Body: optionStringValue(options, "body"),
|
||||
Author: optionStringValue(options, "author"),
|
||||
Metadata: optionAnyMapValue(options, "metadata"),
|
||||
|
|
@ -193,7 +202,8 @@ func (s *PrepSubsystem) handleIssueRecordComment(ctx context.Context, options co
|
|||
// 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{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -275,11 +285,12 @@ func (s *PrepSubsystem) issueCreate(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) issueGet(ctx context.Context, _ *mcp.CallToolRequest, input IssueGetInput) (*mcp.CallToolResult, IssueOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, IssueOutput{}, core.E("issueGet", "slug is required", nil)
|
||||
identifier := issueRecordIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, IssueOutput{}, core.E("issueGet", "id or slug is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "issue.get", "GET", core.Concat("/v1/issues/", input.Slug), nil)
|
||||
result := s.platformPayload(ctx, "issue.get", "GET", core.Concat("/v1/issues/", identifier), nil)
|
||||
if !result.OK {
|
||||
return nil, IssueOutput{}, resultErrorValue("issue.get", result)
|
||||
}
|
||||
|
|
@ -311,8 +322,9 @@ func (s *PrepSubsystem) issueList(ctx context.Context, _ *mcp.CallToolRequest, i
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) issueUpdate(ctx context.Context, _ *mcp.CallToolRequest, input IssueUpdateInput) (*mcp.CallToolResult, IssueOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, IssueOutput{}, core.E("issueUpdate", "slug is required", nil)
|
||||
identifier := issueRecordIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, IssueOutput{}, core.E("issueUpdate", "id or slug is required", nil)
|
||||
}
|
||||
|
||||
body := map[string]any{}
|
||||
|
|
@ -344,7 +356,7 @@ func (s *PrepSubsystem) issueUpdate(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
return nil, IssueOutput{}, core.E("issueUpdate", "at least one field is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "issue.update", "PATCH", core.Concat("/v1/issues/", input.Slug), body)
|
||||
result := s.platformPayload(ctx, "issue.update", "PATCH", core.Concat("/v1/issues/", identifier), body)
|
||||
if !result.OK {
|
||||
return nil, IssueOutput{}, resultErrorValue("issue.update", result)
|
||||
}
|
||||
|
|
@ -356,8 +368,9 @@ func (s *PrepSubsystem) issueUpdate(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) issueComment(ctx context.Context, _ *mcp.CallToolRequest, input IssueCommentInput) (*mcp.CallToolResult, IssueCommentOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, IssueCommentOutput{}, core.E("issueComment", "slug is required", nil)
|
||||
identifier := issueRecordIdentifier(input.Slug, input.IssueID, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, IssueCommentOutput{}, core.E("issueComment", "issue_id, id, or slug is required", nil)
|
||||
}
|
||||
if input.Body == "" {
|
||||
return nil, IssueCommentOutput{}, core.E("issueComment", "body is required", nil)
|
||||
|
|
@ -373,7 +386,7 @@ func (s *PrepSubsystem) issueComment(ctx context.Context, _ *mcp.CallToolRequest
|
|||
body["metadata"] = input.Metadata
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "issue.comment", "POST", core.Concat("/v1/issues/", input.Slug, "/comments"), body)
|
||||
result := s.platformPayload(ctx, "issue.comment", "POST", core.Concat("/v1/issues/", identifier, "/comments"), body)
|
||||
if !result.OK {
|
||||
return nil, IssueCommentOutput{}, resultErrorValue("issue.comment", result)
|
||||
}
|
||||
|
|
@ -385,18 +398,19 @@ func (s *PrepSubsystem) issueComment(ctx context.Context, _ *mcp.CallToolRequest
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) issueArchive(ctx context.Context, _ *mcp.CallToolRequest, input IssueArchiveInput) (*mcp.CallToolResult, IssueArchiveOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, IssueArchiveOutput{}, core.E("issueArchive", "slug is required", nil)
|
||||
identifier := issueRecordIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, IssueArchiveOutput{}, core.E("issueArchive", "id or slug is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "issue.archive", "DELETE", core.Concat("/v1/issues/", input.Slug), nil)
|
||||
result := s.platformPayload(ctx, "issue.archive", "DELETE", core.Concat("/v1/issues/", identifier), nil)
|
||||
if !result.OK {
|
||||
return nil, IssueArchiveOutput{}, resultErrorValue("issue.archive", result)
|
||||
}
|
||||
|
||||
output := IssueArchiveOutput{
|
||||
Success: true,
|
||||
Archived: input.Slug,
|
||||
Archived: identifier,
|
||||
}
|
||||
if values := payloadResourceMap(result.Value.(map[string]any), "issue", "result"); len(values) > 0 {
|
||||
if slug := stringValue(values["slug"]); slug != "" {
|
||||
|
|
@ -427,6 +441,15 @@ func parseIssue(values map[string]any) Issue {
|
|||
}
|
||||
}
|
||||
|
||||
func issueRecordIdentifier(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := core.Trim(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseIssueComment(values map[string]any) IssueComment {
|
||||
return IssueComment{
|
||||
ID: intValue(values["id"]),
|
||||
|
|
|
|||
|
|
@ -53,7 +53,26 @@ func TestIssue_HandleIssueRecordGet_Bad(t *testing.T) {
|
|||
|
||||
result := subsystem.handleIssueRecordGet(context.Background(), core.NewOptions())
|
||||
assert.False(t, result.OK)
|
||||
assert.EqualError(t, result.Value.(error), "issueGet: slug is required")
|
||||
assert.EqualError(t, result.Value.(error), "issueGet: id or slug is required")
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordGet_Good_IDAlias(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/issues/42", r.URL.Path)
|
||||
_, _ = w.Write([]byte(`{"data":{"id":42,"slug":"fix-auth","title":"Fix auth","status":"open"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleIssueRecordGet(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "id", Value: "42"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(IssueOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 42, output.Issue.ID)
|
||||
assert.Equal(t, "fix-auth", output.Issue.Slug)
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordList_Ugly_NestedEnvelope(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ func (s *PrepSubsystem) registerPlatformTools(server *mcp.Server) {
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) syncPushTool(ctx context.Context, _ *mcp.CallToolRequest, input SyncPushInput) (*mcp.CallToolResult, SyncPushOutput, error) {
|
||||
output, err := s.syncPush(ctx, input.AgentID)
|
||||
output, err := s.syncPushInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, SyncPushOutput{}, err
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ func (s *PrepSubsystem) syncPushTool(ctx context.Context, _ *mcp.CallToolRequest
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) syncPullTool(ctx context.Context, _ *mcp.CallToolRequest, input SyncPullInput) (*mcp.CallToolResult, SyncPullOutput, error) {
|
||||
output, err := s.syncPull(ctx, input.AgentID)
|
||||
output, err := s.syncPullInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, SyncPullOutput{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ type SessionGetInput struct {
|
|||
|
||||
// input := agentic.SessionListInput{PlanSlug: "ax-follow-up", Status: "active"}
|
||||
type SessionListInput struct {
|
||||
PlanSlug string `json:"plan_slug,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
PlanSlug string `json:"plan_slug,omitempty"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.SessionContinueInput{SessionID: "ses_abc123", AgentType: "codex"}
|
||||
|
|
@ -177,9 +178,10 @@ func (s *PrepSubsystem) handleSessionGet(ctx context.Context, options core.Optio
|
|||
// result := c.Action("session.list").Run(ctx, core.NewOptions(core.Option{Key: "status", Value: "active"}))
|
||||
func (s *PrepSubsystem) handleSessionList(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.sessionList(ctx, nil, SessionListInput{
|
||||
PlanSlug: optionStringValue(options, "plan_slug", "plan"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
Limit: optionIntValue(options, "limit"),
|
||||
PlanSlug: optionStringValue(options, "plan_slug", "plan"),
|
||||
AgentType: optionStringValue(options, "agent_type", "agent"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
Limit: optionIntValue(options, "limit"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -394,6 +396,7 @@ func (s *PrepSubsystem) sessionGet(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
func (s *PrepSubsystem) sessionList(ctx context.Context, _ *mcp.CallToolRequest, input SessionListInput) (*mcp.CallToolResult, SessionListOutput, error) {
|
||||
path := "/v1/sessions"
|
||||
path = appendQueryParam(path, "plan_slug", input.PlanSlug)
|
||||
path = appendQueryParam(path, "agent_type", input.AgentType)
|
||||
path = appendQueryParam(path, "status", input.Status)
|
||||
if input.Limit > 0 {
|
||||
path = appendQueryParam(path, "limit", core.Sprint(input.Limit))
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ func TestSession_HandleSessionList_Good(t *testing.T) {
|
|||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/sessions", r.URL.Path)
|
||||
require.Equal(t, "ax-follow-up", r.URL.Query().Get("plan_slug"))
|
||||
require.Equal(t, "codex", r.URL.Query().Get("agent_type"))
|
||||
require.Equal(t, "active", r.URL.Query().Get("status"))
|
||||
require.Equal(t, "5", r.URL.Query().Get("limit"))
|
||||
_, _ = w.Write([]byte(`{"data":[{"session_id":"ses_1","agent_type":"codex","status":"active"},{"session_id":"ses_2","agent_type":"claude","status":"completed"}],"count":2}`))
|
||||
|
|
@ -118,6 +119,7 @@ func TestSession_HandleSessionList_Good(t *testing.T) {
|
|||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleSessionList(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "plan_slug", Value: "ax-follow-up"},
|
||||
core.Option{Key: "agent_type", Value: "codex"},
|
||||
core.Option{Key: "status", Value: "active"},
|
||||
core.Option{Key: "limit", Value: 5},
|
||||
))
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ type SprintCreateInput struct {
|
|||
|
||||
// input := agentic.SprintGetInput{Slug: "ax-follow-up"}
|
||||
type SprintGetInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.SprintListInput{Status: "active", Limit: 10}
|
||||
|
|
@ -47,7 +48,8 @@ type SprintListInput struct {
|
|||
|
||||
// input := agentic.SprintUpdateInput{Slug: "ax-follow-up", Status: "completed"}
|
||||
type SprintUpdateInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Goal string `json:"goal,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
|
@ -58,7 +60,8 @@ type SprintUpdateInput struct {
|
|||
|
||||
// input := agentic.SprintArchiveInput{Slug: "ax-follow-up"}
|
||||
type SprintArchiveInput struct {
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.SprintOutput{Success: true, Sprint: agentic.Sprint{Slug: "ax-follow-up"}}
|
||||
|
|
@ -99,7 +102,8 @@ func (s *PrepSubsystem) handleSprintCreate(ctx context.Context, options core.Opt
|
|||
// result := c.Action("sprint.get").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
|
||||
func (s *PrepSubsystem) handleSprintGet(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.sprintGet(ctx, nil, SprintGetInput{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -122,7 +126,8 @@ func (s *PrepSubsystem) handleSprintList(ctx context.Context, options core.Optio
|
|||
// result := c.Action("sprint.update").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
|
||||
func (s *PrepSubsystem) handleSprintUpdate(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.sprintUpdate(ctx, nil, SprintUpdateInput{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
Title: optionStringValue(options, "title"),
|
||||
Goal: optionStringValue(options, "goal"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
|
|
@ -139,7 +144,8 @@ func (s *PrepSubsystem) handleSprintUpdate(ctx context.Context, options core.Opt
|
|||
// result := c.Action("sprint.archive").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
|
||||
func (s *PrepSubsystem) handleSprintArchive(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.sprintArchive(ctx, nil, SprintArchiveInput{
|
||||
Slug: optionStringValue(options, "slug", "_arg"),
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -210,11 +216,12 @@ func (s *PrepSubsystem) sprintCreate(ctx context.Context, _ *mcp.CallToolRequest
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) sprintGet(ctx context.Context, _ *mcp.CallToolRequest, input SprintGetInput) (*mcp.CallToolResult, SprintOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, SprintOutput{}, core.E("sprintGet", "slug is required", nil)
|
||||
identifier := sprintIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, SprintOutput{}, core.E("sprintGet", "id or slug is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "sprint.get", "GET", core.Concat("/v1/sprints/", input.Slug), nil)
|
||||
result := s.platformPayload(ctx, "sprint.get", "GET", core.Concat("/v1/sprints/", identifier), nil)
|
||||
if !result.OK {
|
||||
return nil, SprintOutput{}, resultErrorValue("sprint.get", result)
|
||||
}
|
||||
|
|
@ -241,8 +248,9 @@ func (s *PrepSubsystem) sprintList(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) sprintUpdate(ctx context.Context, _ *mcp.CallToolRequest, input SprintUpdateInput) (*mcp.CallToolResult, SprintOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, SprintOutput{}, core.E("sprintUpdate", "slug is required", nil)
|
||||
identifier := sprintIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, SprintOutput{}, core.E("sprintUpdate", "id or slug is required", nil)
|
||||
}
|
||||
|
||||
body := map[string]any{}
|
||||
|
|
@ -268,7 +276,7 @@ func (s *PrepSubsystem) sprintUpdate(ctx context.Context, _ *mcp.CallToolRequest
|
|||
return nil, SprintOutput{}, core.E("sprintUpdate", "at least one field is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "sprint.update", "PATCH", core.Concat("/v1/sprints/", input.Slug), body)
|
||||
result := s.platformPayload(ctx, "sprint.update", "PATCH", core.Concat("/v1/sprints/", identifier), body)
|
||||
if !result.OK {
|
||||
return nil, SprintOutput{}, resultErrorValue("sprint.update", result)
|
||||
}
|
||||
|
|
@ -280,18 +288,19 @@ func (s *PrepSubsystem) sprintUpdate(ctx context.Context, _ *mcp.CallToolRequest
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) sprintArchive(ctx context.Context, _ *mcp.CallToolRequest, input SprintArchiveInput) (*mcp.CallToolResult, SprintArchiveOutput, error) {
|
||||
if input.Slug == "" {
|
||||
return nil, SprintArchiveOutput{}, core.E("sprintArchive", "slug is required", nil)
|
||||
identifier := sprintIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, SprintArchiveOutput{}, core.E("sprintArchive", "id or slug is required", nil)
|
||||
}
|
||||
|
||||
result := s.platformPayload(ctx, "sprint.archive", "DELETE", core.Concat("/v1/sprints/", input.Slug), nil)
|
||||
result := s.platformPayload(ctx, "sprint.archive", "DELETE", core.Concat("/v1/sprints/", identifier), nil)
|
||||
if !result.OK {
|
||||
return nil, SprintArchiveOutput{}, resultErrorValue("sprint.archive", result)
|
||||
}
|
||||
|
||||
output := SprintArchiveOutput{
|
||||
Success: true,
|
||||
Archived: input.Slug,
|
||||
Archived: identifier,
|
||||
}
|
||||
if values := payloadResourceMap(result.Value.(map[string]any), "sprint", "result"); len(values) > 0 {
|
||||
if slug := stringValue(values["slug"]); slug != "" {
|
||||
|
|
@ -304,6 +313,15 @@ func (s *PrepSubsystem) sprintArchive(ctx context.Context, _ *mcp.CallToolReques
|
|||
return nil, output, nil
|
||||
}
|
||||
|
||||
func sprintIdentifier(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := core.Trim(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseSprint(values map[string]any) Sprint {
|
||||
return Sprint{
|
||||
ID: intValue(values["id"]),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,26 @@ func TestSprint_HandleSprintGet_Bad(t *testing.T) {
|
|||
|
||||
result := subsystem.handleSprintGet(context.Background(), core.NewOptions())
|
||||
assert.False(t, result.OK)
|
||||
assert.EqualError(t, result.Value.(error), "sprintGet: slug is required")
|
||||
assert.EqualError(t, result.Value.(error), "sprintGet: id or slug is required")
|
||||
}
|
||||
|
||||
func TestSprint_HandleSprintGet_Good_IDAlias(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/sprints/7", r.URL.Path)
|
||||
_, _ = w.Write([]byte(`{"data":{"id":7,"slug":"ax-follow-up","title":"AX Follow-up","status":"active"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleSprintGet(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "id", Value: "7"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(SprintOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 7, output.Sprint.ID)
|
||||
assert.Equal(t, "ax-follow-up", output.Sprint.Slug)
|
||||
}
|
||||
|
||||
func TestSprint_HandleSprintList_Ugly_NestedEnvelope(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import (
|
|||
)
|
||||
|
||||
type SyncPushInput struct {
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Dispatches []map[string]any `json:"dispatches,omitempty"`
|
||||
}
|
||||
|
||||
type SyncPushOutput struct {
|
||||
|
|
@ -20,6 +21,7 @@ type SyncPushOutput struct {
|
|||
|
||||
type SyncPullInput struct {
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Since string `json:"since,omitempty"`
|
||||
}
|
||||
|
||||
type SyncPullOutput struct {
|
||||
|
|
@ -41,7 +43,10 @@ type syncStatusState struct {
|
|||
|
||||
// result := c.Action("agentic.sync.push").Run(ctx, core.NewOptions())
|
||||
func (s *PrepSubsystem) handleSyncPush(ctx context.Context, options core.Options) core.Result {
|
||||
output, err := s.syncPush(ctx, options.String("agent_id"))
|
||||
output, err := s.syncPushInput(ctx, SyncPushInput{
|
||||
AgentID: optionStringValue(options, "agent_id", "agent-id", "_arg"),
|
||||
Dispatches: optionAnyMapSliceValue(options, "dispatches"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
|
@ -50,7 +55,10 @@ func (s *PrepSubsystem) handleSyncPush(ctx context.Context, options core.Options
|
|||
|
||||
// result := c.Action("agentic.sync.pull").Run(ctx, core.NewOptions())
|
||||
func (s *PrepSubsystem) handleSyncPull(ctx context.Context, options core.Options) core.Result {
|
||||
output, err := s.syncPull(ctx, options.String("agent_id"))
|
||||
output, err := s.syncPullInput(ctx, SyncPullInput{
|
||||
AgentID: optionStringValue(options, "agent_id", "agent-id", "_arg"),
|
||||
Since: optionStringValue(options, "since"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
|
@ -58,15 +66,19 @@ func (s *PrepSubsystem) handleSyncPull(ctx context.Context, options core.Options
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) syncPush(ctx context.Context, agentID string) (SyncPushOutput, error) {
|
||||
return s.syncPushInput(ctx, SyncPushInput{AgentID: agentID})
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) syncPushInput(ctx context.Context, input SyncPushInput) (SyncPushOutput, error) {
|
||||
agentID := input.AgentID
|
||||
if agentID == "" {
|
||||
agentID = AgentName()
|
||||
}
|
||||
dispatches := collectSyncDispatches()
|
||||
token := s.syncToken()
|
||||
if token == "" {
|
||||
return SyncPushOutput{Success: true, Count: 0}, nil
|
||||
dispatches := input.Dispatches
|
||||
if len(dispatches) == 0 {
|
||||
dispatches = collectSyncDispatches()
|
||||
}
|
||||
|
||||
token := s.syncToken()
|
||||
queuedPushes := readSyncQueue()
|
||||
if len(dispatches) > 0 {
|
||||
queuedPushes = append(queuedPushes, syncQueuedPush{
|
||||
|
|
@ -75,6 +87,12 @@ func (s *PrepSubsystem) syncPush(ctx context.Context, agentID string) (SyncPushO
|
|||
QueuedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
if token == "" {
|
||||
if len(input.Dispatches) > 0 {
|
||||
writeSyncQueue(queuedPushes)
|
||||
}
|
||||
return SyncPushOutput{Success: true, Count: 0}, nil
|
||||
}
|
||||
if len(queuedPushes) == 0 {
|
||||
return SyncPushOutput{Success: true, Count: 0}, nil
|
||||
}
|
||||
|
|
@ -97,6 +115,11 @@ func (s *PrepSubsystem) syncPush(ctx context.Context, agentID string) (SyncPushO
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) syncPull(ctx context.Context, agentID string) (SyncPullOutput, error) {
|
||||
return s.syncPullInput(ctx, SyncPullInput{AgentID: agentID})
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) syncPullInput(ctx context.Context, input SyncPullInput) (SyncPullOutput, error) {
|
||||
agentID := input.AgentID
|
||||
if agentID == "" {
|
||||
agentID = AgentName()
|
||||
}
|
||||
|
|
@ -106,7 +129,9 @@ func (s *PrepSubsystem) syncPull(ctx context.Context, agentID string) (SyncPullO
|
|||
return SyncPullOutput{Success: true, Count: len(cached), Context: cached}, nil
|
||||
}
|
||||
|
||||
endpoint := core.Concat(s.syncAPIURL(), "/v1/agent/context?agent_id=", agentID)
|
||||
path := appendQueryParam("/v1/agent/context", "agent_id", agentID)
|
||||
path = appendQueryParam(path, "since", input.Since)
|
||||
endpoint := core.Concat(s.syncAPIURL(), path)
|
||||
result := HTTPGet(ctx, endpoint, token, "Bearer")
|
||||
if !result.OK {
|
||||
cached := readSyncContext()
|
||||
|
|
|
|||
|
|
@ -62,6 +62,51 @@ func TestSync_HandleSyncPush_Good(t *testing.T) {
|
|||
assert.False(t, readSyncStatusState().LastPushAt.IsZero())
|
||||
}
|
||||
|
||||
func TestSync_HandleSyncPush_Good_UsesProvidedDispatches(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
t.Setenv("CORE_AGENT_API_KEY", "secret-token")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/agent/sync", r.URL.Path)
|
||||
|
||||
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, "charon", payload["agent_id"])
|
||||
|
||||
dispatches, ok := payload["dispatches"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, dispatches, 1)
|
||||
|
||||
record, ok := dispatches[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "external-1", record["workspace"])
|
||||
require.Equal(t, "completed", record["status"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"synced":1}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := &PrepSubsystem{
|
||||
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
||||
brainURL: server.URL,
|
||||
}
|
||||
output, err := subsystem.syncPushInput(context.Background(), SyncPushInput{
|
||||
AgentID: "charon",
|
||||
Dispatches: []map[string]any{
|
||||
{"workspace": "external-1", "status": "completed"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, 1, output.Count)
|
||||
assert.Empty(t, readSyncQueue())
|
||||
}
|
||||
|
||||
func TestSync_HandleSyncPush_Bad(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
|
|
@ -90,6 +135,31 @@ func TestSync_HandleSyncPush_Bad(t *testing.T) {
|
|||
assert.Empty(t, readSyncQueue())
|
||||
}
|
||||
|
||||
func TestSync_HandleSyncPush_Bad_QueuesProvidedDispatchesWhenOffline(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
t.Setenv("CORE_AGENT_API_KEY", "")
|
||||
|
||||
subsystem := &PrepSubsystem{
|
||||
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
||||
}
|
||||
output, err := subsystem.syncPushInput(context.Background(), SyncPushInput{
|
||||
AgentID: "charon",
|
||||
Dispatches: []map[string]any{
|
||||
{"workspace": "external-1", "status": "completed"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, 0, output.Count)
|
||||
|
||||
queued := readSyncQueue()
|
||||
require.Len(t, queued, 1)
|
||||
assert.Equal(t, "charon", queued[0].AgentID)
|
||||
require.Len(t, queued[0].Dispatches, 1)
|
||||
assert.Equal(t, "external-1", queued[0].Dispatches[0]["workspace"])
|
||||
}
|
||||
|
||||
func TestSync_HandleSyncPush_Ugly(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
|
|
@ -159,6 +229,34 @@ func TestSync_HandleSyncPull_Good(t *testing.T) {
|
|||
assert.False(t, readSyncStatusState().LastPullAt.IsZero())
|
||||
}
|
||||
|
||||
func TestSync_HandleSyncPull_Good_SinceQuery(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
t.Setenv("CORE_AGENT_API_KEY", "secret-token")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/agent/context", r.URL.Path)
|
||||
require.Equal(t, "codex", r.URL.Query().Get("agent_id"))
|
||||
require.Equal(t, "2026-03-30T00:00:00Z", r.URL.Query().Get("since"))
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":"mem-2","content":"Recent pattern"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := &PrepSubsystem{
|
||||
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
|
||||
brainURL: server.URL,
|
||||
}
|
||||
output, err := subsystem.syncPullInput(context.Background(), SyncPullInput{
|
||||
AgentID: "codex",
|
||||
Since: "2026-03-30T00:00:00Z",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, 1, output.Count)
|
||||
require.Len(t, output.Context, 1)
|
||||
assert.Equal(t, "mem-2", output.Context[0]["id"])
|
||||
}
|
||||
|
||||
func TestSync_HandleSyncPush_Good_ReportMetadata(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", root)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue