From 5f1f4a267fc8779fa9e6c34a8388d8379f7cd881 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 30 Jul 2026 09:21:11 -0700 Subject: [PATCH] Carry an explicit project into the bare boost list usage hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #590 withdrew the account-wide boost listing (bc3 #12464) while this branch was in flight, absorbing its flag removal, surface acknowledgments, docs, and most of its tests. What survives is the one refinement #590 did not pick up: a bare `boost list` that names a project — via --project, its --in alias, or the root-level flag — gets a usage hint that carries that project forward into the corrected invocation, instead of the generic missing-argument path. An explicit --event rides along in the same hint rather than being silently dropped from the correction. A config-supplied project still takes the generic path: only an explicit flag is a statement of intent worth echoing back. The new test pins the contract across all three flag forms plus the --event combination, and asserts no request reaches the API. It fails against the pre-change code, where a bare interactive invocation answered with help and a nil error rather than a structured usage error. --- internal/commands/boost.go | 15 +++++++++++++++ internal/commands/boost_test.go | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/internal/commands/boost.go b/internal/commands/boost.go index b4eee92a..66d05e87 100644 --- a/internal/commands/boost.go +++ b/internal/commands/boost.go @@ -68,6 +68,21 @@ response, so there is nothing to page through either.`, RunE: func(cmd *cobra.Command, args []string) error { app := appctx.FromContext(cmd.Context()) if len(args) == 0 { + // An explicitly named project cannot stand in for the item ID, + // but it is a statement of intent the hint can honor: carry it + // forward into the corrected invocation. Otherwise the generic + // missing-argument path answers (help when interactive). + explicitProject := *project + if explicitProject == "" { + explicitProject = app.Flags.Project + } + if explicitProject != "" { + hint := fmt.Sprintf("Pass the item's ID or URL: basecamp boost list --project %s", explicitProject) + if eventID != "" { + hint = fmt.Sprintf("%s --event %s", hint, eventID) + } + return output.ErrUsageHint("Boosts belong to an item, so listing them needs one", hint) + } return missingArg(cmd, "") } if err := ensureAccount(cmd, app); err != nil { diff --git a/internal/commands/boost_test.go b/internal/commands/boost_test.go index 79023dda..cea398a3 100644 --- a/internal/commands/boost_test.go +++ b/internal/commands/boost_test.go @@ -297,6 +297,40 @@ func TestBoostListConfiguredProjectStillNeedsAnID(t *testing.T) { assert.Empty(t, transport.recorded()) } +// An explicitly named project cannot stand in for the item ID either, but it +// is a statement of intent: the hint carries it forward into the corrected +// invocation — through --project, its --in alias, and the root-level form +// that lands in app.Flags.Project. +func TestBoostListExplicitProjectWithoutIDCarriesProjectIntoHint(t *testing.T) { + assertHintCarriesProject := func(t *testing.T, err error, transport *recordingTransport) { + t.Helper() + requireBoostUsageError(t, err, "listing them needs one") + var e *output.Error + require.True(t, errors.As(err, &e)) + assert.Contains(t, e.Hint, "--project 123") + assert.Empty(t, transport.recorded(), "a usage error must not reach the API") + } + + cmd, app, transport := setupBoostListTest(t, nil) + assertHintCarriesProject(t, executeBoostCommand(cmd, app, "list", "--project", "123"), transport) + + cmd, app, transport = setupBoostListTest(t, nil) + assertHintCarriesProject(t, executeBoostCommand(cmd, app, "list", "--in", "123"), transport) + + cmd, app, transport = setupBoostListTest(t, nil) + app.Flags.Project = "123" + assertHintCarriesProject(t, executeBoostCommand(cmd, app, "list"), transport) + + // --event is explicit intent too: when the structured hint fires, it + // rides along rather than being silently dropped from the correction. + cmd, app, transport = setupBoostListTest(t, nil) + err := executeBoostCommand(cmd, app, "list", "--project", "123", "--event", "5") + assertHintCarriesProject(t, err, transport) + var e *output.Error + require.True(t, errors.As(err, &e)) + assert.Contains(t, e.Hint, "--event 5") +} + // The pagination flags existed only for the account-wide feed. With that gone // they must not linger: an item's boosts arrive in one unpaginated response, // and the SDK documents BoostListOptions.Page as not honoring a page number.