diff --git a/packages/cli/src/commands/ci/fetch-default-org-slug.mts b/packages/cli/src/commands/ci/fetch-default-org-slug.mts index ea144771c..930e4baf5 100644 --- a/packages/cli/src/commands/ci/fetch-default-org-slug.mts +++ b/packages/cli/src/commands/ci/fetch-default-org-slug.mts @@ -29,8 +29,7 @@ export async function getDefaultOrgSlug(): Promise> { } const { organizations } = orgsCResult.data - const keys = Object.keys(organizations) - if (!keys.length) { + if (!organizations.length) { return { ok: false, message: 'Failed to establish identity', @@ -38,10 +37,11 @@ export async function getDefaultOrgSlug(): Promise> { } } - const [firstKey] = keys - const slug = firstKey - ? ((organizations as any)[firstKey]?.name ?? undefined) - : undefined + // Use the API slug (URL-safe identifier), not the display name. Previously + // read `.name` which broke API calls for orgs like "Alamos GmbH" whose + // display name contains spaces, producing URLs like + // `/v0/orgs/Alamos%20GmbH/full-scans` that 404'd. + const slug = organizations[0]?.slug if (!slug) { return { ok: false, diff --git a/packages/cli/src/commands/scan/suggest-org-slug.mts b/packages/cli/src/commands/scan/suggest-org-slug.mts index 55b09dea6..d19d2ff39 100644 --- a/packages/cli/src/commands/scan/suggest-org-slug.mts +++ b/packages/cli/src/commands/scan/suggest-org-slug.mts @@ -13,19 +13,21 @@ export async function suggestOrgSlug(): Promise { return undefined } - // Ignore a failed request here. It was not the primary goal of - // running this command and reporting it only leads to end-user confusion. const { organizations } = orgsCResult.data const proceed = await select({ message: 'Missing org name; do you want to use any of these orgs for this scan?', choices: [ ...organizations.map(o => { - const name = o.name ?? o.slug + // Display the human-readable name when available, but always + // return the slug — downstream API routes are slug-keyed, and + // orgs with spaces in their display name produced 404s when + // we sent the display name through the URL. + const display = o.name ?? o.slug return { - name: `Yes [${name}]`, - value: name, - description: `Use "${name}" as the organization`, + name: `Yes [${display}]`, + value: o.slug, + description: `Use "${display}" as the organization`, } }), { diff --git a/packages/cli/test/unit/commands/ci/fetch-default-org-slug.test.mts b/packages/cli/test/unit/commands/ci/fetch-default-org-slug.test.mts index 2075e64a3..7c41610b1 100644 --- a/packages/cli/test/unit/commands/ci/fetch-default-org-slug.test.mts +++ b/packages/cli/test/unit/commands/ci/fetch-default-org-slug.test.mts @@ -97,13 +97,15 @@ describe('getDefaultOrgSlug', () => { mockFetchFn.mockResolvedValue({ ok: true, data: { - organizations: { - 'org-1': { + // fetchOrganization converts the SDK dict into an array of org + // objects before returning, so mock the array shape directly. + organizations: [ + { id: 'org-1', name: 'Test Organization', slug: 'test-org', }, - }, + ], }, }) @@ -112,7 +114,31 @@ describe('getDefaultOrgSlug', () => { expect(result).toEqual({ ok: true, message: 'Retrieved default org from server', - data: 'Test Organization', + data: 'test-org', + }) + }) + + it('returns slug (not display name) for orgs with spaces', async () => { + // Regression guard for SMO-622: returning display name produced + // URLs like `/v0/orgs/Alamos%20GmbH/...` that 404'd. + mockFn.mockReturnValue(undefined) + mockOrgSlug.value = undefined + + mockFetchFn.mockResolvedValue({ + ok: true, + data: { + organizations: [ + { id: 'org-1', name: 'Alamos GmbH', slug: 'alamos-gmbh' }, + ], + }, + }) + + const result = await getDefaultOrgSlug() + + expect(result).toEqual({ + ok: true, + message: 'Retrieved default org from server', + data: 'alamos-gmbh', }) }) @@ -139,7 +165,7 @@ describe('getDefaultOrgSlug', () => { mockFetchFn.mockResolvedValue({ ok: true, data: { - organizations: {}, + organizations: [], }, }) @@ -152,20 +178,15 @@ describe('getDefaultOrgSlug', () => { }) }) - it('returns error when organization has no name', async () => { + it('returns error when organization has no slug', async () => { mockFn.mockReturnValue(undefined) mockOrgSlug.value = undefined mockFetchFn.mockResolvedValue({ ok: true, data: { - organizations: { - 'org-1': { - id: 'org-1', - slug: 'org-slug', - // Missing name field. - }, - }, + // Missing slug — defensive check in case the API ever omits it. + organizations: [{ id: 'org-1', name: 'Test Org' }], }, }) diff --git a/packages/cli/test/unit/commands/scan/suggest-org-slug.test.mts b/packages/cli/test/unit/commands/scan/suggest-org-slug.test.mts index e184c62a1..afb41f005 100644 --- a/packages/cli/test/unit/commands/scan/suggest-org-slug.test.mts +++ b/packages/cli/test/unit/commands/scan/suggest-org-slug.test.mts @@ -136,5 +136,29 @@ describe('suggest-org-slug', () => { expect(noChoice).toBeDefined() expect(noChoice!.value).toBe('') }) + + it('returns the slug (not display name) for orgs where they differ', async () => { + // Regression guard: passing the display name through to the API + // produced 404s for orgs with spaces, e.g. + // `/v0/orgs/Alamos%20GmbH/...` instead of `/v0/orgs/alamos-gmbh/...`. + mockFetchOrganization.mockResolvedValue({ + ok: true, + data: { + organizations: [{ name: 'Alamos GmbH', slug: 'alamos-gmbh' }], + }, + }) + mockSelect.mockResolvedValue('alamos-gmbh') + + await suggestOrgSlug() + + const callArg = mockSelect.mock.calls[0]![0] as { + choices: Array<{ name: string; value: string; description: string }> + } + // The choice value must be the slug. The visible label/description + // still use the friendlier display name. + expect(callArg.choices[0]!.value).toBe('alamos-gmbh') + expect(callArg.choices[0]!.name).toContain('Alamos GmbH') + expect(callArg.choices[0]!.description).toContain('Alamos GmbH') + }) }) })