From e3ccd15818aa7c137a16a0f41d7c50be3ad01239 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Sun, 17 May 2026 10:24:54 +0100 Subject: [PATCH] fix(multi): accept --projects as an array MultiCommand declared --projects with InputOption::VALUE_REQUIRED only, so Symfony returned the option value as a string. getSelectedProjects() then read it through ArrayArgument::getOption(), which insists on an array and threw BadMethodCallException: "The value of option --projects is not an array" before any project lookup happened. Add InputOption::VALUE_IS_ARRAY to the option definition, matching the pattern used by every other command that consumes its option through ArrayArgument::getOption (Environment\List, Resources, User\Add, etc.). ArrayArgument::split() still handles comma- and whitespace-separated values, so -p a,b,c, -p a -p b, and -p "a b" all work. Add an integration test that runs multi against two mocked projects using both comma-separated and repeated -p forms, and asserts the unknown-ID path produces the "Project ID(s) not found" error. Co-authored-by: Claude Opus 4.7 (1M context) --- integration-tests/multi_test.go | 62 +++++++++++++++++++++++++++++ legacy/src/Command/MultiCommand.php | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 integration-tests/multi_test.go diff --git a/integration-tests/multi_test.go b/integration-tests/multi_test.go new file mode 100644 index 000000000..eaa505a07 --- /dev/null +++ b/integration-tests/multi_test.go @@ -0,0 +1,62 @@ +package tests + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/upsun/cli/pkg/mockapi" +) + +func TestMulti(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + projectID1 := mockapi.ProjectID() + projectID2 := mockapi.ProjectID() + + apiHandler.SetProjects([]*mockapi.Project{ + { + ID: projectID1, + Title: "Project One", + Links: mockapi.MakeHALLinks("self=/projects/" + projectID1), + }, + { + ID: projectID2, + Title: "Project Two", + Links: mockapi.MakeHALLinks("self=/projects/" + projectID2), + }, + }) + + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + stdOut, stdErr, err := f.RunCombinedOutput( + "multi", "-p", projectID1+","+projectID2, "--", "pro:info", "title", + ) + assert.NoError(t, err) + assert.Contains(t, stdOut, "Project One") + assert.Contains(t, stdOut, "Project Two") + assert.Contains(t, stdErr, "Running command on 2 projects") + assert.Contains(t, stdErr, "("+projectID1+")") + assert.Contains(t, stdErr, "("+projectID2+")") + + stdOut, _, err = f.RunCombinedOutput( + "multi", "-p", projectID1, "-p", projectID2, "--", "pro:info", "title", + ) + assert.NoError(t, err) + assert.Contains(t, stdOut, "Project One") + assert.Contains(t, stdOut, "Project Two") + + stdOut, stdErr, err = f.RunCombinedOutput( + "multi", "-p", "nonexistent-id", "--", "pro:info", "title", + ) + assert.Error(t, err) + assert.Contains(t, stdErr, "Project ID(s) not found") + assert.Contains(t, stdErr, "nonexistent-id") + assert.NotContains(t, stdOut, "Project One") +} diff --git a/legacy/src/Command/MultiCommand.php b/legacy/src/Command/MultiCommand.php index 68ca85c77..019489fef 100644 --- a/legacy/src/Command/MultiCommand.php +++ b/legacy/src/Command/MultiCommand.php @@ -36,7 +36,7 @@ protected function configure(): void { $this ->addArgument('cmd', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'The command to execute') - ->addOption('projects', 'p', InputOption::VALUE_REQUIRED, 'A list of project IDs. ' . ArrayArgument::SPLIT_HELP) + ->addOption('projects', 'p', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A list of project IDs. ' . ArrayArgument::SPLIT_HELP) ->addOption('continue', null, InputOption::VALUE_NONE, 'Continue running commands even if an exception is encountered') ->addOption('sort', null, InputOption::VALUE_REQUIRED, 'A property by which to sort the list of project options', 'title') ->addOption('reverse', null, InputOption::VALUE_NONE, 'Reverse the order of project options');