Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/commands/assign.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,10 @@ func doAssignStep(cmd *cobra.Command, app *appctx.App, stepIDStr, assigneeID str
}
assigneeIDs = append(assigneeIDs, assigneeIDInt)

// The API rejects step updates without a title, so carry over the
// current one.
updated, err := app.Account().CardSteps().Update(cmd.Context(), stepID, &basecamp.UpdateStepRequest{
Title: step.Title,
AssigneeIDs: assigneeIDs,
})
Comment thread
nnemirovsky marked this conversation as resolved.
if err != nil {
Expand Down Expand Up @@ -746,7 +749,10 @@ func doUnassignStep(cmd *cobra.Command, app *appctx.App, stepIDStr string, assig

assigneeIDs := removeID(existingAssigneeIDs(step.Assignees), assigneeIDInt)

// The API rejects step updates without a title, so carry over the
// current one.
updated, err := app.Account().CardSteps().Update(cmd.Context(), stepID, &basecamp.UpdateStepRequest{
Title: step.Title,
AssigneeIDs: assigneeIDs,
})
Comment thread
nnemirovsky marked this conversation as resolved.
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions internal/commands/cards.go
Original file line number Diff line number Diff line change
Expand Up @@ -2009,6 +2009,14 @@ You can pass either a step ID or a Basecamp URL:
req := &basecamp.UpdateStepRequest{}
if title != "" {
req.Title = title
} else {
// The API rejects step updates without a title, so carry
// over the current one when only other fields change.
current, err := app.Account().CardSteps().Get(cmd.Context(), stepID)
if err != nil {
return convertSDKError(err)
}
req.Title = current.Title
Comment thread
nnemirovsky marked this conversation as resolved.
}
Comment thread
nnemirovsky marked this conversation as resolved.
Comment thread
nnemirovsky marked this conversation as resolved.
Comment thread
nnemirovsky marked this conversation as resolved.
Comment thread
nnemirovsky marked this conversation as resolved.
if dueOn != "" {
req.DueOn = dateparse.Parse(dueOn)
Expand Down
69 changes: 69 additions & 0 deletions internal/commands/cards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,75 @@ func TestCardsStepUpdateRequiresFields(t *testing.T) {
assert.NoError(t, err, "expected help output, not error")
}

// mockStepUpdateTransport serves the current step on GET and captures the
// update body on PUT.
type mockStepUpdateTransport struct {
getCount int
capturedPut []byte
}

func (t *mockStepUpdateTransport) RoundTrip(req *http.Request) (*http.Response, error) {
Comment thread
nnemirovsky marked this conversation as resolved.
Comment thread
nnemirovsky marked this conversation as resolved.
header := make(http.Header)
header.Set("Content-Type", "application/json")

stepJSON := `{"id": 456, "title": "Current title", "completed": false, "assignees": []}`

switch req.Method {
case "GET":
t.getCount++
case "PUT":
if req.Body != nil {
body, _ := io.ReadAll(req.Body)
t.capturedPut = body
req.Body.Close()
}
Comment thread
nnemirovsky marked this conversation as resolved.
Outdated
default:
return nil, errors.New("unexpected request")
}

return &http.Response{
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(stepJSON)),
Header: header,
}, nil
Comment thread
nnemirovsky marked this conversation as resolved.
Outdated
}

// TestCardsStepUpdateAssigneesOnlyCarriesTitle verifies that updating only
// assignees fetches the current step and includes its title in the request —
// the API rejects step updates without a title.
func TestCardsStepUpdateAssigneesOnlyCarriesTitle(t *testing.T) {
transport := &mockStepUpdateTransport{}
app := setupCardsMockApp(t, transport)

cmd := newCardsStepUpdateCmd()
err := executeCommand(cmd, app, "456", "--assignees", "789")
require.NoError(t, err)

assert.Equal(t, 1, transport.getCount)

var body map[string]any
require.NoError(t, json.Unmarshal(transport.capturedPut, &body))
assert.Equal(t, "Current title", body["title"])
assert.Equal(t, []any{float64(789)}, body["assignee_ids"])
Comment thread
nnemirovsky marked this conversation as resolved.
}

// TestCardsStepUpdateWithTitleSkipsFetch verifies that an explicit title is
// sent as-is without fetching the current step.
func TestCardsStepUpdateWithTitleSkipsFetch(t *testing.T) {
transport := &mockStepUpdateTransport{}
app := setupCardsMockApp(t, transport)

cmd := newCardsStepUpdateCmd()
err := executeCommand(cmd, app, "456", "New title")
require.NoError(t, err)

assert.Equal(t, 0, transport.getCount)

var body map[string]any
require.NoError(t, json.Unmarshal(transport.capturedPut, &body))
assert.Equal(t, "New title", body["title"])
}

// TestCardsStepMoveRequiresCard tests that --card is required for step move.
func TestCardsStepMoveShowsHelp(t *testing.T) {
app, _ := setupTestApp(t)
Expand Down
Loading