From faf889a3f0365c6ef4081ec601c27926938d232b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 14:48:57 +0000 Subject: [PATCH 1/3] feat: accept Honeycomb Management API key in integration connect Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vem7UpKJAXmFwtXrqkwreh --- src/commands/integration/connect.ts | 80 +++++++++++++++++++++- test/integration-connect-honeycomb.test.ts | 24 +++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 test/integration-connect-honeycomb.test.ts diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index 27a953a..e072060 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -30,6 +30,7 @@ import { promptConfirmOrBack, promptSelectOrBack, promptPasswordOrBack, + promptTextOrBack, } from '../../utils/prompt'; type ConnectBody = Parameters[0]; @@ -354,6 +355,15 @@ async function connectMcp( return 'connected'; } +// The API stores the Management API key only as a pair; an empty ID means the +// step was skipped and querying stays on Honeycomb's REST API. +export function honeycombManagementKeyFields( + managementApiKeyId: string, + managementApiKeySecret: string +): { managementApiKeyId?: string; managementApiKeySecret?: string } { + return managementApiKeyId && managementApiKeySecret ? { managementApiKeyId, managementApiKeySecret } : {}; +} + // --- Credential-based connects: each wizard step can go back to the previous // one, and backing out of the first returns BACK to re-open type selection --- async function connectWithCredentials( @@ -410,6 +420,9 @@ async function connectWithCredentials( } else if (type === 'honeycomb') { let region: 'us' | 'eu' = 'us'; let apiKey = ''; + let managementApiKeyId = ''; + let managementApiKeySecret = ''; + const managementSecretFlag = getArgString(args, 'managementApiKeySecret'); const ok = await runSteps([ choiceStep<'us' | 'eu'>( config, @@ -442,9 +455,71 @@ async function connectWithCredentials( apiKey = v; } ), + async () => { + const fromFlag = getArgString(args, 'managementApiKeyId'); + if (fromFlag !== undefined) { + managementApiKeyId = fromFlag; + return SKIPPED; + } + if (!isInteractive(config.nonInteractive)) { + if (managementSecretFlag !== undefined) { + throw new CLIError( + 'Missing required flag: --management-api-key-id', + ExitCode.USAGE, + 'Pass both --management-api-key-id and --management-api-key-secret, or neither.' + ); + } + return SKIPPED; + } + note('Optional. Create one in your Honeycomb team settings under API Keys.', 'Management API key'); + const value = await promptTextOrBack( + { nonInteractive: config.nonInteractive }, + 'Management API key ID', + { + placeholder: 'Enter to skip', + ...(managementSecretFlag !== undefined + ? { validate: (v: string) => (v ? undefined : 'Required with --management-api-key-secret') } + : {}), + } + ); + if (value === BACK) return BACK; + managementApiKeyId = (value || '').trim(); + return; + }, + async () => { + if (managementApiKeyId === '') return SKIPPED; + if (managementSecretFlag !== undefined) { + managementApiKeySecret = managementSecretFlag; + return SKIPPED; + } + if (!isInteractive(config.nonInteractive)) { + throw new CLIError( + 'Missing required flag: --management-api-key-secret', + ExitCode.USAGE, + 'Pass both --management-api-key-id and --management-api-key-secret, or neither.' + ); + } + const value = await promptPasswordOrBack( + { nonInteractive: config.nonInteractive }, + 'Management API key secret' + ); + if (value === BACK) return BACK; + managementApiKeySecret = value; + return; + }, ]); if (!ok) return BACK; - body = { type: 'honeycomb', workspaceId, region, apiKey }; + // Spread instead of literal fields: the client is generated from the live + // prod spec, which gains these two optional fields only when the matching + // API deploy lands. The spread keeps typecheck green on both sides; the + // old API strips unknown keys, the new one validates them. + body = { + type: 'honeycomb', + workspaceId, + region, + apiKey, + ...honeycombManagementKeyFields(managementApiKeyId, managementApiKeySecret), + }; } else if (type === 'axiom') { let region: 'us-east-1' | 'eu-central-1' = 'us-east-1'; let apiToken = ''; @@ -622,6 +697,8 @@ export const integrationConnectCommand: Command = { { flag: '--region ', description: 'Honeycomb (us|eu) or Axiom (us-east-1|eu-central-1)', type: 'string' }, { flag: '--api-key ', description: 'API key (Datadog / Honeycomb / Devin / Cursor / Factory / Conductor)', type: 'string' }, { flag: '--app-key ', description: 'App key (Datadog only)', type: 'string' }, + { flag: '--management-api-key-id ', description: 'Management API key ID (Honeycomb, optional)', type: 'string' }, + { flag: '--management-api-key-secret ', description: 'Management API key secret (Honeycomb, optional)', type: 'string' }, { flag: '--api-token ', description: 'API token (Axiom / Better Stack global token)', type: 'string' }, { flag: '--uptime-api-token ', description: 'Uptime API token (Better Stack only)', type: 'string' }, { flag: '--telemetry-api-token ', description: 'Telemetry API token (Better Stack only)', type: 'string' }, @@ -643,6 +720,7 @@ export const integrationConnectCommand: Command = { 'polylane integration connect --category code-agent', 'polylane integration connect --type datadog --site us5.datadoghq.com --api-key ... --app-key ...', 'polylane integration connect --type honeycomb --region us --api-key ...', + 'polylane integration connect --type honeycomb --region us --api-key ... --management-api-key-id ... --management-api-key-secret ...', 'polylane integration connect --type axiom --region us-east-1 --api-token ...', 'polylane integration connect --type betterstack --api-token ... --uptime-api-token ... --telemetry-api-token ...', 'polylane integration connect --type cursor --api-key crsr_...', diff --git a/test/integration-connect-honeycomb.test.ts b/test/integration-connect-honeycomb.test.ts new file mode 100644 index 0000000..2d5e553 --- /dev/null +++ b/test/integration-connect-honeycomb.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { honeycombManagementKeyFields } from '../src/commands/integration/connect'; + +describe('honeycombManagementKeyFields', () => { + it('returns both fields when both are set', () => { + assert.deepEqual(honeycombManagementKeyFields('hcxik_id', 'secret'), { + managementApiKeyId: 'hcxik_id', + managementApiKeySecret: 'secret', + }); + }); + + it('returns nothing when both are empty', () => { + assert.deepEqual(honeycombManagementKeyFields('', ''), {}); + }); + + it('returns nothing when only the ID is set', () => { + assert.deepEqual(honeycombManagementKeyFields('hcxik_id', ''), {}); + }); + + it('returns nothing when only the secret is set', () => { + assert.deepEqual(honeycombManagementKeyFields('', 'secret'), {}); + }); +}); From 4a27fbabe26063e5aac9a1ee2afb5f76e9a1eac6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 16:36:35 +0000 Subject: [PATCH 2/3] feat: require the Honeycomb Management API key in integration connect Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vem7UpKJAXmFwtXrqkwreh --- src/commands/integration/connect.ts | 54 +++++++++++----------- test/integration-connect-honeycomb.test.ts | 22 ++++++--- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index e072060..c7ba41d 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -355,13 +355,21 @@ async function connectMcp( return 'connected'; } -// The API stores the Management API key only as a pair; an empty ID means the -// step was skipped and querying stays on Honeycomb's REST API. +// The API stores the Management API key only as a pair, and both halves are +// required — a missing half is a usage error before any request is sent. export function honeycombManagementKeyFields( managementApiKeyId: string, managementApiKeySecret: string -): { managementApiKeyId?: string; managementApiKeySecret?: string } { - return managementApiKeyId && managementApiKeySecret ? { managementApiKeyId, managementApiKeySecret } : {}; +): { managementApiKeyId: string; managementApiKeySecret: string } { + if (!managementApiKeyId || !managementApiKeySecret) { + const missing = !managementApiKeyId ? '--management-api-key-id' : '--management-api-key-secret'; + throw new CLIError( + `Missing required flag: ${missing}`, + ExitCode.USAGE, + 'Pass both --management-api-key-id and --management-api-key-secret.' + ); + } + return { managementApiKeyId, managementApiKeySecret }; } // --- Credential-based connects: each wizard step can go back to the previous @@ -462,32 +470,23 @@ async function connectWithCredentials( return SKIPPED; } if (!isInteractive(config.nonInteractive)) { - if (managementSecretFlag !== undefined) { - throw new CLIError( - 'Missing required flag: --management-api-key-id', - ExitCode.USAGE, - 'Pass both --management-api-key-id and --management-api-key-secret, or neither.' - ); - } - return SKIPPED; + throw new CLIError( + 'Missing required flag: --management-api-key-id', + ExitCode.USAGE, + 'Pass both --management-api-key-id and --management-api-key-secret.' + ); } - note('Optional. Create one in your Honeycomb team settings under API Keys.', 'Management API key'); + note('Create one in your Honeycomb team settings under API Keys.', 'Management API key'); const value = await promptTextOrBack( { nonInteractive: config.nonInteractive }, 'Management API key ID', - { - placeholder: 'Enter to skip', - ...(managementSecretFlag !== undefined - ? { validate: (v: string) => (v ? undefined : 'Required with --management-api-key-secret') } - : {}), - } + { validate: (v: string) => (v.trim() ? undefined : 'Required') } ); if (value === BACK) return BACK; - managementApiKeyId = (value || '').trim(); + managementApiKeyId = value.trim(); return; }, async () => { - if (managementApiKeyId === '') return SKIPPED; if (managementSecretFlag !== undefined) { managementApiKeySecret = managementSecretFlag; return SKIPPED; @@ -496,7 +495,7 @@ async function connectWithCredentials( throw new CLIError( 'Missing required flag: --management-api-key-secret', ExitCode.USAGE, - 'Pass both --management-api-key-id and --management-api-key-secret, or neither.' + 'Pass both --management-api-key-id and --management-api-key-secret.' ); } const value = await promptPasswordOrBack( @@ -510,9 +509,9 @@ async function connectWithCredentials( ]); if (!ok) return BACK; // Spread instead of literal fields: the client is generated from the live - // prod spec, which gains these two optional fields only when the matching - // API deploy lands. The spread keeps typecheck green on both sides; the - // old API strips unknown keys, the new one validates them. + // prod spec, which gains these two fields only when the matching API + // deploy lands. The spread keeps typecheck green on both sides; the old + // API strips unknown keys, the new one validates them. body = { type: 'honeycomb', workspaceId, @@ -697,8 +696,8 @@ export const integrationConnectCommand: Command = { { flag: '--region ', description: 'Honeycomb (us|eu) or Axiom (us-east-1|eu-central-1)', type: 'string' }, { flag: '--api-key ', description: 'API key (Datadog / Honeycomb / Devin / Cursor / Factory / Conductor)', type: 'string' }, { flag: '--app-key ', description: 'App key (Datadog only)', type: 'string' }, - { flag: '--management-api-key-id ', description: 'Management API key ID (Honeycomb, optional)', type: 'string' }, - { flag: '--management-api-key-secret ', description: 'Management API key secret (Honeycomb, optional)', type: 'string' }, + { flag: '--management-api-key-id ', description: 'Management API key ID (Honeycomb)', type: 'string' }, + { flag: '--management-api-key-secret ', description: 'Management API key secret (Honeycomb)', type: 'string' }, { flag: '--api-token ', description: 'API token (Axiom / Better Stack global token)', type: 'string' }, { flag: '--uptime-api-token ', description: 'Uptime API token (Better Stack only)', type: 'string' }, { flag: '--telemetry-api-token ', description: 'Telemetry API token (Better Stack only)', type: 'string' }, @@ -719,7 +718,6 @@ export const integrationConnectCommand: Command = { 'polylane integration connect --category observability', 'polylane integration connect --category code-agent', 'polylane integration connect --type datadog --site us5.datadoghq.com --api-key ... --app-key ...', - 'polylane integration connect --type honeycomb --region us --api-key ...', 'polylane integration connect --type honeycomb --region us --api-key ... --management-api-key-id ... --management-api-key-secret ...', 'polylane integration connect --type axiom --region us-east-1 --api-token ...', 'polylane integration connect --type betterstack --api-token ... --uptime-api-token ... --telemetry-api-token ...', diff --git a/test/integration-connect-honeycomb.test.ts b/test/integration-connect-honeycomb.test.ts index 2d5e553..05caea5 100644 --- a/test/integration-connect-honeycomb.test.ts +++ b/test/integration-connect-honeycomb.test.ts @@ -1,6 +1,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { honeycombManagementKeyFields } from '../src/commands/integration/connect'; +import { CLIError } from '../src/errors/base'; describe('honeycombManagementKeyFields', () => { it('returns both fields when both are set', () => { @@ -10,15 +11,24 @@ describe('honeycombManagementKeyFields', () => { }); }); - it('returns nothing when both are empty', () => { - assert.deepEqual(honeycombManagementKeyFields('', ''), {}); + it('throws when both are empty', () => { + assert.throws( + () => honeycombManagementKeyFields('', ''), + (err: unknown) => err instanceof CLIError && err.message.includes('--management-api-key-id') + ); }); - it('returns nothing when only the ID is set', () => { - assert.deepEqual(honeycombManagementKeyFields('hcxik_id', ''), {}); + it('throws when only the ID is set', () => { + assert.throws( + () => honeycombManagementKeyFields('hcxik_id', ''), + (err: unknown) => err instanceof CLIError && err.message.includes('--management-api-key-secret') + ); }); - it('returns nothing when only the secret is set', () => { - assert.deepEqual(honeycombManagementKeyFields('', 'secret'), {}); + it('throws when only the secret is set', () => { + assert.throws( + () => honeycombManagementKeyFields('', 'secret'), + (err: unknown) => err instanceof CLIError && err.message.includes('--management-api-key-id') + ); }); }); From 37e2d3c22a9545b27f3d3d41ba282d4078f575ce Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 17:16:41 +0000 Subject: [PATCH 3/3] fix: fail loudly when the API drops the Honeycomb Management API key An API build that predates the management-key fields strips them from the connect body and stores the integration without them, silently discarding the user's key. The connect response echoes the stored metadata (the key ID survives redaction), so the CLI now verifies managementApiKeyId is present after a Honeycomb connect and exits non-zero with a disconnect-and-retry hint when it is not. Also from the same review round: the two management-key wizard steps share one step factory, and both key halves are trimmed on the flag path as well as the interactive path (whitespace-only values now count as missing). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vem7UpKJAXmFwtXrqkwreh --- src/commands/integration/connect.ts | 140 +++++++++++++-------- test/integration-connect-honeycomb.test.ts | 82 +++++++++++- 2 files changed, 169 insertions(+), 53 deletions(-) diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index c7ba41d..14dd6c5 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -16,6 +16,7 @@ import { choiceStep, secretStep, SKIPPED, + type WizardStep, } from '../helpers'; import type { Integration } from '../../generated/types'; import { CLIError } from '../../errors/base'; @@ -355,21 +356,73 @@ async function connectMcp( return 'connected'; } +const MANAGEMENT_KEY_PAIR_HINT = 'Pass both --management-api-key-id and --management-api-key-secret.'; + // The API stores the Management API key only as a pair, and both halves are -// required — a missing half is a usage error before any request is sent. +// required — a missing (or whitespace-only) half is a usage error before any +// request is sent. Both halves are trimmed here so the flag and prompt paths +// behave identically on pasted values. export function honeycombManagementKeyFields( managementApiKeyId: string, managementApiKeySecret: string ): { managementApiKeyId: string; managementApiKeySecret: string } { - if (!managementApiKeyId || !managementApiKeySecret) { - const missing = !managementApiKeyId ? '--management-api-key-id' : '--management-api-key-secret'; - throw new CLIError( - `Missing required flag: ${missing}`, - ExitCode.USAGE, - 'Pass both --management-api-key-id and --management-api-key-secret.' - ); + const id = managementApiKeyId.trim(); + const secret = managementApiKeySecret.trim(); + if (!id || !secret) { + const missing = !id ? '--management-api-key-id' : '--management-api-key-secret'; + throw new CLIError(`Missing required flag: ${missing}`, ExitCode.USAGE, MANAGEMENT_KEY_PAIR_HINT); } - return { managementApiKeyId, managementApiKeySecret }; + return { managementApiKeyId: id, managementApiKeySecret: secret }; +} + +// Both management-key steps share the same shape: a flag short-circuits, a +// non-interactive run without the flag is a usage error, and only the prompt +// differs. +function managementKeyStep( + config: Config, + args: Record, + key: 'managementApiKeyId' | 'managementApiKeySecret', + flag: string, + prompt: () => Promise, + set: (value: string) => void +): WizardStep { + return async () => { + const fromFlag = getArgString(args, key); + if (fromFlag !== undefined) { + set(fromFlag); + return SKIPPED; + } + if (!isInteractive(config.nonInteractive)) { + throw new CLIError(`Missing required flag: ${flag}`, ExitCode.USAGE, MANAGEMENT_KEY_PAIR_HINT); + } + const value = await prompt(); + if (value === BACK) return BACK; + set(value); + return; + }; +} + +// An API build that predates the management-key fields strips them from the +// connect body and stores the integration without them. The response echoes +// the stored metadata (the key ID survives redaction), so a missing ID there +// means the key was silently dropped — that must fail loudly, not read as a +// successful connect. The generated metadata type gains managementApiKeyId +// only when the matching API deploy lands, so the field is read through a +// structural cast, same trust boundary as the request-body spread. +export function assertHoneycombManagementKeyStored( + integration: Pick +): void { + const metadata = integration.metadata as { managementApiKeyId?: unknown } | null | undefined; + if (metadata != null && typeof metadata.managementApiKeyId === 'string' && metadata.managementApiKeyId !== '') { + return; + } + throw new CLIError( + 'The Polylane API does not support the Honeycomb Management API key yet — the key was not stored', + ExitCode.GENERAL, + 'Honeycomb was connected without query access. Disconnect it, then retry once the API is updated:\n' + + `polylane integration disconnect ${integration.id} --yes` + + (integration._html_url ? `\nor reconnect from the console: ${integration._html_url}` : '') + ); } // --- Credential-based connects: each wizard step can go back to the previous @@ -430,7 +483,6 @@ async function connectWithCredentials( let apiKey = ''; let managementApiKeyId = ''; let managementApiKeySecret = ''; - const managementSecretFlag = getArgString(args, 'managementApiKeySecret'); const ok = await runSteps([ choiceStep<'us' | 'eu'>( config, @@ -463,55 +515,38 @@ async function connectWithCredentials( apiKey = v; } ), - async () => { - const fromFlag = getArgString(args, 'managementApiKeyId'); - if (fromFlag !== undefined) { - managementApiKeyId = fromFlag; - return SKIPPED; - } - if (!isInteractive(config.nonInteractive)) { - throw new CLIError( - 'Missing required flag: --management-api-key-id', - ExitCode.USAGE, - 'Pass both --management-api-key-id and --management-api-key-secret.' - ); - } - note('Create one in your Honeycomb team settings under API Keys.', 'Management API key'); - const value = await promptTextOrBack( - { nonInteractive: config.nonInteractive }, - 'Management API key ID', - { validate: (v: string) => (v.trim() ? undefined : 'Required') } - ); - if (value === BACK) return BACK; - managementApiKeyId = value.trim(); - return; - }, - async () => { - if (managementSecretFlag !== undefined) { - managementApiKeySecret = managementSecretFlag; - return SKIPPED; + managementKeyStep( + config, + args, + 'managementApiKeyId', + '--management-api-key-id', + () => { + note('Create one in your Honeycomb team settings under API Keys.', 'Management API key'); + return promptTextOrBack({ nonInteractive: config.nonInteractive }, 'Management API key ID', { + validate: (v: string) => (v.trim() ? undefined : 'Required'), + }); + }, + (v) => { + managementApiKeyId = v; } - if (!isInteractive(config.nonInteractive)) { - throw new CLIError( - 'Missing required flag: --management-api-key-secret', - ExitCode.USAGE, - 'Pass both --management-api-key-id and --management-api-key-secret.' - ); + ), + managementKeyStep( + config, + args, + 'managementApiKeySecret', + '--management-api-key-secret', + () => promptPasswordOrBack({ nonInteractive: config.nonInteractive }, 'Management API key secret'), + (v) => { + managementApiKeySecret = v; } - const value = await promptPasswordOrBack( - { nonInteractive: config.nonInteractive }, - 'Management API key secret' - ); - if (value === BACK) return BACK; - managementApiKeySecret = value; - return; - }, + ), ]); if (!ok) return BACK; // Spread instead of literal fields: the client is generated from the live // prod spec, which gains these two fields only when the matching API // deploy lands. The spread keeps typecheck green on both sides; the old - // API strips unknown keys, the new one validates them. + // API strips unknown keys (caught after connect by + // assertHoneycombManagementKeyStored), the new one validates them. body = { type: 'honeycomb', workspaceId, @@ -638,6 +673,7 @@ async function connectWithCredentials( } const integration = await api.integrationsConnect(body); + if (type === 'honeycomb') assertHoneycombManagementKeyStored(integration); printConnectSuccess(config, integration, TYPE_OPTIONS.find((o) => o.value === type)?.label ?? type); if (isCodeAgentType(integration.type) && !config.quiet && config.output !== 'json') { process.stderr.write(`Connecting ${CODE_AGENTS[integration.type].name} makes it the default autofix executor.\n`); diff --git a/test/integration-connect-honeycomb.test.ts b/test/integration-connect-honeycomb.test.ts index 05caea5..c83a683 100644 --- a/test/integration-connect-honeycomb.test.ts +++ b/test/integration-connect-honeycomb.test.ts @@ -1,7 +1,8 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { honeycombManagementKeyFields } from '../src/commands/integration/connect'; +import { honeycombManagementKeyFields, assertHoneycombManagementKeyStored } from '../src/commands/integration/connect'; import { CLIError } from '../src/errors/base'; +import type { Integration } from '../src/generated/types'; describe('honeycombManagementKeyFields', () => { it('returns both fields when both are set', () => { @@ -11,6 +12,13 @@ describe('honeycombManagementKeyFields', () => { }); }); + it('trims surrounding whitespace from both fields', () => { + assert.deepEqual(honeycombManagementKeyFields(' hcxik_id\n', '\tsecret '), { + managementApiKeyId: 'hcxik_id', + managementApiKeySecret: 'secret', + }); + }); + it('throws when both are empty', () => { assert.throws( () => honeycombManagementKeyFields('', ''), @@ -18,6 +26,17 @@ describe('honeycombManagementKeyFields', () => { ); }); + it('treats whitespace-only values as missing', () => { + assert.throws( + () => honeycombManagementKeyFields(' ', 'secret'), + (err: unknown) => err instanceof CLIError && err.message.includes('--management-api-key-id') + ); + assert.throws( + () => honeycombManagementKeyFields('hcxik_id', ' \n'), + (err: unknown) => err instanceof CLIError && err.message.includes('--management-api-key-secret') + ); + }); + it('throws when only the ID is set', () => { assert.throws( () => honeycombManagementKeyFields('hcxik_id', ''), @@ -32,3 +51,64 @@ describe('honeycombManagementKeyFields', () => { ); }); }); + +type ConnectedIntegration = Pick; + +function honeycombIntegration(extra: Record, htmlUrl?: string): ConnectedIntegration { + return { + id: 'integration_test1', + metadata: { + type: 'honeycomb', + region: 'us', + apiKey: '', + teamSlug: 'team', + environmentSlug: 'env', + ...extra, + }, + ...(htmlUrl ? { _html_url: htmlUrl } : {}), + }; +} + +describe('assertHoneycombManagementKeyStored', () => { + it('passes when the response metadata carries the management key ID', () => { + assert.doesNotThrow(() => + assertHoneycombManagementKeyStored(honeycombIntegration({ managementApiKeyId: 'hcxik_id' })) + ); + }); + + it('throws when the API dropped the management key fields', () => { + assert.throws( + () => assertHoneycombManagementKeyStored(honeycombIntegration({})), + (err: unknown) => + err instanceof CLIError && + err.message === + 'The Polylane API does not support the Honeycomb Management API key yet — the key was not stored' && + err.hint !== undefined && + err.hint.includes('polylane integration disconnect integration_test1 --yes') + ); + }); + + it('throws when the stored management key ID is empty', () => { + assert.throws( + () => assertHoneycombManagementKeyStored(honeycombIntegration({ managementApiKeyId: '' })), + (err: unknown) => err instanceof CLIError + ); + }); + + it('throws when the response has no metadata', () => { + assert.throws( + () => assertHoneycombManagementKeyStored({ id: 'integration_test1', metadata: null }), + (err: unknown) => err instanceof CLIError + ); + }); + + it('includes the console link in the hint when the response carries one', () => { + assert.throws( + () => assertHoneycombManagementKeyStored(honeycombIntegration({}, 'https://console.example/integration_test1')), + (err: unknown) => + err instanceof CLIError && + err.hint !== undefined && + err.hint.includes('or reconnect from the console: https://console.example/integration_test1') + ); + }); +});