From fb577ce3d19ef6adc56ca032cffa84da057b22af Mon Sep 17 00:00:00 2001 From: Syed Jafri Date: Wed, 15 Jul 2026 12:09:59 -0700 Subject: [PATCH 1/4] cleanup: use a single logger in base_trainer --- sagemaker-train/src/sagemaker/train/base_trainer.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sagemaker-train/src/sagemaker/train/base_trainer.py b/sagemaker-train/src/sagemaker/train/base_trainer.py index da94cc0982..c17199a611 100644 --- a/sagemaker-train/src/sagemaker/train/base_trainer.py +++ b/sagemaker-train/src/sagemaker/train/base_trainer.py @@ -34,6 +34,9 @@ from sagemaker.train.defaults import TrainDefaults from sagemaker.train.utils import _get_unique_name +logger = logging.getLogger(__name__) + + class BaseTrainer(ABC): """Abstract base class for all SageMaker training workflows. @@ -156,7 +159,6 @@ def _fetch_full_recipe_template(self) -> Optional[Dict[str, Any]]: Returns None if the template can't be fetched (fallback to synthetic template). """ - logger = logging.getLogger(__name__) frt = getattr(self.hyperparameters, '_full_recipe_template', None) if hasattr(self, 'hyperparameters') else None if isinstance(frt, dict): return frt @@ -392,8 +394,6 @@ def _train_serverful_smtj(self, training_dataset=None, validation_dataset=None, ) from sagemaker.train.defaults import TrainDefaults - logger = logging.getLogger(__name__) - sagemaker_session = TrainDefaults.get_sagemaker_session( sagemaker_session=self.sagemaker_session ) @@ -848,8 +848,6 @@ def _train_hyperpod(self, training_dataset=None, validation_dataset=None, (SFT, DPO, RLVR). """ - logger = logging.getLogger(__name__) - sagemaker_session = TrainDefaults.get_sagemaker_session( sagemaker_session=self.sagemaker_session ) From 706e73e7c9dd8172fdb2a4ab63a014d3ae021cfb Mon Sep 17 00:00:00 2001 From: Syed Jafri Date: Wed, 15 Jul 2026 13:42:39 -0700 Subject: [PATCH 2/4] fix: update recipe resolver unit tests --- .../train/test_trainer_recipe_integration.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/sagemaker-train/tests/unit/train/test_trainer_recipe_integration.py b/sagemaker-train/tests/unit/train/test_trainer_recipe_integration.py index bb873a6b8b..97a754b24d 100644 --- a/sagemaker-train/tests/unit/train/test_trainer_recipe_integration.py +++ b/sagemaker-train/tests/unit/train/test_trainer_recipe_integration.py @@ -611,6 +611,9 @@ def test_non_spec_keys_flow_into_train_hyperparameters( overrides={"training_config": {"sequence_length": 8192}}, ) + # For serverless (ie compute=None): Non-spec keys that aren't overridden don't flow to hyperparams + trainer.compute = MagicMock() + with patch("sagemaker.train.sft_trainer.TrainingJob") as mock_tj, \ patch("sagemaker.train.sft_trainer.TrainDefaults") as mock_defaults, \ patch("sagemaker.train.sft_trainer._create_input_data_config") as mock_input, \ @@ -640,6 +643,54 @@ def test_non_spec_keys_flow_into_train_hyperparameters( assert "warmup_ratio" in final_hp assert final_hp["warmup_ratio"] == "0.1" + @patch("sagemaker.train.sft_trainer._validate_eula_for_gated_model", return_value=False) + @patch("sagemaker.train.sft_trainer._get_fine_tuning_options_and_model_arn") + @patch("sagemaker.train.sft_trainer._validate_and_resolve_model_package_group", return_value="my-group") + @patch("sagemaker.train.sft_trainer._resolve_model_and_name", return_value=("model_obj", "nova-lite-v2")) + def test_serverless_non_spec_keys_dont_flow_into_train_hyperparameters( + self, mock_resolve, mock_validate_group, mock_get_options, mock_eula, + mock_hyperparams_with_full_template + ): + """Non-spec keys from full template are included in final training hyperparameters.""" + mock_get_options.return_value = (mock_hyperparams_with_full_template, "model-arn", False) + + from sagemaker.train.sft_trainer import SFTTrainer + + trainer = SFTTrainer( + model="nova-lite-v2", + model_package_group="my-group", + training_dataset="s3://bucket/train.jsonl", + overrides={"training_config": {"sequence_length": 8191}}, + ) + + with patch("sagemaker.train.sft_trainer.TrainingJob") as mock_tj, \ + patch("sagemaker.train.sft_trainer.TrainDefaults") as mock_defaults, \ + patch("sagemaker.train.sft_trainer._create_input_data_config") as mock_input, \ + patch("sagemaker.train.sft_trainer._convert_input_data_to_channels", return_value=[]), \ + patch("sagemaker.train.sft_trainer._create_output_config", return_value=MagicMock()), \ + patch("sagemaker.train.sft_trainer._create_serverless_config", return_value=MagicMock()), \ + patch("sagemaker.train.sft_trainer._create_mlflow_config", return_value=None), \ + patch("sagemaker.train.sft_trainer._create_model_package_config", return_value=None), \ + patch("sagemaker.train.sft_trainer._validate_hyperparameter_values"), \ + patch("sagemaker.train.sft_trainer._get_jumpstart_tags", return_value=[]): + + mock_session = MagicMock() + mock_session.boto_session.region_name = "us-west-2" + mock_defaults.get_sagemaker_session.return_value = mock_session + mock_defaults.get_role.return_value = "arn:aws:iam::123:role/Role" + mock_tj.create.return_value = MagicMock() + + trainer.train(wait=False) + + call_kwargs = mock_tj.create.call_args[1] + final_hp = call_kwargs["hyper_parameters"] + + # Non-spec key is now in final hyperparameters + assert "sequence_length" in final_hp + assert final_hp["sequence_length"] == "8191" + # Other full template keys also present + assert "warmup_ratio" not in final_hp + @patch("sagemaker.train.sft_trainer._validate_eula_for_gated_model", return_value=False) @patch("sagemaker.train.sft_trainer._get_fine_tuning_options_and_model_arn") @patch("sagemaker.train.sft_trainer._validate_and_resolve_model_package_group", return_value="my-group") @@ -660,6 +711,9 @@ def test_nested_keys_flow_into_train_hyperparameters( overrides={"training_config": {"lr_scheduler": {"warmup_steps": 30}}}, ) + # For serverless (ie compute=None): Non-spec keys that aren't overridden don't flow to hyperparams + trainer.compute = MagicMock() + with patch("sagemaker.train.sft_trainer.TrainingJob") as mock_tj, \ patch("sagemaker.train.sft_trainer.TrainDefaults") as mock_defaults, \ patch("sagemaker.train.sft_trainer._create_input_data_config") as mock_input, \ @@ -725,6 +779,9 @@ def test_deeply_nested_peft_keys_flow_into_hyperparameters( overrides={"training_config": {"peft": {"lora_tuning": {"alpha": 128}}}}, ) + # For serverless (ie compute=None): Non-spec keys that aren't overridden don't flow to hyperparams + trainer.compute = MagicMock() + with patch("sagemaker.train.sft_trainer.TrainingJob") as mock_tj, \ patch("sagemaker.train.sft_trainer.TrainDefaults") as mock_defaults, \ patch("sagemaker.train.sft_trainer._create_input_data_config"), \ From f73e7299edb8f9ddbc16ff5ffaf4916be62dba15 Mon Sep 17 00:00:00 2001 From: Syed Jafri Date: Wed, 15 Jul 2026 16:01:36 -0700 Subject: [PATCH 3/4] Release 3.16.0 (2026-07-15): Bump VERSION files and internal dependency pins to 3.16.0 / 2.16.0 / 1.16.0. Update CHANGELOGs across root and submodules (core, train, serve, mlops). --- CHANGELOG.md | 18 ++++++++++++++++++ VERSION | 2 +- pyproject.toml | 8 ++++---- sagemaker-core/CHANGELOG.md | 11 +++++++++++ sagemaker-core/VERSION | 2 +- sagemaker-mlops/CHANGELOG.md | 6 ++++++ sagemaker-mlops/VERSION | 2 +- sagemaker-mlops/pyproject.toml | 6 +++--- sagemaker-serve/CHANGELOG.md | 6 ++++++ sagemaker-serve/VERSION | 2 +- sagemaker-serve/pyproject.toml | 4 ++-- sagemaker-train/CHANGELOG.md | 7 +++++++ sagemaker-train/VERSION | 2 +- sagemaker-train/pyproject.toml | 2 +- 14 files changed, 63 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b98007772..31c5c5e97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,22 @@ # Changelog +## v3.16.0 (2026-07-15) + +### New Features + +- feat: actionable guidance for removed v2 interfaces (#6004) +- feat(serve): add SageMaker GenAI inference benchmarking and recommendation (#5874) +- feat(feature-store): add BatchWriteRecord and ListRecords to FeatureGroup (#5983) + +### Bug Fixes + +- fix(iam): scope repo-level ECR actions to prevent false deny in preflight validation (#6024) +- fix: filter full recipe template from serverless train() (#6021) +- Fix sm-train unit tests + use single logger in base trainer (#6030) + +### Tests + +- test(mlops): Skip non-PEP440 version keys in sklearn_latest_version (#6022) + ## v3.15.1 (2026-07-09) ### New Features diff --git a/VERSION b/VERSION index c3df54c9b8..1eeac129c5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.15.1 +3.16.0 diff --git a/pyproject.toml b/pyproject.toml index df12568933..43146b2025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,10 +32,10 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "sagemaker-core>=2.15.1,<3.0.0", - "sagemaker-train>=1.15.1,<2.0.0", - "sagemaker-serve>=1.15.1,<2.0.0", - "sagemaker-mlops>=1.15.1,<2.0.0", + "sagemaker-core>=2.16.0,<3.0.0", + "sagemaker-train>=1.16.0,<2.0.0", + "sagemaker-serve>=1.16.0,<2.0.0", + "sagemaker-mlops>=1.16.0,<2.0.0", ] [project.optional-dependencies] diff --git a/sagemaker-core/CHANGELOG.md b/sagemaker-core/CHANGELOG.md index e8dde0d475..ce1d2e0044 100644 --- a/sagemaker-core/CHANGELOG.md +++ b/sagemaker-core/CHANGELOG.md @@ -1,4 +1,15 @@ # Changelog +## v2.16.0 (2026-07-15) + +### New Features + +- feat: actionable guidance for removed v2 interfaces (#6004) +- feat(feature-store): add BatchWriteRecord and ListRecords to FeatureGroup (#5983) + +### Bug Fixes + +- fix(iam): scope repo-level ECR actions to prevent false deny in preflight validation (#6024) + ## v2.15.1 (2026-07-09) ### New Features diff --git a/sagemaker-core/VERSION b/sagemaker-core/VERSION index 3b1fc7950f..7524906967 100644 --- a/sagemaker-core/VERSION +++ b/sagemaker-core/VERSION @@ -1 +1 @@ -2.15.1 +2.16.0 diff --git a/sagemaker-mlops/CHANGELOG.md b/sagemaker-mlops/CHANGELOG.md index 7b2e32960c..86c3678ff1 100644 --- a/sagemaker-mlops/CHANGELOG.md +++ b/sagemaker-mlops/CHANGELOG.md @@ -1,4 +1,10 @@ # Changelog +## v1.16.0 (2026-07-15) + +### Tests + +- test(mlops): Skip non-PEP440 version keys in sklearn_latest_version (#6022) + ## v1.15.1 (2026-07-09) ### New Features diff --git a/sagemaker-mlops/VERSION b/sagemaker-mlops/VERSION index ace44233b4..15b989e398 100644 --- a/sagemaker-mlops/VERSION +++ b/sagemaker-mlops/VERSION @@ -1 +1 @@ -1.15.1 +1.16.0 diff --git a/sagemaker-mlops/pyproject.toml b/sagemaker-mlops/pyproject.toml index 5581f55b1d..c9240d7564 100644 --- a/sagemaker-mlops/pyproject.toml +++ b/sagemaker-mlops/pyproject.toml @@ -22,9 +22,9 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "sagemaker-core>=2.15.1", - "sagemaker-train>=1.15.1", - "sagemaker-serve>=1.15.1", + "sagemaker-core>=2.16.0", + "sagemaker-train>=1.16.0", + "sagemaker-serve>=1.16.0", "cryptography>=46.0.0", "boto3>=1.42.2,<2.0", "botocore>=1.42.2,<2.0", diff --git a/sagemaker-serve/CHANGELOG.md b/sagemaker-serve/CHANGELOG.md index 1cd9f54bbc..1a0b946954 100644 --- a/sagemaker-serve/CHANGELOG.md +++ b/sagemaker-serve/CHANGELOG.md @@ -1,4 +1,10 @@ # Changelog +## v1.16.0 (2026-07-15) + +### New Features + +- feat(serve): add SageMaker GenAI inference benchmarking and recommendation (#5874) + ## v1.15.1 (2026-07-09) ### New Features diff --git a/sagemaker-serve/VERSION b/sagemaker-serve/VERSION index ace44233b4..15b989e398 100644 --- a/sagemaker-serve/VERSION +++ b/sagemaker-serve/VERSION @@ -1 +1 @@ -1.15.1 +1.16.0 diff --git a/sagemaker-serve/pyproject.toml b/sagemaker-serve/pyproject.toml index 36d3db6eae..b5606cc20d 100644 --- a/sagemaker-serve/pyproject.toml +++ b/sagemaker-serve/pyproject.toml @@ -22,8 +22,8 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "sagemaker-core>=2.15.1", - "sagemaker-train>=1.15.1", + "sagemaker-core>=2.16.0", + "sagemaker-train>=1.16.0", "boto3>=1.42.2,<2.0", "botocore>=1.35.75,<2.0", "deepdiff", diff --git a/sagemaker-train/CHANGELOG.md b/sagemaker-train/CHANGELOG.md index 1916318886..e8eba09a27 100644 --- a/sagemaker-train/CHANGELOG.md +++ b/sagemaker-train/CHANGELOG.md @@ -1,4 +1,11 @@ # Changelog +## v1.16.0 (2026-07-15) + +### Bug Fixes + +- fix: filter full recipe template from serverless train() (#6021) +- Fix sm-train unit tests + use single logger in base trainer (#6030) + ## v1.15.1 (2026-07-09) ### New Features diff --git a/sagemaker-train/VERSION b/sagemaker-train/VERSION index ace44233b4..15b989e398 100644 --- a/sagemaker-train/VERSION +++ b/sagemaker-train/VERSION @@ -1 +1 @@ -1.15.1 +1.16.0 diff --git a/sagemaker-train/pyproject.toml b/sagemaker-train/pyproject.toml index 6c792ab8fb..2a1c2b4cfb 100644 --- a/sagemaker-train/pyproject.toml +++ b/sagemaker-train/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "sagemaker-core>=2.15.1", + "sagemaker-core>=2.16.0", "graphene>=3,<4", "typing_extensions>=4.9.0", "tblib>=1.7.0", From cfd34e8b19ab4a57002aa52b06a9fc469a3c150f Mon Sep 17 00:00:00 2001 From: Syed Jafri Date: Tue, 21 Jul 2026 15:09:49 -0700 Subject: [PATCH 4/4] fix: use absolute path in data mixing recipe path construction --- .../train/common_utils/data_mixing_utils.py | 21 ++++------ ..._hyperpod.py => test_nova_sft_hyperpod.py} | 0 .../train/test_sft_data_mixing_hyperpod.py | 40 ++++++++++++++++--- .../common_utils/test_data_mixing_utils.py | 22 ++++++---- 4 files changed, 58 insertions(+), 25 deletions(-) rename sagemaker-train/tests/integ/train/{test_nova_hyperpod.py => test_nova_sft_hyperpod.py} (100%) diff --git a/sagemaker-train/src/sagemaker/train/common_utils/data_mixing_utils.py b/sagemaker-train/src/sagemaker/train/common_utils/data_mixing_utils.py index 2fb52f2995..0f91c9425f 100644 --- a/sagemaker-train/src/sagemaker/train/common_utils/data_mixing_utils.py +++ b/sagemaker-train/src/sagemaker/train/common_utils/data_mixing_utils.py @@ -480,16 +480,18 @@ def build_hyperpod_datamix_recipe_from_context( 4. Inject customer_data_percent and nova_data_percentages into data_mixing.sources 5. Validate non-zero categories exist in template's nova_data section 6. Write final YAML to HyperPod CLI recipes directory - 7. Return (relative_recipe_path, image_uri) + 7. Return (recipe_path, image_uri) Args: context: The HyperPodTemplateContext from resolve_hyperpod_datamix_context. validated_config: A DataMixingConfig validated via validate_data_mixing_categories. Returns: - Tuple of (relative_recipe_path, image_uri). relative_recipe_path is relative - to the HyperPod CLI recipes_collection/recipes directory with .yaml extension - removed. image_uri is the container image URI from the context (or None). + Tuple of (recipe_path, image_uri). recipe_path is the absolute filesystem + path (including the .yaml extension) of the generated recipe written under + the HyperPod CLI recipes_collection/recipes directory; it is consumed by the + recipe resolver as a user recipe file. image_uri is the container image URI + from the context (or None). Raises: RuntimeError: If hyperpod_cli is not installed. @@ -652,18 +654,11 @@ def _apply_overrides(recipe: dict, overrides: dict) -> dict: with open(recipe_path, "w") as f: f.write(recipe_output) - relative_path = ( - recipe_path.split(HYPERPOD_RECIPE_PATH, 1)[1] - .lstrip("/").lstrip("\\") - .removesuffix(".yaml") - ) - logger.info( - "Generated HyperPod datamix recipe at '%s' (relative: '%s') from context '%s'.", + "Generated HyperPod datamix recipe at '%s' from context '%s'.", recipe_path, - relative_path, context.recipe_name, ) - return relative_path, context.image_uri + return recipe_path, context.image_uri diff --git a/sagemaker-train/tests/integ/train/test_nova_hyperpod.py b/sagemaker-train/tests/integ/train/test_nova_sft_hyperpod.py similarity index 100% rename from sagemaker-train/tests/integ/train/test_nova_hyperpod.py rename to sagemaker-train/tests/integ/train/test_nova_sft_hyperpod.py diff --git a/sagemaker-train/tests/integ/train/test_sft_data_mixing_hyperpod.py b/sagemaker-train/tests/integ/train/test_sft_data_mixing_hyperpod.py index 5e7c00b045..9bf1ac1908 100644 --- a/sagemaker-train/tests/integ/train/test_sft_data_mixing_hyperpod.py +++ b/sagemaker-train/tests/integ/train/test_sft_data_mixing_hyperpod.py @@ -36,6 +36,7 @@ import pytest from sagemaker.train.sft_trainer import SFTTrainer from sagemaker.train.common import TrainingType +from sagemaker.train.base_trainer import BaseTrainer from sagemaker.train.data_mixing_config import DataMixingConfig from sagemaker.core.training.configs import HyperPodCompute @@ -49,8 +50,8 @@ REGION = "us-east-1" DATA_PREFIX = "test-sft-data-mixing-hyperpod-integ" NUM_TRAINING_SAMPLES = 300 -HYPERPOD_CLUSTER_NAME = "riv-rig" -HYPERPOD_INSTANCE_TYPE = "ml.p5.48xlarge" +HYPERPOD_CLUSTER_NAME = "pysdk-hp-integ-tests" +HYPERPOD_INSTANCE_TYPE = "ml.g6.12xlarge" def _generate_training_data() -> str: @@ -125,10 +126,10 @@ def training_resources(sagemaker_session_us_east_1): @pytest.mark.gpu_intensive @pytest.mark.us_east_1 def test_sft_trainer_nova_micro_data_mixing_hyperpod(sagemaker_session_us_east_1, training_resources): - """Test SFT trainer with Nova Lite 2 model and data mixing on HyperPod. + """Test SFT trainer with Nova Micro model and data mixing on HyperPod. This end-to-end test submits a real HyperPod training job with DataMixingConfig - for Nova Lite 2. The SDK resolves the datamix recipe from SageMaker Hub, validates + for Nova Micro. The SDK resolves the datamix recipe from SageMaker Hub, validates categories, and includes the serialized config in the HyperPod override parameters. """ unique_id = f"{int(time.time())}-{random.randint(1000, 9999)}" @@ -184,4 +185,33 @@ def test_sft_trainer_nova_micro_data_mixing_hyperpod(sagemaker_session_us_east_1 assert get_job_result.returncode == 0, ( f"hyperpod get-job failed for '{job_name}': {get_job_result.stderr}" ) - logger.info(f"Verified job '{job_name}' exists on the cluster.") + logger.info(f"Verified job '{job_name}' exists on the cluster using hp-cli.") + + # Poll for job completion by checking for the manifest in S3. + # The manifest is written under {output_s3_path}/{job_name}/manifest.json + # once training finishes, so its presence confirms end-to-end completion. + + output_s3_path = training_resources["s3_output_path"] + max_wait_time = 21600 # 6 hour timeout (HyperPod jobs can take longer) + poll_interval = 60 # Check every 60 seconds + start_time = time.time() + checkpoint_path = None + + while time.time() - start_time < max_wait_time: + checkpoint_path = BaseTrainer._resolve_checkpoint_from_manifest( + job_name=job_name, + output_s3_path=output_s3_path, + sagemaker_session=sagemaker_session_us_east_1, + ) + if checkpoint_path: + logger.info(f"Checkpoint resolved: {checkpoint_path}") + break + + elapsed = int(time.time() - start_time) + logger.info(f"Waiting for manifest... ({elapsed}s elapsed)") + time.sleep(poll_interval) + + assert checkpoint_path is not None, ( + f"Job {job_name} did not produce a manifest within {max_wait_time}s" + ) + logger.info(f"Training complete. Checkpoint: {checkpoint_path}") diff --git a/sagemaker-train/tests/unit/train/common_utils/test_data_mixing_utils.py b/sagemaker-train/tests/unit/train/common_utils/test_data_mixing_utils.py index 0ae2044cb3..1e69073f88 100644 --- a/sagemaker-train/tests/unit/train/common_utils/test_data_mixing_utils.py +++ b/sagemaker-train/tests/unit/train/common_utils/test_data_mixing_utils.py @@ -13,6 +13,8 @@ """Unit tests for data mixing utility functions.""" from __future__ import absolute_import +import os + import pytest from sagemaker.train.data_mixing_config import DataMixingConfig @@ -823,8 +825,12 @@ def capture_write(path, mode="r", **kwargs): assert parsed["data_mixing"]["sources"]["nova_data"]["en-entertainment"] == 20 assert parsed["data_mixing"]["sources"]["nova_data"]["en-scientific"] == 10 - def test_return_value_is_relative_path_and_image_uri(self): - """Return value should be (relative_path_without_extension, image_uri).""" + def test_return_value_is_absolute_recipe_path_and_image_uri(self): + """Return value should be (absolute_recipe_path_with_extension, image_uri). + + The path must be a loadable filesystem path (absolute, .yaml extension + intact) because the recipe resolver opens it as a user recipe file. + """ from unittest.mock import patch, MagicMock, mock_open from sagemaker.train.common_utils.data_mixing_utils import ( @@ -843,12 +849,14 @@ def test_return_value_is_relative_path_and_image_uri(self): with patch("builtins.open", m_open): result = build_hyperpod_datamix_recipe_from_context(context, config) - relative_path, image_uri = result + recipe_path, image_uri = result - # relative_path should not end with .yaml - assert not relative_path.endswith(".yaml") - # relative_path should contain fine-tuning/nova - assert "fine-tuning/nova" in relative_path + # recipe_path must be an absolute filesystem path + assert os.path.isabs(recipe_path) + # recipe_path must retain the .yaml extension so it can be opened + assert recipe_path.endswith(".yaml") + # recipe_path should live under the HyperPod CLI recipes fine-tuning/nova dir + assert "fine-tuning/nova" in recipe_path # image_uri should match the context assert image_uri == self.IMAGE_URI