From a613fb6d165d44635266f4bd19aafacdf7f8e409 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Fri, 3 Apr 2026 13:43:00 -0400 Subject: [PATCH 1/5] Add generated Lex Runtime V2 client with integration tests --- .../tests/integration/__init__.py | 23 +++ .../test_bidirectional_streaming.py | 108 +++++++++++ .../tests/integration/test_non_streaming.py | 39 ++++ .../tests/setup_resources.py | 182 ++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py create mode 100644 clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py create mode 100644 clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py create mode 100644 clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py new file mode 100644 index 0000000..aac2b58 --- /dev/null +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py @@ -0,0 +1,23 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +from smithy_aws_core.identity import EnvironmentCredentialsResolver + +from aws_sdk_lex_runtime_v2.client import LexRuntimeV2Client +from aws_sdk_lex_runtime_v2.config import Config + +BOT_ID = os.environ["LEX_BOT_ID"] +BOT_ALIAS_ID = "TSTALIASID" +LOCALE_ID = "en_US" + + +def create_lex_client(region: str) -> LexRuntimeV2Client: + return LexRuntimeV2Client( + config=Config( + endpoint_uri=f"https://runtime-v2-lex.{region}.amazonaws.com", + region=region, + aws_credentials_identity_resolver=EnvironmentCredentialsResolver(), + ) + ) diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py new file mode 100644 index 0000000..eba8c01 --- /dev/null +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py @@ -0,0 +1,108 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test bidirectional streaming event stream handling.""" + +import asyncio +import uuid + +from smithy_core.aio.eventstream import DuplexEventStream + +from aws_sdk_lex_runtime_v2.models import ( + StartConversationInput, + StartConversationRequestEventStream, + StartConversationRequestEventStreamConfigurationEvent, + StartConversationRequestEventStreamTextInputEvent, + StartConversationRequestEventStreamDisconnectionEvent, + StartConversationResponseEventStream, + StartConversationResponseEventStreamTextResponseEvent, + StartConversationOutput, + ConfigurationEvent, + TextInputEvent, + DisconnectionEvent, +) +from . import BOT_ID, BOT_ALIAS_ID, LOCALE_ID, create_lex_client + + +async def _send_events( + stream: DuplexEventStream[ + StartConversationRequestEventStream, + StartConversationResponseEventStream, + StartConversationOutput, + ], +) -> None: + """Send configuration, text input, and disconnection events.""" + input_stream = stream.input_stream + + await input_stream.send( + StartConversationRequestEventStreamConfigurationEvent( + value=ConfigurationEvent(response_content_type="text/plain; charset=utf-8") + ) + ) + + await input_stream.send( + StartConversationRequestEventStreamTextInputEvent( + value=TextInputEvent(text="Hello") + ) + ) + + await asyncio.sleep(3) + + await input_stream.send( + StartConversationRequestEventStreamDisconnectionEvent( + value=DisconnectionEvent() + ) + ) + + await input_stream.close() + + +async def _receive_events( + stream: DuplexEventStream[ + StartConversationRequestEventStream, + StartConversationResponseEventStream, + StartConversationOutput, + ], +) -> tuple[bool, list[str]]: + """Receive and collect output from the stream. + + Returns: + Tuple of (got_text_response, messages) + """ + got_text_response = False + messages: list[str] = [] + + _, output_stream = await stream.await_output() + if output_stream is None: + return got_text_response, messages + + async for event in output_stream: + if isinstance(event, StartConversationResponseEventStreamTextResponseEvent): + got_text_response = True + if event.value.messages: + for msg in event.value.messages: + if msg.content: + messages.append(msg.content) + + return got_text_response, messages + + +async def test_start_conversation() -> None: + """Test bidirectional streaming StartConversation operation.""" + client = create_lex_client("us-east-1") + + stream = await client.start_conversation( + input=StartConversationInput( + bot_id=BOT_ID, + bot_alias_id=BOT_ALIAS_ID, + locale_id=LOCALE_ID, + session_id=str(uuid.uuid4()), + conversation_mode="TEXT", + ) + ) + + results = await asyncio.gather(_send_events(stream), _receive_events(stream)) + + got_text_response, messages = results[1] + assert got_text_response, "Expected to receive a TextResponse event" + assert len(messages) > 0, "Expected at least one message in the response" diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py new file mode 100644 index 0000000..2322b43 --- /dev/null +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py @@ -0,0 +1,39 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test non-streaming output type handling.""" + +import uuid + +from aws_sdk_lex_runtime_v2.models import RecognizeTextInput, RecognizeTextOutput +from . import BOT_ID, BOT_ALIAS_ID, LOCALE_ID, create_lex_client + + +async def test_recognize_text() -> None: + """Test non-streaming RecognizeText operation.""" + client = create_lex_client("us-east-1") + response = await client.recognize_text( + input=RecognizeTextInput( + bot_id=BOT_ID, + bot_alias_id=BOT_ALIAS_ID, + locale_id=LOCALE_ID, + session_id=str(uuid.uuid4()), + text="Hello", + ) + ) + + assert isinstance(response, RecognizeTextOutput) + assert response.session_id is not None + + # Verify session state with matched intent + assert response.session_state is not None + assert response.session_state.intent is not None + assert response.session_state.intent.name == "Greeting" + + # Verify interpretations contain the matched intent + assert response.interpretations is not None + assert len(response.interpretations) > 0 + + intent_names = [i.intent.name for i in response.interpretations if i.intent] + assert "Greeting" in intent_names + assert "FallbackIntent" in intent_names diff --git a/clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py b/clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py new file mode 100644 index 0000000..4144abb --- /dev/null +++ b/clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py @@ -0,0 +1,182 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "boto3", +# ] +# /// +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Setup script to create AWS resources for Lex Runtime V2 integration tests. + +Creates a simple Lex V2 bot with a Greeting intent for testing. + +Note: + This script is intended for local testing only and should not be used for + production setups. + +Usage: + uv run tests/setup_resources.py +""" + +import json +import time + +import boto3 + + +def create_lex_bot() -> tuple[str, str, str]: + """Create a simple Lex V2 bot for testing. + + Returns: + Tuple of (bot_id, bot_alias_id, locale_id) + """ + region = "us-east-1" + iam = boto3.client("iam") + lex = boto3.client("lexv2-models", region_name=region) + sts = boto3.client("sts") + + account_id = sts.get_caller_identity()["Account"] + role_name = "LexRuntimeV2IntegrationTestRole" + bot_name = "smithy-python-test-bot" + locale_id = "en_US" + + # Create IAM role for the bot + trust_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lexv2.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + + try: + iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps(trust_policy), + ) + except iam.exceptions.EntityAlreadyExistsException: + pass + + role_arn = f"arn:aws:iam::{account_id}:role/{role_name}" + + # Check if bot already exists + existing_bots = lex.list_bots( + filters=[{"name": "BotName", "values": [bot_name], "operator": "EQ"}] + ) + if existing_bots["botSummaries"]: + bot_id = existing_bots["botSummaries"][0]["botId"] + print(f"Bot already exists: {bot_id}") + else: + response = lex.create_bot( + botName=bot_name, + roleArn=role_arn, + dataPrivacy={"childDirected": False}, + idleSessionTTLInSeconds=300, + ) + bot_id = response["botId"] + print(f"Created bot: {bot_id}") + _wait_for_bot(lex, bot_id) + + # Ensure locale exists + try: + locale_resp = lex.describe_bot_locale( + botId=bot_id, botVersion="DRAFT", localeId=locale_id + ) + locale_status = locale_resp["botLocaleStatus"] + print(f"Locale status: {locale_status}") + except lex.exceptions.ResourceNotFoundException: + print("Creating locale...") + lex.create_bot_locale( + botId=bot_id, + botVersion="DRAFT", + localeId=locale_id, + nluIntentConfidenceThreshold=0.40, + ) + _wait_for_bot_locale(lex, bot_id, locale_id, target_status="NotBuilt") + locale_status = "NotBuilt" + + # Create intent and build locale if not already built + if locale_status != "Built": + intent_name = "Greeting" + existing_intents = lex.list_intents( + botId=bot_id, botVersion="DRAFT", localeId=locale_id, + filters=[{"name": "IntentName", "values": [intent_name], "operator": "EQ"}], + ) + if not existing_intents["intentSummaries"]: + print(f"Creating intent: {intent_name}") + lex.create_intent( + intentName=intent_name, + botId=bot_id, + botVersion="DRAFT", + localeId=locale_id, + sampleUtterances=[ + {"utterance": "Hello"}, + {"utterance": "Hi"}, + {"utterance": "Hey"}, + ], + intentClosingSetting={ + "closingResponse": { + "messageGroups": [ + { + "message": { + "plainTextMessage": { + "value": "Hello! How can I help you?" + } + } + } + ], + }, + "active": True, + }, + ) + + print("Building locale...") + lex.build_bot_locale( + botId=bot_id, botVersion="DRAFT", localeId=locale_id + ) + _wait_for_bot_locale(lex, bot_id, locale_id, target_status="Built") + + # Use TSTALIASID (test alias, always available) + bot_alias_id = "TSTALIASID" + + return bot_id, bot_alias_id, locale_id + + +def _wait_for_bot(lex, bot_id: str, timeout: int = 60) -> None: + for _ in range(timeout // 5): + response = lex.describe_bot(botId=bot_id) + status = response["botStatus"] + if status == "Available": + return + if status in ("Failed", "Deleting"): + raise RuntimeError(f"Bot creation failed with status: {status}") + time.sleep(5) + raise TimeoutError("Bot did not become available") + + +def _wait_for_bot_locale( + lex, bot_id: str, locale_id: str, target_status: str, timeout: int = 60 +) -> None: + for _ in range(timeout // 5): + response = lex.describe_bot_locale( + botId=bot_id, botVersion="DRAFT", localeId=locale_id + ) + status = response["botLocaleStatus"] + if status == target_status: + return + if status in ("Failed", "Deleting"): + raise RuntimeError(f"Bot locale failed with status: {status}") + time.sleep(5) + raise TimeoutError(f"Bot locale did not reach {target_status}") + + +if __name__ == "__main__": + bot_id, bot_alias_id, locale_id = create_lex_bot() + + print("\nSetup complete. Export this environment variable before running tests:") + print(f"export LEX_BOT_ID={bot_id}") From 8f6e5ec2fe05f34e2090b8d6499f9ad79bdfb449 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Fri, 3 Apr 2026 15:44:42 -0400 Subject: [PATCH 2/5] Add change entry for the new lex package --- ...untime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json diff --git a/clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json b/clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json new file mode 100644 index 0000000..11b706e --- /dev/null +++ b/clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json @@ -0,0 +1,4 @@ +{ + "type": "api-change", + "description": "Initial client release with support for current Amazon Lex Runtime V2 operations." +} \ No newline at end of file From 62f612408c5e2743faa5468d47f1fe0185e685a1 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Wed, 15 Apr 2026 17:41:37 -0400 Subject: [PATCH 3/5] fix: Address review comments and add conftest for test lifecycle --- ...ange-fa23e4248cb149cfacb71f203f455e83.json | 4 - .../_private/schemas.py | 8 + .../tests/integration/__init__.py | 4 +- .../tests/integration/conftest.py | 134 +++++++++++++ .../test_bidirectional_streaming.py | 78 ++++++-- .../tests/integration/test_non_streaming.py | 31 +-- .../tests/setup_resources.py | 182 ------------------ 7 files changed, 221 insertions(+), 220 deletions(-) delete mode 100644 clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json create mode 100644 clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py delete mode 100644 clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py diff --git a/clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json b/clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json deleted file mode 100644 index 11b706e..0000000 --- a/clients/aws-sdk-lex-runtime-v2/.changes/next-release/aws-sdk-lex-runtime-v2-api-change-fa23e4248cb149cfacb71f203f455e83.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "api-change", - "description": "Initial client release with support for current Amazon Lex Runtime V2 operations." -} \ No newline at end of file diff --git a/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py b/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py index 89cc5da..dcbbf27 100644 --- a/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py +++ b/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py @@ -2920,3 +2920,11 @@ target=ELICIT_SUB_SLOT, index=1, ) + +SLOT.members["values"] = Schema.member( + id=SLOT.id.with_member("values"), target=VALUES, index=2 +) + +SLOT.members["subSlots"] = Schema.member( + id=SLOT.id.with_member("subSlots"), target=SLOTS, index=3 +) diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py index aac2b58..0da32eb 100644 --- a/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/__init__.py @@ -1,16 +1,14 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import os - from smithy_aws_core.identity import EnvironmentCredentialsResolver from aws_sdk_lex_runtime_v2.client import LexRuntimeV2Client from aws_sdk_lex_runtime_v2.config import Config -BOT_ID = os.environ["LEX_BOT_ID"] BOT_ALIAS_ID = "TSTALIASID" LOCALE_ID = "en_US" +REGION = "us-east-1" def create_lex_client(region: str) -> LexRuntimeV2Client: diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py new file mode 100644 index 0000000..591447c --- /dev/null +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py @@ -0,0 +1,134 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Pytest fixtures for Lex Runtime V2 integration tests. + +Creates and tears down a Lex V2 bot with a Greeting intent once per +test session. All integration tests receive the bot_id via the +``lex_bot`` fixture. +""" + +import json + +import boto3 +import pytest + +from . import LOCALE_ID, REGION + +ROLE_NAME = "LexRuntimeV2IntegrationTestRole" +BOT_NAME = "smithy-python-integ-test-bot" + + +def _create_lex_bot() -> str: + """Create a Lex V2 bot with a Greeting intent. + + Returns: + The bot ID. + """ + iam = boto3.client("iam") + lex = boto3.client("lexv2-models", region_name=REGION) + sts = boto3.client("sts") + + account_id = sts.get_caller_identity()["Account"] + role_arn = f"arn:aws:iam::{account_id}:role/{ROLE_NAME}" + + # Create IAM role for the bot + trust_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lexv2.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + try: + iam.create_role( + RoleName=ROLE_NAME, AssumeRolePolicyDocument=json.dumps(trust_policy) + ) + except iam.exceptions.EntityAlreadyExistsException: + pass + + # Create bot + response = lex.create_bot( + botName=BOT_NAME, + roleArn=role_arn, + dataPrivacy={"childDirected": False}, + # 5-minute idle timeout is sufficient for integration tests. + idleSessionTTLInSeconds=300, + ) + bot_id = response["botId"] + lex.get_waiter("bot_available").wait(botId=bot_id) + + # Create locale + lex.create_bot_locale( + botId=bot_id, + botVersion="DRAFT", + localeId=LOCALE_ID, + # Required field. Confidence threshold (0-1) that determines when Lex + # inserts AMAZON.FallbackIntent into the interpretations list. + # 0.40 is a reasonable value for a simple test bot. + nluIntentConfidenceThreshold=0.40, + ) + lex.get_waiter("bot_locale_created").wait( + botId=bot_id, botVersion="DRAFT", localeId=LOCALE_ID + ) + + # Create intent + lex.create_intent( + intentName="Greeting", + botId=bot_id, + botVersion="DRAFT", + localeId=LOCALE_ID, + sampleUtterances=[ + {"utterance": "Hello"}, + {"utterance": "Hi"}, + {"utterance": "Hey"}, + ], + intentClosingSetting={ + "closingResponse": { + "messageGroups": [ + { + "message": { + "plainTextMessage": {"value": "Hello! How can I help you?"} + } + } + ] + }, + "active": True, + }, + ) + + # Build locale + lex.build_bot_locale(botId=bot_id, botVersion="DRAFT", localeId=LOCALE_ID) + lex.get_waiter("bot_locale_built").wait( + botId=bot_id, botVersion="DRAFT", localeId=LOCALE_ID + ) + + return bot_id + + +def _delete_lex_bot(bot_id: str) -> None: + """Delete a Lex V2 bot and its associated IAM role. + + Args: + bot_id: The bot ID to delete. + """ + lex = boto3.client("lexv2-models", region_name=REGION) + iam = boto3.client("iam") + + lex.delete_bot(botId=bot_id, skipResourceInUseCheck=True) + + try: + iam.delete_role(RoleName=ROLE_NAME) + except iam.exceptions.NoSuchEntityException: + pass + + +@pytest.fixture(scope="session") +def lex_bot(): + """Create a Lex bot for the test session and delete it after.""" + bot_id = _create_lex_bot() + yield bot_id + _delete_lex_bot(bot_id) diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py index eba8c01..fc12bea 100644 --- a/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_bidirectional_streaming.py @@ -15,13 +15,16 @@ StartConversationRequestEventStreamTextInputEvent, StartConversationRequestEventStreamDisconnectionEvent, StartConversationResponseEventStream, + StartConversationResponseEventStreamHeartbeatEvent, + StartConversationResponseEventStreamIntentResultEvent, StartConversationResponseEventStreamTextResponseEvent, + StartConversationResponseEventStreamTranscriptEvent, StartConversationOutput, ConfigurationEvent, TextInputEvent, DisconnectionEvent, ) -from . import BOT_ID, BOT_ALIAS_ID, LOCALE_ID, create_lex_client +from . import BOT_ALIAS_ID, LOCALE_ID, REGION, create_lex_client async def _send_events( @@ -63,37 +66,70 @@ async def _receive_events( StartConversationResponseEventStream, StartConversationOutput, ], -) -> tuple[bool, list[str]]: +) -> tuple[bool, bool, bool, bool]: """Receive and collect output from the stream. Returns: - Tuple of (got_text_response, messages) + Tuple of (got_transcript, got_intent_result, got_text_response, got_heartbeat) """ + got_transcript = False + got_intent_result = False got_text_response = False - messages: list[str] = [] + got_heartbeat = False _, output_stream = await stream.await_output() if output_stream is None: - return got_text_response, messages + return got_transcript, got_intent_result, got_text_response, got_heartbeat async for event in output_stream: - if isinstance(event, StartConversationResponseEventStreamTextResponseEvent): + if isinstance(event, StartConversationResponseEventStreamTranscriptEvent): + got_transcript = True + assert event.value.event_id is not None + assert event.value.transcript == "Hello" + elif isinstance(event, StartConversationResponseEventStreamIntentResultEvent): + got_intent_result = True + assert event.value.event_id is not None + assert event.value.input_mode == "Text" + assert event.value.session_id is not None + assert event.value.session_state is not None + assert event.value.session_state.intent is not None + assert event.value.session_state.intent.name == "Greeting" + assert event.value.session_state.intent.state == "Fulfilled" + assert event.value.interpretations is not None + assert len(event.value.interpretations) == 2 + interps_by_name = { + i.intent.name: i for i in event.value.interpretations if i.intent + } + assert "Greeting" in interps_by_name + assert "FallbackIntent" in interps_by_name + assert interps_by_name["Greeting"].nlu_confidence is not None + assert interps_by_name["Greeting"].nlu_confidence.score == 1.0 + elif isinstance(event, StartConversationResponseEventStreamTextResponseEvent): got_text_response = True - if event.value.messages: - for msg in event.value.messages: - if msg.content: - messages.append(msg.content) - - return got_text_response, messages - - -async def test_start_conversation() -> None: + assert event.value.event_id is not None + assert event.value.messages is not None + assert len(event.value.messages) == 1 + msg = event.value.messages[0] + assert msg.content_type == "PlainText" + assert msg.content == "Hello! How can I help you?" + elif isinstance(event, StartConversationResponseEventStreamHeartbeatEvent): + got_heartbeat = True + assert event.value.event_id is not None + else: + raise RuntimeError( + f"Received unexpected event type in stream: {type(event).__name__}" + ) + + return got_transcript, got_intent_result, got_text_response, got_heartbeat + + +async def test_start_conversation(lex_bot: str) -> None: """Test bidirectional streaming StartConversation operation.""" - client = create_lex_client("us-east-1") + client = create_lex_client(REGION) stream = await client.start_conversation( input=StartConversationInput( - bot_id=BOT_ID, + bot_id=lex_bot, bot_alias_id=BOT_ALIAS_ID, locale_id=LOCALE_ID, session_id=str(uuid.uuid4()), @@ -103,6 +139,8 @@ async def test_start_conversation() -> None: results = await asyncio.gather(_send_events(stream), _receive_events(stream)) - got_text_response, messages = results[1] - assert got_text_response, "Expected to receive a TextResponse event" - assert len(messages) > 0, "Expected at least one message in the response" + got_transcript, got_intent_result, got_text_response, got_heartbeat = results[1] + assert got_transcript, "Expected to receive a TranscriptEvent" + assert got_intent_result, "Expected to receive an IntentResultEvent" + assert got_text_response, "Expected to receive a TextResponseEvent" + assert got_heartbeat, "Expected to receive a HeartbeatEvent" diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py index 2322b43..d3abd05 100644 --- a/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/test_non_streaming.py @@ -6,15 +6,15 @@ import uuid from aws_sdk_lex_runtime_v2.models import RecognizeTextInput, RecognizeTextOutput -from . import BOT_ID, BOT_ALIAS_ID, LOCALE_ID, create_lex_client +from . import BOT_ALIAS_ID, LOCALE_ID, REGION, create_lex_client -async def test_recognize_text() -> None: +async def test_recognize_text(lex_bot: str) -> None: """Test non-streaming RecognizeText operation.""" - client = create_lex_client("us-east-1") + client = create_lex_client(REGION) response = await client.recognize_text( input=RecognizeTextInput( - bot_id=BOT_ID, + bot_id=lex_bot, bot_alias_id=BOT_ALIAS_ID, locale_id=LOCALE_ID, session_id=str(uuid.uuid4()), @@ -25,15 +25,24 @@ async def test_recognize_text() -> None: assert isinstance(response, RecognizeTextOutput) assert response.session_id is not None - # Verify session state with matched intent + # Verify messages + assert response.messages is not None + assert len(response.messages) == 1 + msg = response.messages[0] + assert msg.content_type == "PlainText" + assert msg.content == "Hello! How can I help you?" + + # Verify session state assert response.session_state is not None assert response.session_state.intent is not None assert response.session_state.intent.name == "Greeting" + assert response.session_state.intent.state == "Fulfilled" - # Verify interpretations contain the matched intent + # Verify interpretations assert response.interpretations is not None - assert len(response.interpretations) > 0 - - intent_names = [i.intent.name for i in response.interpretations if i.intent] - assert "Greeting" in intent_names - assert "FallbackIntent" in intent_names + assert len(response.interpretations) == 2 + interps_by_name = {i.intent.name: i for i in response.interpretations if i.intent} + assert "Greeting" in interps_by_name + assert "FallbackIntent" in interps_by_name + assert interps_by_name["Greeting"].nlu_confidence is not None + assert interps_by_name["Greeting"].nlu_confidence.score == 1.0 diff --git a/clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py b/clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py deleted file mode 100644 index 4144abb..0000000 --- a/clients/aws-sdk-lex-runtime-v2/tests/setup_resources.py +++ /dev/null @@ -1,182 +0,0 @@ -# /// script -# requires-python = ">=3.12" -# dependencies = [ -# "boto3", -# ] -# /// -# -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Setup script to create AWS resources for Lex Runtime V2 integration tests. - -Creates a simple Lex V2 bot with a Greeting intent for testing. - -Note: - This script is intended for local testing only and should not be used for - production setups. - -Usage: - uv run tests/setup_resources.py -""" - -import json -import time - -import boto3 - - -def create_lex_bot() -> tuple[str, str, str]: - """Create a simple Lex V2 bot for testing. - - Returns: - Tuple of (bot_id, bot_alias_id, locale_id) - """ - region = "us-east-1" - iam = boto3.client("iam") - lex = boto3.client("lexv2-models", region_name=region) - sts = boto3.client("sts") - - account_id = sts.get_caller_identity()["Account"] - role_name = "LexRuntimeV2IntegrationTestRole" - bot_name = "smithy-python-test-bot" - locale_id = "en_US" - - # Create IAM role for the bot - trust_policy = { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": {"Service": "lexv2.amazonaws.com"}, - "Action": "sts:AssumeRole", - } - ], - } - - try: - iam.create_role( - RoleName=role_name, - AssumeRolePolicyDocument=json.dumps(trust_policy), - ) - except iam.exceptions.EntityAlreadyExistsException: - pass - - role_arn = f"arn:aws:iam::{account_id}:role/{role_name}" - - # Check if bot already exists - existing_bots = lex.list_bots( - filters=[{"name": "BotName", "values": [bot_name], "operator": "EQ"}] - ) - if existing_bots["botSummaries"]: - bot_id = existing_bots["botSummaries"][0]["botId"] - print(f"Bot already exists: {bot_id}") - else: - response = lex.create_bot( - botName=bot_name, - roleArn=role_arn, - dataPrivacy={"childDirected": False}, - idleSessionTTLInSeconds=300, - ) - bot_id = response["botId"] - print(f"Created bot: {bot_id}") - _wait_for_bot(lex, bot_id) - - # Ensure locale exists - try: - locale_resp = lex.describe_bot_locale( - botId=bot_id, botVersion="DRAFT", localeId=locale_id - ) - locale_status = locale_resp["botLocaleStatus"] - print(f"Locale status: {locale_status}") - except lex.exceptions.ResourceNotFoundException: - print("Creating locale...") - lex.create_bot_locale( - botId=bot_id, - botVersion="DRAFT", - localeId=locale_id, - nluIntentConfidenceThreshold=0.40, - ) - _wait_for_bot_locale(lex, bot_id, locale_id, target_status="NotBuilt") - locale_status = "NotBuilt" - - # Create intent and build locale if not already built - if locale_status != "Built": - intent_name = "Greeting" - existing_intents = lex.list_intents( - botId=bot_id, botVersion="DRAFT", localeId=locale_id, - filters=[{"name": "IntentName", "values": [intent_name], "operator": "EQ"}], - ) - if not existing_intents["intentSummaries"]: - print(f"Creating intent: {intent_name}") - lex.create_intent( - intentName=intent_name, - botId=bot_id, - botVersion="DRAFT", - localeId=locale_id, - sampleUtterances=[ - {"utterance": "Hello"}, - {"utterance": "Hi"}, - {"utterance": "Hey"}, - ], - intentClosingSetting={ - "closingResponse": { - "messageGroups": [ - { - "message": { - "plainTextMessage": { - "value": "Hello! How can I help you?" - } - } - } - ], - }, - "active": True, - }, - ) - - print("Building locale...") - lex.build_bot_locale( - botId=bot_id, botVersion="DRAFT", localeId=locale_id - ) - _wait_for_bot_locale(lex, bot_id, locale_id, target_status="Built") - - # Use TSTALIASID (test alias, always available) - bot_alias_id = "TSTALIASID" - - return bot_id, bot_alias_id, locale_id - - -def _wait_for_bot(lex, bot_id: str, timeout: int = 60) -> None: - for _ in range(timeout // 5): - response = lex.describe_bot(botId=bot_id) - status = response["botStatus"] - if status == "Available": - return - if status in ("Failed", "Deleting"): - raise RuntimeError(f"Bot creation failed with status: {status}") - time.sleep(5) - raise TimeoutError("Bot did not become available") - - -def _wait_for_bot_locale( - lex, bot_id: str, locale_id: str, target_status: str, timeout: int = 60 -) -> None: - for _ in range(timeout // 5): - response = lex.describe_bot_locale( - botId=bot_id, botVersion="DRAFT", localeId=locale_id - ) - status = response["botLocaleStatus"] - if status == target_status: - return - if status in ("Failed", "Deleting"): - raise RuntimeError(f"Bot locale failed with status: {status}") - time.sleep(5) - raise TimeoutError(f"Bot locale did not reach {target_status}") - - -if __name__ == "__main__": - bot_id, bot_alias_id, locale_id = create_lex_bot() - - print("\nSetup complete. Export this environment variable before running tests:") - print(f"export LEX_BOT_ID={bot_id}") From 89c911a69eaa20c749907c34ce8fa4b0dd03c129 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Wed, 22 Apr 2026 20:07:54 -0400 Subject: [PATCH 4/5] Add unique suffix to resource names to avoid race condition in parallel runs --- .../src/aws_sdk_lex_runtime_v2/_private/schemas.py | 8 -------- .../aws-sdk-lex-runtime-v2/tests/integration/conftest.py | 6 ++++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py b/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py index dcbbf27..89cc5da 100644 --- a/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py +++ b/clients/aws-sdk-lex-runtime-v2/src/aws_sdk_lex_runtime_v2/_private/schemas.py @@ -2920,11 +2920,3 @@ target=ELICIT_SUB_SLOT, index=1, ) - -SLOT.members["values"] = Schema.member( - id=SLOT.id.with_member("values"), target=VALUES, index=2 -) - -SLOT.members["subSlots"] = Schema.member( - id=SLOT.id.with_member("subSlots"), target=SLOTS, index=3 -) diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py index 591447c..28e77c4 100644 --- a/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py @@ -9,14 +9,16 @@ """ import json +import uuid import boto3 import pytest from . import LOCALE_ID, REGION -ROLE_NAME = "LexRuntimeV2IntegrationTestRole" -BOT_NAME = "smithy-python-integ-test-bot" +_UNIQUE_SUFFIX = uuid.uuid4().hex +ROLE_NAME = f"LexRuntimeV2IntegTestRole-{_UNIQUE_SUFFIX}" +BOT_NAME = f"smithy-python-integ-test-bot-{_UNIQUE_SUFFIX}" def _create_lex_bot() -> str: From 0344db7b900e3c60d16870ac72736edf84f3db0d Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Fri, 1 May 2026 15:59:21 -0400 Subject: [PATCH 5/5] Ensure fixture cleanup on partial setup failure --- .../tests/integration/conftest.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py b/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py index 28e77c4..8a2407c 100644 --- a/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py +++ b/clients/aws-sdk-lex-runtime-v2/tests/integration/conftest.py @@ -111,16 +111,17 @@ def _create_lex_bot() -> str: return bot_id -def _delete_lex_bot(bot_id: str) -> None: +def _delete_lex_bot(bot_id: str | None) -> None: """Delete a Lex V2 bot and its associated IAM role. Args: - bot_id: The bot ID to delete. + bot_id: The bot ID to delete, or None if creation failed. """ lex = boto3.client("lexv2-models", region_name=REGION) iam = boto3.client("iam") - lex.delete_bot(botId=bot_id, skipResourceInUseCheck=True) + if bot_id: + lex.delete_bot(botId=bot_id, skipResourceInUseCheck=True) try: iam.delete_role(RoleName=ROLE_NAME) @@ -131,6 +132,9 @@ def _delete_lex_bot(bot_id: str) -> None: @pytest.fixture(scope="session") def lex_bot(): """Create a Lex bot for the test session and delete it after.""" - bot_id = _create_lex_bot() - yield bot_id - _delete_lex_bot(bot_id) + bot_id = None + try: + bot_id = _create_lex_bot() + yield bot_id + finally: + _delete_lex_bot(bot_id)