diff --git a/src/cli/operations/jobs/recommendation/__tests__/resolve-bundle-version.test.ts b/src/cli/operations/jobs/recommendation/__tests__/resolve-bundle-version.test.ts new file mode 100644 index 000000000..7ef5fc65b --- /dev/null +++ b/src/cli/operations/jobs/recommendation/__tests__/resolve-bundle-version.test.ts @@ -0,0 +1,38 @@ +import type { DeployedState } from '../../../../../schema'; +import { resolveBundleVersionId } from '../build-config'; +import { describe, expect, it } from 'vitest'; + +const BUNDLE_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:configuration-bundle/Proj-PromptV1-abc123'; +const VERSION_ID = 'a7be96aa-8a83-47b8-8d98-0829b14c1d9b'; + +const deployedState = { + targets: { + default: { + resources: { + configBundles: { + PromptV1: { bundleArn: BUNDLE_ARN, versionId: VERSION_ID }, + }, + }, + }, + }, +} as unknown as DeployedState; + +describe('resolveBundleVersionId', () => { + it("expands 'LATEST' to the deployed versionId", () => { + expect(resolveBundleVersionId(BUNDLE_ARN, 'LATEST', deployedState)).toBe(VERSION_ID); + }); + + it('returns an explicit version verbatim (never touches deployed state)', () => { + const explicit = '11111111-2222-3333-4444-555555555555'; + expect(resolveBundleVersionId(BUNDLE_ARN, explicit, deployedState)).toBe(explicit); + }); + + it("returns undefined when 'LATEST' cannot be resolved (bundle not deployed)", () => { + const unknownArn = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:configuration-bundle/Proj-Unknown-zzz999'; + expect(resolveBundleVersionId(unknownArn, 'LATEST', deployedState)).toBeUndefined(); + }); + + it('returns undefined for LATEST when there are no deployed targets', () => { + expect(resolveBundleVersionId(BUNDLE_ARN, 'LATEST', { targets: {} } as unknown as DeployedState)).toBeUndefined(); + }); +}); diff --git a/src/cli/operations/jobs/recommendation/build-config.ts b/src/cli/operations/jobs/recommendation/build-config.ts index 21dd00e71..7a12a72fd 100644 --- a/src/cli/operations/jobs/recommendation/build-config.ts +++ b/src/cli/operations/jobs/recommendation/build-config.ts @@ -75,6 +75,29 @@ export function resolveComponentKeyForJsonPath(key: string, deployedState: Deplo return key; } +/** + * Resolve a config-bundle version reference to a concrete version UUID. + * + * The recommendation API only accepts a concrete versionId — passing the literal 'LATEST' through + * yields a 400 (versionId fails the UUID pattern). When 'LATEST' is given, look the bundle up in + * deployed state (by ARN) and return its deployed versionId. An explicit version is returned + * verbatim. Returns undefined when 'LATEST' cannot be resolved (bundle not deployed) so the caller + * can surface a friendly error instead of sending 'LATEST' to the API. Mirrors the ab-test path's + * resolveConfigBundleVersion. + */ +export function resolveBundleVersionId( + bundleArn: string, + versionRef: string, + deployedState: DeployedState +): string | undefined { + if (versionRef !== 'LATEST') return versionRef; + for (const target of Object.values(deployedState.targets ?? {})) { + const bundle = Object.values(target.resources?.configBundles ?? {}).find(b => b.bundleArn === bundleArn); + if (bundle?.versionId) return bundle.versionId; + } + return undefined; +} + /** Flatten statusReasons + result errorCode/errorMessage into a single display string (FAILED only). */ export function extractFailureDetails(pollResult: { statusReasons?: string[]; diff --git a/src/cli/operations/jobs/recommendation/handler.ts b/src/cli/operations/jobs/recommendation/handler.ts index 8889b86df..84b51f0d6 100644 --- a/src/cli/operations/jobs/recommendation/handler.ts +++ b/src/cli/operations/jobs/recommendation/handler.ts @@ -27,6 +27,7 @@ import { buildRecommendationConfig, extractAccountIdFromArn, extractFailureDetails, + resolveBundleVersionId, resolveComponentKeyForJsonPath, resolveEvaluatorId, } from './build-config'; @@ -141,6 +142,7 @@ export const recommendationHandler: RecommendationHandler = { // Resolve config-bundle ARN + short JSONPath (from deployed state / agentcore.json) let bundleArn: string | undefined; + let resolvedBundleVersion = opts.bundleVersion; let resolvedSystemPromptJsonPath = opts.systemPromptJsonPath; if (opts.inputSource === 'config-bundle' && opts.bundleName) { if (opts.bundleName.startsWith('arn:')) { @@ -162,6 +164,22 @@ export const recommendationHandler: RecommendationHandler = { } } + // Expand '--bundle-version LATEST' to the deployed versionId. The recommendation API only + // accepts a concrete version UUID, so passing 'LATEST' through verbatim yields a 400. (The + // ab-test path resolves LATEST the same way; this mirrors it.) + if (resolvedBundleVersion === 'LATEST' && bundleArn) { + const versionId = resolveBundleVersionId(bundleArn, resolvedBundleVersion, deployedState); + if (!versionId) { + const err = new ResourceNotFoundError( + `Could not resolve version "LATEST" for config bundle "${opts.bundleName}". ` + + 'Run `agentcore deploy` first, or pass an explicit version UUID.' + ); + logger?.finalize(false); + return { success: false, error: err }; + } + resolvedBundleVersion = versionId; + } + if (resolvedSystemPromptJsonPath && !resolvedSystemPromptJsonPath.startsWith('$')) { const bundleName = opts.bundleName.startsWith('arn:') ? Object.values(deployedState.targets) @@ -186,7 +204,7 @@ export const recommendationHandler: RecommendationHandler = { type: opts.type, inlineContent, bundleArn, - bundleVersion: opts.bundleVersion, + bundleVersion: resolvedBundleVersion, systemPromptJsonPath: resolvedSystemPromptJsonPath, toolDescJsonPaths: opts.toolDescJsonPaths, inputSource: opts.inputSource, @@ -242,7 +260,7 @@ export const recommendationHandler: RecommendationHandler = { inputSource: opts.inputSource, bundleName: opts.bundleName, bundleArn, - bundleVersion: opts.bundleVersion, + bundleVersion: resolvedBundleVersion, systemPromptJsonPath: resolvedSystemPromptJsonPath, toolDescJsonPaths: opts.toolDescJsonPaths, ...(opts.kmsKeyArn ? { kmsKeyArn: opts.kmsKeyArn } : {}),