From b5f0da0de38ed884a4cbb0e9135c4256a6639d4a Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Wed, 29 Apr 2026 12:05:23 +0100 Subject: [PATCH] fix(resources:get): skip remote container selection for array options ResourcesGetCommand defines --app and --worker as VALUE_IS_ARRAY (filters, not selectors). When the command is run without those options the empty default ([]) reached Selector::selectRemoteContainer, where it cleared the !== null check and was cast to a string, producing several "Array to string conversion" warnings followed by: Worker not found: Array (in app: Array) In Selector::getSelection, treat array-typed --app as a signal to skip the whole remote container block. The calling command filters services itself via ResourcesUtil::filterServices, so no per-container selection is needed. Add an integration test that runs resources:get against a mocked API and asserts the command exits successfully with no array-cast warnings. --- integration-tests/resources_get_test.go | 87 +++++++++++++++++++++++++ legacy/src/Selector/Selector.php | 5 +- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 integration-tests/resources_get_test.go diff --git a/integration-tests/resources_get_test.go b/integration-tests/resources_get_test.go new file mode 100644 index 000000000..575ff585f --- /dev/null +++ b/integration-tests/resources_get_test.go @@ -0,0 +1,87 @@ +package tests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/upsun/cli/pkg/mockapi" +) + +// TestResourcesGet is a regression test for "Worker not found: Array (in app: +// Array)" raised when running resources:get without --app or --worker. The +// command defines those options as VALUE_IS_ARRAY (filters), so an empty +// default ([]) was being cast to string and treated as a worker name in +// Selector::selectRemoteContainer. +func TestResourcesGet(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + + projectID := mockapi.ProjectID() + + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + Links: mockapi.MakeHALLinks( + "self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments", + ), + DefaultBranch: "main", + }}) + + apiHandler.SetEnvironments([]*mockapi.Environment{ + makeEnv(projectID, "main", "production", "active", nil), + }) + + apiHandler.Get("/projects/"+projectID+"/settings", func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "sizing_api_enabled": true, + }) + }) + + nextPath := "/projects/" + projectID + "/environments/main/deployments/next" + apiHandler.Get(nextPath, func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "webapps": map[string]any{ + "app": map[string]any{ + "name": "app", + "type": "golang:1.23", + "container_profile": "BALANCED", + "resources": map[string]any{ + "profile_size": "0.1", + }, + "instance_count": 1, + "disk": 512, + }, + }, + "services": map[string]any{}, + "workers": map[string]any{}, + "routes": map[string]any{}, + "container_profiles": map[string]any{ + "BALANCED": map[string]any{ + "0.1": map[string]any{ + "cpu": "0.1", + "memory": "256", + "cpu_type": "guaranteed", + }, + }, + }, + }) + }) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + stdout, stderr, err := f.RunCombinedOutput("resources:get", "-p", projectID, "-e", "main") + require.NoError(t, err, "stdout: %s\nstderr: %s", stdout, stderr) + assert.NotContains(t, stderr, "Array to string conversion") + assert.NotContains(t, stderr, "Worker not found") + assert.Contains(t, stdout, "app") +} diff --git a/legacy/src/Selector/Selector.php b/legacy/src/Selector/Selector.php index 97cd7e7de..c6f214962 100644 --- a/legacy/src/Selector/Selector.php +++ b/legacy/src/Selector/Selector.php @@ -170,7 +170,10 @@ public function getSelection(InputInterface $input, ?SelectorConfig $config = nu // Select the app. $appName = null; $remoteContainer = null; - if ($input->hasOption('app')) { + // Skip when --app is an array option (used purely as a filter, e.g. + // by resources:get); the calling command will handle filtering itself. + // VALUE_IS_ARRAY options always return an array (empty by default). + if ($input->hasOption('app') && !is_array($input->getOption('app'))) { if ($input->getOption('app')) { $appName = (string) $input->getOption('app'); } elseif (isset($result['appId'])) {