From 146a88ab6f20107d984d2129597f4bf681f33f8b Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 09:53:07 +0000 Subject: [PATCH 1/3] Accept ?w= URL query parameter alongside ?o= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Databricks UI is migrating from ?o= to ?w= as the SPOG URL query parameter, matching the new workspace addressing header. Extend the CLI's URL parsers to recognize ?w= in addition to the existing ?o= and ?workspace_id= spellings. Pure addition; no existing URL changes meaning. This affects: - databricks api — workspace ID extracted from the path and sent as the routing header on the call. - databricks auth login --host "https://...?w=..." — workspace ID extracted from the host URL and persisted to the profile. - workspace.host in databricks.yml — same parser is reused. When more than one spelling is present on a single URL, ?o= takes precedence over ?w=, which takes precedence over ?workspace_id=. The "o" precedence preserves the meaning of any URL already pasted from older UI builds, shell history, or committed databricks.yml files. The cmd/api helper extractOrgIDFromQuery is renamed to extractWorkspaceIDFromQuery to reflect that it now returns the value under multiple recognized parameter names. --- .../workspace-id-from-w-query/out.test.toml | 3 ++ .../api/workspace-id-from-w-query/output.txt | 21 +++++++++++ .../cmd/api/workspace-id-from-w-query/script | 2 + cmd/api/api.go | 37 ++++++++++++------- cmd/api/api_test.go | 30 ++++++++++++++- cmd/auth/login.go | 6 ++- libs/auth/hostparams.go | 17 +++++++-- libs/auth/hostparams_test.go | 20 ++++++++++ 8 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 acceptance/cmd/api/workspace-id-from-w-query/out.test.toml create mode 100644 acceptance/cmd/api/workspace-id-from-w-query/output.txt create mode 100644 acceptance/cmd/api/workspace-id-from-w-query/script diff --git a/acceptance/cmd/api/workspace-id-from-w-query/out.test.toml b/acceptance/cmd/api/workspace-id-from-w-query/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-w-query/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-from-w-query/output.txt b/acceptance/cmd/api/workspace-id-from-w-query/output.txt new file mode 100644 index 00000000000..34d0d3c847a --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-w-query/output.txt @@ -0,0 +1,21 @@ +{} + +>>> print_requests.py --get //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Workspace-Id": [ + "999" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list", + "q": { + "w": "999" + } +} diff --git a/acceptance/cmd/api/workspace-id-from-w-query/script b/acceptance/cmd/api/workspace-id-from-w-query/script new file mode 100644 index 00000000000..f60139e4b7c --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-w-query/script @@ -0,0 +1,2 @@ +MSYS_NO_PATHCONV=1 $CLI api get "/api/2.0/clusters/list?w=999" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" "999" diff --git a/cmd/api/api.go b/cmd/api/api.go index 044584c1220..59528527b53 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -27,12 +27,18 @@ const ( // header for rollback safety. workspaceIDHeader = "X-Databricks-Workspace-Id" - // orgIDQueryParam is the SPOG (single-page-of-glass) URL convention used - // by the Databricks UI: "?o=" identifies the workspace a URL - // targets. When present on the path, we treat it as a per-call override - // for the workspace routing identifier so that pasted SPOG URLs route - // correctly without requiring --workspace-id. - orgIDQueryParam = "o" + // orgIDQueryParam and workspaceIDQueryParam are the SPOG + // (single-page-of-glass) URL convention used by the Databricks UI: + // "?o=" or "?w=" identifies the workspace a + // URL targets. When present on the path, we treat it as a per-call + // override for the workspace routing identifier so that pasted SPOG URLs + // route correctly without requiring --workspace-id. "w" is the new + // spelling that matches the X-Databricks-Workspace-Id header; "o" stays + // accepted for URLs already pasted from older UI builds, shell history, + // or committed databricks.yml files. "o" takes precedence when both are + // present to preserve the meaning of existing URLs. + orgIDQueryParam = "o" + workspaceIDQueryParam = "w" ) // accountSegmentRe matches a non-empty segment immediately after "accounts/", @@ -165,14 +171,19 @@ func hasAccountSegment(rawPath string) (bool, error) { return accountSegmentRe.MatchString(p), nil } -// extractOrgIDFromQuery returns the value of the "o" query parameter on path -// (the SPOG URL convention), or "" if absent or empty. -func extractOrgIDFromQuery(rawPath string) (string, error) { +// extractWorkspaceIDFromQuery returns the workspace ID encoded in the path's +// query string (the SPOG URL convention). It checks "o" first, then "w"; +// returns "" if neither is present or non-empty. +func extractWorkspaceIDFromQuery(rawPath string) (string, error) { u, err := url.Parse(rawPath) if err != nil { return "", fmt.Errorf("parse path: %w", err) } - return u.Query().Get(orgIDQueryParam), nil + q := u.Query() + if v := q.Get(orgIDQueryParam); v != "" { + return v, nil + } + return q.Get(workspaceIDQueryParam), nil } // resolveOrgID picks the value (if any) for the workspace routing identifier @@ -197,12 +208,12 @@ func resolveOrgID( } return workspaceIDFlag, nil } - orgIDFromQuery, err := extractOrgIDFromQuery(path) + workspaceIDFromQuery, err := extractWorkspaceIDFromQuery(path) if err != nil { return "", err } - if orgIDFromQuery != "" { - return orgIDFromQuery, nil + if workspaceIDFromQuery != "" { + return workspaceIDFromQuery, nil } isAccount, err := hasAccountSegment(path) if err != nil { diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go index 69cd28fe5fe..0a50b1d842c 100644 --- a/cmd/api/api_test.go +++ b/cmd/api/api_test.go @@ -46,7 +46,7 @@ func TestHasAccountSegment(t *testing.T) { } } -func TestExtractOrgIDFromQuery(t *testing.T) { +func TestExtractWorkspaceIDFromQuery(t *testing.T) { cases := []struct { name string path string @@ -60,10 +60,15 @@ func TestExtractOrgIDFromQuery(t *testing.T) { {"unrelated o-prefixed param ignored", "/api/2.0/clusters/list?other=1", ""}, {"absolute URL", "https://example.com/api/2.0/clusters/list?o=42", "42"}, {"first value wins on duplicate", "/api/2.0/clusters/list?o=1&o=2", "1"}, + {"w param present", "/api/2.2/jobs/list?w=7474644166319138", "7474644166319138"}, + {"w param empty", "/api/2.0/clusters/list?w=", ""}, + {"w among other params", "/api/2.0/clusters/list?foo=bar&w=123", "123"}, + {"o wins over w when both present", "/api/2.0/clusters/list?o=111&w=222", "111"}, + {"w used when o is empty", "/api/2.0/clusters/list?o=&w=222", "222"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - got, err := extractOrgIDFromQuery(c.path) + got, err := extractWorkspaceIDFromQuery(c.path) require.NoError(t, err) assert.Equal(t, c.want, got) }) @@ -76,6 +81,7 @@ func TestResolveOrgID(t *testing.T) { accountPath = "/api/2.0/accounts/abc-123/network-policies" proxyPath = "/api/2.0/preview/accounts/access-control/rule-sets" spogPath = "/api/2.2/jobs/list?o=7474644166319138" + spogPathW = "/api/2.2/jobs/list?w=7474644166319138" spogAccountPath = "/api/2.0/accounts/abc-123/network-policies?o=7474644166319138" spogWorkspaceID = "7474644166319138" resolvedWSID = "900800700600" @@ -189,6 +195,26 @@ func TestResolveOrgID(t *testing.T) { path: spogAccountPath, want: spogWorkspaceID, }, + { + name: "?w= sets identifier when no flag and no profile WorkspaceID", + cfgWorkspaceID: "", + path: spogPathW, + want: spogWorkspaceID, + }, + { + name: "?w= overrides profile WorkspaceID", + cfgWorkspaceID: resolvedWSID, + path: spogPathW, + want: spogWorkspaceID, + }, + { + name: "--workspace-id wins over ?w=", + workspaceIDFlag: flagWSID, + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: spogPathW, + want: flagWSID, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 2e8cce02404..640ee7f16ba 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -118,9 +118,11 @@ use the flags directly to specify both. The host URL may include query parameters to set the workspace and account ID: - databricks auth login --host "https://?o=&account_id=" + databricks auth login --host "https://?w=&account_id=" -Note: URLs containing "?" must be quoted to prevent shell interpretation. +The workspace ID may be passed as ?w= (preferred), ?o= (legacy), or +?workspace_id=. Note: URLs containing "?" must be quoted to prevent shell +interpretation. If a profile with the given name already exists, it is updated. Otherwise a new profile is created. diff --git a/libs/auth/hostparams.go b/libs/auth/hostparams.go index f363cfa5a35..ace04bfed6f 100644 --- a/libs/auth/hostparams.go +++ b/libs/auth/hostparams.go @@ -11,7 +11,7 @@ type HostParams struct { // Host is the URL with query parameters stripped. Host string - // WorkspaceID extracted from ?o= or ?workspace_id=. + // WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=. // Empty if not present or not numeric. WorkspaceID string @@ -21,9 +21,14 @@ type HostParams struct { } // ExtractHostQueryParams parses recognized query parameters from a host URL. -// Recognized parameters: o (workspace_id), workspace_id, a (account_id), account_id. -// Workspace IDs must be numeric; non-numeric values are ignored. -// The returned Host has all query parameters and fragments stripped. +// Recognized parameters: o (workspace_id), w (workspace_id), workspace_id, +// a (account_id), account_id. The "w" spelling matches the new +// X-Databricks-Workspace-Id routing header; "o" is the legacy SPOG form and +// stays accepted so URLs already in databricks.yml / shell history keep +// working. When more than one is present, "o" wins to preserve the meaning +// of existing URLs. Workspace IDs must be numeric; non-numeric values are +// ignored. The returned Host has all query parameters and fragments +// stripped. func ExtractHostQueryParams(host string) HostParams { u, err := url.Parse(host) if err != nil || u.RawQuery == "" { @@ -37,6 +42,10 @@ func ExtractHostQueryParams(host string) HostParams { if _, err := strconv.ParseInt(v, 10, 64); err == nil { workspaceID = v } + } else if v := q.Get("w"); v != "" { + if _, err := strconv.ParseInt(v, 10, 64); err == nil { + workspaceID = v + } } else if v := q.Get("workspace_id"); v != "" { if _, err := strconv.ParseInt(v, 10, 64); err == nil { workspaceID = v diff --git a/libs/auth/hostparams_test.go b/libs/auth/hostparams_test.go index 900c69fb92d..bbfabfbf75b 100644 --- a/libs/auth/hostparams_test.go +++ b/libs/auth/hostparams_test.go @@ -27,11 +27,26 @@ func TestExtractHostQueryParams(t *testing.T) { host: "https://spog.example.com/?account_id=abc", want: HostParams{Host: "https://spog.example.com", AccountID: "abc"}, }, + { + name: "extract workspace_id from ?w=", + host: "https://spog.example.com/?w=12345", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "12345"}, + }, { name: "extract workspace_id from ?workspace_id=", host: "https://spog.example.com/?workspace_id=99999", want: HostParams{Host: "https://spog.example.com", WorkspaceID: "99999"}, }, + { + name: "?o= wins over ?w= when both present", + host: "https://spog.example.com/?o=11111&w=22222", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"}, + }, + { + name: "?w= wins over ?workspace_id= when both present", + host: "https://spog.example.com/?w=11111&workspace_id=22222", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"}, + }, { name: "no query params leaves host unchanged", host: "https://spog.example.com", @@ -42,6 +57,11 @@ func TestExtractHostQueryParams(t *testing.T) { host: "https://spog.example.com/?o=abc", want: HostParams{Host: "https://spog.example.com"}, }, + { + name: "non-numeric ?w= is skipped", + host: "https://spog.example.com/?w=abc", + want: HostParams{Host: "https://spog.example.com"}, + }, { name: "non-numeric ?workspace_id= is skipped", host: "https://spog.example.com/?workspace_id=abc", From 144792db3d1ef9cb24edccca5a656633a62db955 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 13:23:03 +0000 Subject: [PATCH 2/3] Drop numeric-only validation from host URL workspace ID parsing The new X-Databricks-Workspace-Id routing header accepts non-numeric workspace identifiers (e.g. connection-style strings) in addition to classic numeric workspace IDs; the server disambiguates. The URL parser was rejecting any non-numeric value via strconv.ParseInt and silently dropping it, which would block pasted URLs that carry one of the new identifier shapes. Remove the numeric validation from all three branches (?o=, ?w=, ?workspace_id=) in ExtractHostQueryParams and pass the raw value through. Affects every caller of the helper: - databricks auth login --host - workspace.host in databricks.yml (via bundle/config/workspace.go) Flip the existing "non-numeric is skipped" tests in hostparams_test.go and host_env_test.go to "non-numeric is passed through". Add a positive test case for a UUID-shaped connection-style identifier. --- libs/auth/host_env_test.go | 7 ++++--- libs/auth/hostparams.go | 25 +++++++++++-------------- libs/auth/hostparams_test.go | 17 +++++++++++------ 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/libs/auth/host_env_test.go b/libs/auth/host_env_test.go index 152f4d22d47..17fa76ff68a 100644 --- a/libs/auth/host_env_test.go +++ b/libs/auth/host_env_test.go @@ -53,9 +53,10 @@ func TestNormalizeDatabricksConfigFromEnv(t *testing.T) { name: "no host env is a no-op", }, { - name: "non-numeric o is dropped, host trailing slash trimmed", - host: "https://acme.databricks.net/?o=notanumber", - wantHost: "https://acme.databricks.net", + name: "non-numeric o is passed through, host trailing slash trimmed", + host: "https://acme.databricks.net/?o=notanumber", + wantHost: "https://acme.databricks.net", + wantWorkspaceID: "notanumber", }, } diff --git a/libs/auth/hostparams.go b/libs/auth/hostparams.go index ace04bfed6f..74eaf79636e 100644 --- a/libs/auth/hostparams.go +++ b/libs/auth/hostparams.go @@ -2,7 +2,6 @@ package auth import ( "net/url" - "strconv" "strings" ) @@ -12,7 +11,10 @@ type HostParams struct { Host string // WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=. - // Empty if not present or not numeric. + // Empty if not present. Any non-empty value is accepted as-is — + // X-Databricks-Workspace-Id supports both classic numeric workspace IDs + // and non-numeric connection-style identifiers, and the server + // disambiguates. WorkspaceID string // AccountID extracted from ?a= or ?account_id=. @@ -26,9 +28,10 @@ type HostParams struct { // X-Databricks-Workspace-Id routing header; "o" is the legacy SPOG form and // stays accepted so URLs already in databricks.yml / shell history keep // working. When more than one is present, "o" wins to preserve the meaning -// of existing URLs. Workspace IDs must be numeric; non-numeric values are -// ignored. The returned Host has all query parameters and fragments -// stripped. +// of existing URLs. Workspace ID values are passed through unchanged — the +// server-side header accepts both classic numeric IDs and non-numeric +// connection-style identifiers. The returned Host has all query parameters +// and fragments stripped. func ExtractHostQueryParams(host string) HostParams { u, err := url.Parse(host) if err != nil || u.RawQuery == "" { @@ -39,17 +42,11 @@ func ExtractHostQueryParams(host string) HostParams { var workspaceID string if v := q.Get("o"); v != "" { - if _, err := strconv.ParseInt(v, 10, 64); err == nil { - workspaceID = v - } + workspaceID = v } else if v := q.Get("w"); v != "" { - if _, err := strconv.ParseInt(v, 10, 64); err == nil { - workspaceID = v - } + workspaceID = v } else if v := q.Get("workspace_id"); v != "" { - if _, err := strconv.ParseInt(v, 10, 64); err == nil { - workspaceID = v - } + workspaceID = v } var accountID string diff --git a/libs/auth/hostparams_test.go b/libs/auth/hostparams_test.go index bbfabfbf75b..523fdd23cac 100644 --- a/libs/auth/hostparams_test.go +++ b/libs/auth/hostparams_test.go @@ -53,19 +53,24 @@ func TestExtractHostQueryParams(t *testing.T) { want: HostParams{Host: "https://spog.example.com"}, }, { - name: "non-numeric ?o= is skipped", + name: "non-numeric ?o= is passed through", host: "https://spog.example.com/?o=abc", - want: HostParams{Host: "https://spog.example.com"}, + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, }, { - name: "non-numeric ?w= is skipped", + name: "non-numeric ?w= is passed through", host: "https://spog.example.com/?w=abc", - want: HostParams{Host: "https://spog.example.com"}, + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, }, { - name: "non-numeric ?workspace_id= is skipped", + name: "non-numeric ?workspace_id= is passed through", host: "https://spog.example.com/?workspace_id=abc", - want: HostParams{Host: "https://spog.example.com"}, + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, + }, + { + name: "connection-id-style ?w= value passed through", + host: "https://spog.example.com/?w=123e4567-e89b-12d3-a456-426614174000", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "123e4567-e89b-12d3-a456-426614174000"}, }, { name: "invalid URL is left unchanged", From 815f916bf9c83a0370e96e783001c1456652f1e4 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 15:42:07 +0000 Subject: [PATCH 3/3] Restrict non-numeric workspace ID acceptance to ?w= only Legacy URL spellings ?o= and ?workspace_id= predate the broader workspace identifier shapes the new routing header accepts. URLs in databricks.yml, browser bookmarks, and shell history that use those forms are always numeric, so keep the strconv.ParseInt validation on those branches to silently drop garbage values (matching pre-PR behavior). ?w= is the new spelling and must pass non-numeric connection-style identifiers through unchanged. Adjust the test cases: "non-numeric ?o= is skipped" and "non-numeric ?workspace_id= is skipped" go back to expecting the value to be dropped. The ?w= non-numeric and UUID-shaped positive cases stay. --- libs/auth/host_env_test.go | 7 +++---- libs/auth/hostparams.go | 30 +++++++++++++++++------------- libs/auth/hostparams_test.go | 8 ++++---- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/libs/auth/host_env_test.go b/libs/auth/host_env_test.go index 17fa76ff68a..152f4d22d47 100644 --- a/libs/auth/host_env_test.go +++ b/libs/auth/host_env_test.go @@ -53,10 +53,9 @@ func TestNormalizeDatabricksConfigFromEnv(t *testing.T) { name: "no host env is a no-op", }, { - name: "non-numeric o is passed through, host trailing slash trimmed", - host: "https://acme.databricks.net/?o=notanumber", - wantHost: "https://acme.databricks.net", - wantWorkspaceID: "notanumber", + name: "non-numeric o is dropped, host trailing slash trimmed", + host: "https://acme.databricks.net/?o=notanumber", + wantHost: "https://acme.databricks.net", }, } diff --git a/libs/auth/hostparams.go b/libs/auth/hostparams.go index 74eaf79636e..103d2834dcb 100644 --- a/libs/auth/hostparams.go +++ b/libs/auth/hostparams.go @@ -2,6 +2,7 @@ package auth import ( "net/url" + "strconv" "strings" ) @@ -11,10 +12,9 @@ type HostParams struct { Host string // WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=. - // Empty if not present. Any non-empty value is accepted as-is — - // X-Databricks-Workspace-Id supports both classic numeric workspace IDs - // and non-numeric connection-style identifiers, and the server - // disambiguates. + // Empty if not present. ?o= and ?workspace_id= are legacy spellings that + // remain numeric-only; ?w= is the new spelling and is passed through + // unchanged so non-numeric connection-style identifiers reach the server. WorkspaceID string // AccountID extracted from ?a= or ?account_id=. @@ -25,13 +25,13 @@ type HostParams struct { // ExtractHostQueryParams parses recognized query parameters from a host URL. // Recognized parameters: o (workspace_id), w (workspace_id), workspace_id, // a (account_id), account_id. The "w" spelling matches the new -// X-Databricks-Workspace-Id routing header; "o" is the legacy SPOG form and -// stays accepted so URLs already in databricks.yml / shell history keep -// working. When more than one is present, "o" wins to preserve the meaning -// of existing URLs. Workspace ID values are passed through unchanged — the -// server-side header accepts both classic numeric IDs and non-numeric -// connection-style identifiers. The returned Host has all query parameters -// and fragments stripped. +// X-Databricks-Workspace-Id routing header and accepts any non-empty value +// (including non-numeric connection-style identifiers). The legacy "o" and +// "workspace_id" spellings remain numeric-only — they predate the broader +// identifier shapes and historical URLs carrying those forms are always +// numeric. When more than one spelling is present, "o" wins to preserve the +// meaning of existing URLs. The returned Host has all query parameters and +// fragments stripped. func ExtractHostQueryParams(host string) HostParams { u, err := url.Parse(host) if err != nil || u.RawQuery == "" { @@ -42,11 +42,15 @@ func ExtractHostQueryParams(host string) HostParams { var workspaceID string if v := q.Get("o"); v != "" { - workspaceID = v + if _, err := strconv.ParseInt(v, 10, 64); err == nil { + workspaceID = v + } } else if v := q.Get("w"); v != "" { workspaceID = v } else if v := q.Get("workspace_id"); v != "" { - workspaceID = v + if _, err := strconv.ParseInt(v, 10, 64); err == nil { + workspaceID = v + } } var accountID string diff --git a/libs/auth/hostparams_test.go b/libs/auth/hostparams_test.go index 523fdd23cac..5fe43a3045a 100644 --- a/libs/auth/hostparams_test.go +++ b/libs/auth/hostparams_test.go @@ -53,9 +53,9 @@ func TestExtractHostQueryParams(t *testing.T) { want: HostParams{Host: "https://spog.example.com"}, }, { - name: "non-numeric ?o= is passed through", + name: "non-numeric ?o= is skipped (legacy spelling stays numeric-only)", host: "https://spog.example.com/?o=abc", - want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, + want: HostParams{Host: "https://spog.example.com"}, }, { name: "non-numeric ?w= is passed through", @@ -63,9 +63,9 @@ func TestExtractHostQueryParams(t *testing.T) { want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, }, { - name: "non-numeric ?workspace_id= is passed through", + name: "non-numeric ?workspace_id= is skipped (legacy spelling stays numeric-only)", host: "https://spog.example.com/?workspace_id=abc", - want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, + want: HostParams{Host: "https://spog.example.com"}, }, { name: "connection-id-style ?w= value passed through",