feat(agentic): add sprint CLI commands
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
131607215f
commit
2bb4279123
4 changed files with 335 additions and 0 deletions
|
|
@ -80,6 +80,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) {
|
|||
s.registerCommitCommands()
|
||||
s.registerSessionCommands()
|
||||
s.registerTaskCommands()
|
||||
s.registerSprintCommands()
|
||||
s.registerStateCommands()
|
||||
s.registerLanguageCommands()
|
||||
s.registerSetupCommands()
|
||||
|
|
|
|||
220
pkg/agentic/commands_sprint.go
Normal file
220
pkg/agentic/commands_sprint.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
func (s *PrepSubsystem) registerSprintCommands() {
|
||||
c := s.Core()
|
||||
c.Command("sprint", core.Command{Description: "Manage tracked platform sprints", Action: s.cmdSprint})
|
||||
c.Command("agentic:sprint", core.Command{Description: "Manage tracked platform sprints", Action: s.cmdSprint})
|
||||
c.Command("sprint/create", core.Command{Description: "Create a tracked platform sprint", Action: s.cmdSprintCreate})
|
||||
c.Command("agentic:sprint/create", core.Command{Description: "Create a tracked platform sprint", Action: s.cmdSprintCreate})
|
||||
c.Command("sprint/get", core.Command{Description: "Read a tracked platform sprint by slug or ID", Action: s.cmdSprintGet})
|
||||
c.Command("agentic:sprint/get", core.Command{Description: "Read a tracked platform sprint by slug or ID", Action: s.cmdSprintGet})
|
||||
c.Command("sprint/list", core.Command{Description: "List tracked platform sprints", Action: s.cmdSprintList})
|
||||
c.Command("agentic:sprint/list", core.Command{Description: "List tracked platform sprints", Action: s.cmdSprintList})
|
||||
c.Command("sprint/update", core.Command{Description: "Update a tracked platform sprint", Action: s.cmdSprintUpdate})
|
||||
c.Command("agentic:sprint/update", core.Command{Description: "Update a tracked platform sprint", Action: s.cmdSprintUpdate})
|
||||
c.Command("sprint/archive", core.Command{Description: "Archive a tracked platform sprint", Action: s.cmdSprintArchive})
|
||||
c.Command("agentic:sprint/archive", core.Command{Description: "Archive a tracked platform sprint", Action: s.cmdSprintArchive})
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdSprint(options core.Options) core.Result {
|
||||
action := optionStringValue(options, "action")
|
||||
switch action {
|
||||
case "create":
|
||||
return s.cmdSprintCreate(options)
|
||||
case "get", "show":
|
||||
return s.cmdSprintGet(options)
|
||||
case "list":
|
||||
return s.cmdSprintList(options)
|
||||
case "update":
|
||||
return s.cmdSprintUpdate(options)
|
||||
case "archive", "delete":
|
||||
return s.cmdSprintArchive(options)
|
||||
case "":
|
||||
core.Print(nil, "usage: core-agent sprint create --title=\"AX Follow-up\" [--goal=\"Finish RFC parity\"] [--status=active]")
|
||||
core.Print(nil, " core-agent sprint get <slug-or-id>")
|
||||
core.Print(nil, " core-agent sprint list [--status=active] [--limit=10]")
|
||||
core.Print(nil, " core-agent sprint update <slug-or-id> [--title=\"...\"] [--goal=\"...\"] [--status=completed]")
|
||||
core.Print(nil, " core-agent sprint archive <slug-or-id>")
|
||||
return core.Result{OK: true}
|
||||
default:
|
||||
core.Print(nil, "usage: core-agent sprint create --title=\"AX Follow-up\" [--goal=\"Finish RFC parity\"] [--status=active]")
|
||||
core.Print(nil, " core-agent sprint get <slug-or-id>")
|
||||
core.Print(nil, " core-agent sprint list [--status=active] [--limit=10]")
|
||||
core.Print(nil, " core-agent sprint update <slug-or-id> [--title=\"...\"] [--goal=\"...\"] [--status=completed]")
|
||||
core.Print(nil, " core-agent sprint archive <slug-or-id>")
|
||||
return core.Result{Value: core.E("agentic.cmdSprint", core.Concat("unknown sprint command: ", action), nil), OK: false}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdSprintCreate(options core.Options) core.Result {
|
||||
title := optionStringValue(options, "title")
|
||||
if title == "" {
|
||||
core.Print(nil, "usage: core-agent sprint create --title=\"AX Follow-up\" [--goal=\"Finish RFC parity\"] [--status=active]")
|
||||
return core.Result{Value: core.E("agentic.cmdSprintCreate", "title is required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleSprintCreate(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "title", Value: title},
|
||||
core.Option{Key: "goal", Value: optionStringValue(options, "goal")},
|
||||
core.Option{Key: "status", Value: optionStringValue(options, "status")},
|
||||
core.Option{Key: "metadata", Value: optionAnyMapValue(options, "metadata")},
|
||||
core.Option{Key: "started_at", Value: optionStringValue(options, "started_at", "started-at")},
|
||||
core.Option{Key: "ended_at", Value: optionStringValue(options, "ended_at", "ended-at")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdSprintCreate", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(SprintOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdSprintCreate", "invalid sprint create output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
core.Print(nil, "slug: %s", output.Sprint.Slug)
|
||||
core.Print(nil, "title: %s", output.Sprint.Title)
|
||||
core.Print(nil, "status: %s", output.Sprint.Status)
|
||||
if output.Sprint.Goal != "" {
|
||||
core.Print(nil, "goal: %s", output.Sprint.Goal)
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdSprintGet(options core.Options) core.Result {
|
||||
identifier := optionStringValue(options, "slug", "id", "_arg")
|
||||
if identifier == "" {
|
||||
core.Print(nil, "usage: core-agent sprint get <slug-or-id>")
|
||||
return core.Result{Value: core.E("agentic.cmdSprintGet", "id or slug is required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleSprintGet(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: optionStringValue(options, "slug")},
|
||||
core.Option{Key: "id", Value: optionStringValue(options, "id", "_arg")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdSprintGet", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(SprintOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdSprintGet", "invalid sprint get output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
core.Print(nil, "slug: %s", output.Sprint.Slug)
|
||||
core.Print(nil, "title: %s", output.Sprint.Title)
|
||||
core.Print(nil, "status: %s", output.Sprint.Status)
|
||||
if output.Sprint.Goal != "" {
|
||||
core.Print(nil, "goal: %s", output.Sprint.Goal)
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdSprintList(options core.Options) core.Result {
|
||||
result := s.handleSprintList(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "status", Value: optionStringValue(options, "status")},
|
||||
core.Option{Key: "limit", Value: optionIntValue(options, "limit")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdSprintList", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(SprintListOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdSprintList", "invalid sprint list output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
if output.Count == 0 {
|
||||
core.Print(nil, "no sprints")
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
for _, sprint := range output.Sprints {
|
||||
core.Print(nil, " %-10s %-24s %s", sprint.Status, sprint.Slug, sprint.Title)
|
||||
}
|
||||
core.Print(nil, "%d sprint(s)", output.Count)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdSprintUpdate(options core.Options) core.Result {
|
||||
identifier := optionStringValue(options, "slug", "id", "_arg")
|
||||
if identifier == "" {
|
||||
core.Print(nil, "usage: core-agent sprint update <slug-or-id> [--title=\"...\"] [--goal=\"...\"] [--status=completed]")
|
||||
return core.Result{Value: core.E("agentic.cmdSprintUpdate", "id or slug is required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleSprintUpdate(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: optionStringValue(options, "slug")},
|
||||
core.Option{Key: "id", Value: optionStringValue(options, "id", "_arg")},
|
||||
core.Option{Key: "title", Value: optionStringValue(options, "title")},
|
||||
core.Option{Key: "goal", Value: optionStringValue(options, "goal")},
|
||||
core.Option{Key: "status", Value: optionStringValue(options, "status")},
|
||||
core.Option{Key: "metadata", Value: optionAnyMapValue(options, "metadata")},
|
||||
core.Option{Key: "started_at", Value: optionStringValue(options, "started_at", "started-at")},
|
||||
core.Option{Key: "ended_at", Value: optionStringValue(options, "ended_at", "ended-at")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdSprintUpdate", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(SprintOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdSprintUpdate", "invalid sprint update output", nil)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
core.Print(nil, "slug: %s", output.Sprint.Slug)
|
||||
core.Print(nil, "title: %s", output.Sprint.Title)
|
||||
core.Print(nil, "status: %s", output.Sprint.Status)
|
||||
if output.Sprint.Goal != "" {
|
||||
core.Print(nil, "goal: %s", output.Sprint.Goal)
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdSprintArchive(options core.Options) core.Result {
|
||||
identifier := optionStringValue(options, "slug", "id", "_arg")
|
||||
if identifier == "" {
|
||||
core.Print(nil, "usage: core-agent sprint archive <slug-or-id>")
|
||||
return core.Result{Value: core.E("agentic.cmdSprintArchive", "id or slug is required", nil), OK: false}
|
||||
}
|
||||
|
||||
result := s.handleSprintArchive(s.commandContext(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: optionStringValue(options, "slug")},
|
||||
core.Option{Key: "id", Value: optionStringValue(options, "id", "_arg")},
|
||||
))
|
||||
if !result.OK {
|
||||
err := commandResultError("agentic.cmdSprintArchive", result)
|
||||
core.Print(nil, "error: %v", err)
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
||||
output, ok := result.Value.(SprintArchiveOutput)
|
||||
if !ok {
|
||||
err := core.E("agentic.cmdSprintArchive", "invalid sprint 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}
|
||||
}
|
||||
112
pkg/agentic/commands_sprint_test.go
Normal file
112
pkg/agentic/commands_sprint_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package agentic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCommandsSprint_RegisterCommands_Good(t *testing.T) {
|
||||
c := core.New(core.WithOption("name", "test"))
|
||||
s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{})}
|
||||
|
||||
s.registerSprintCommands()
|
||||
|
||||
assert.Contains(t, c.Commands(), "sprint")
|
||||
assert.Contains(t, c.Commands(), "agentic:sprint")
|
||||
assert.Contains(t, c.Commands(), "sprint/create")
|
||||
assert.Contains(t, c.Commands(), "agentic:sprint/create")
|
||||
assert.Contains(t, c.Commands(), "sprint/get")
|
||||
assert.Contains(t, c.Commands(), "agentic:sprint/get")
|
||||
assert.Contains(t, c.Commands(), "sprint/list")
|
||||
assert.Contains(t, c.Commands(), "agentic:sprint/list")
|
||||
assert.Contains(t, c.Commands(), "sprint/update")
|
||||
assert.Contains(t, c.Commands(), "agentic:sprint/update")
|
||||
assert.Contains(t, c.Commands(), "sprint/archive")
|
||||
assert.Contains(t, c.Commands(), "agentic:sprint/archive")
|
||||
}
|
||||
|
||||
func TestCommandsSprint_CmdSprintCreate_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/sprints", 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
|
||||
require.True(t, core.JSONUnmarshalString(bodyResult.Value.(string), &payload).OK)
|
||||
require.Equal(t, "AX Follow-up", payload["title"])
|
||||
require.Equal(t, "Finish RFC parity", payload["goal"])
|
||||
require.Equal(t, "active", payload["status"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"sprint":{"id":7,"slug":"ax-follow-up","title":"AX Follow-up","goal":"Finish RFC parity","status":"active"}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
output := captureStdout(t, func() {
|
||||
result := subsystem.cmdSprintCreate(core.NewOptions(
|
||||
core.Option{Key: "title", Value: "AX Follow-up"},
|
||||
core.Option{Key: "goal", Value: "Finish RFC parity"},
|
||||
core.Option{Key: "status", Value: "active"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "slug: ax-follow-up")
|
||||
assert.Contains(t, output, "title: AX Follow-up")
|
||||
assert.Contains(t, output, "status: active")
|
||||
assert.Contains(t, output, "goal: Finish RFC parity")
|
||||
}
|
||||
|
||||
func TestCommandsSprint_CmdSprintList_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/sprints", r.URL.Path)
|
||||
require.Equal(t, "active", r.URL.Query().Get("status"))
|
||||
require.Equal(t, "5", r.URL.Query().Get("limit"))
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":1,"slug":"ax-follow-up","title":"AX Follow-up","status":"active"},{"id":2,"slug":"rfc-parity","title":"RFC Parity","status":"active"}],"count":2}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
output := captureStdout(t, func() {
|
||||
result := subsystem.cmdSprintList(core.NewOptions(
|
||||
core.Option{Key: "status", Value: "active"},
|
||||
core.Option{Key: "limit", Value: 5},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "ax-follow-up")
|
||||
assert.Contains(t, output, "rfc-parity")
|
||||
assert.Contains(t, output, "2 sprint(s)")
|
||||
}
|
||||
|
||||
func TestCommandsSprint_CmdSprintArchive_Bad_MissingIdentifier(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
|
||||
result := subsystem.cmdSprintArchive(core.NewOptions())
|
||||
|
||||
assert.False(t, result.OK)
|
||||
require.Error(t, result.Value.(error))
|
||||
assert.Contains(t, result.Value.(error).Error(), "id or slug is required")
|
||||
}
|
||||
|
||||
func TestCommandsSprint_CmdSprintGet_Ugly_InvalidResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"data":`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.cmdSprintGet(core.NewOptions(core.Option{Key: "_arg", Value: "ax-follow-up"}))
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
|
@ -1523,6 +1523,8 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) {
|
|||
assert.Contains(t, cmds, "task")
|
||||
assert.Contains(t, cmds, "task/update")
|
||||
assert.Contains(t, cmds, "task/toggle")
|
||||
assert.Contains(t, cmds, "sprint")
|
||||
assert.Contains(t, cmds, "sprint/create")
|
||||
}
|
||||
|
||||
func TestCommands_CmdPRManage_Good_NoCandidates(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue