From c3525c1b29e92ed32e7336e49555d3291bf82e44 Mon Sep 17 00:00:00 2001 From: Praveen Mittal Date: Fri, 7 Aug 2026 22:24:21 +0200 Subject: [PATCH] fix: _overwrite_pipeline raises IndexError when a pipeline has zero versions version_body["data"][0] was unguarded in _overwrite_pipeline. The 404 case (pipeline doesn't exist) was handled, but a pipeline that exists with zero saved versions -- created but never versioned -- returns 200 with an empty data list, and the unguarded index raised IndexError instead of falling through to any of the method's other branches. Treat an empty versions list the same as "latest version isn't a draft": there's no draft to patch, so create a new version via POST /pipelines/{name}/versions, same endpoint already used for that case. --- .../_service/pipeline_service.py | 12 +++-- tests/unit/service/test_pipeline_service.py | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/deepset_cloud_sdk/_service/pipeline_service.py b/deepset_cloud_sdk/_service/pipeline_service.py index 42d925d0..1e3469d5 100644 --- a/deepset_cloud_sdk/_service/pipeline_service.py +++ b/deepset_cloud_sdk/_service/pipeline_service.py @@ -398,11 +398,13 @@ async def _overwrite_pipeline(self, name: str, pipeline_yaml: str) -> Response: return await self._create_pipeline(name=name, pipeline_yaml=pipeline_yaml) version_body = version_response.json() - latest_version = version_body["data"][0] - version_id = latest_version["version_id"] - is_draft = latest_version.get("is_draft", False) + versions = version_body["data"] - if is_draft: + # The pipeline exists but may have no saved versions yet (e.g. created but never + # versioned) -- there's then no draft to patch, same as when the latest version + # simply isn't a draft. + if versions and versions[0].get("is_draft", False): + version_id = versions[0]["version_id"] # Patch existing draft version logger.debug(f"Patching existing draft version '{version_id}' of pipeline '{name}'.") return await self._api.patch( @@ -412,7 +414,7 @@ async def _overwrite_pipeline(self, name: str, pipeline_yaml: str) -> Response: ) # Create a new version - logger.debug(f"Latest version '{version_id}' of pipeline '{name}' is not a draft, creating new version.") + logger.debug(f"Pipeline '{name}' has no draft version, creating new version.") return await self._api.post( workspace_name=self._workspace_name, endpoint=f"pipelines/{name}/versions", diff --git a/tests/unit/service/test_pipeline_service.py b/tests/unit/service/test_pipeline_service.py index 5df9a1df..59bd297f 100644 --- a/tests/unit/service/test_pipeline_service.py +++ b/tests/unit/service/test_pipeline_service.py @@ -680,6 +680,52 @@ async def test_import_pipeline_with_overwrite_true_creates_new_version_when_not_ assert create_version_call.kwargs["endpoint"] == "pipelines/test_pipeline_overwrite/versions" assert "config_yaml" in create_version_call.kwargs["json"] + @pytest.mark.asyncio + async def test_import_pipeline_with_overwrite_true_creates_new_version_when_no_versions_exist( + self, pipeline_service: PipelineService, index_pipeline: Pipeline, mock_api: AsyncMock + ) -> None: + """Test importing a pipeline with overwrite=True creates a new version when the pipeline has no versions. + + A pipeline can exist (GET /versions returns 200) with an empty `data` list -- e.g. it was + created but never versioned. This must not raise an IndexError; it should create a new + version the same way it would if the latest version simply wasn't a draft. + """ + config = PipelineConfig( + name="test_pipeline_overwrite", + inputs=PipelineInputs(query=["retriever.query"]), + outputs=PipelineOutputs(documents="meta_ranker.documents"), + strict_validation=False, + overwrite=True, + ) + + # Mock successful validation response + validation_response = Mock(spec=Response) + validation_response.status_code = HTTPStatus.NO_CONTENT.value + + # Mock versions response: pipeline exists, but has zero saved versions + versions_response = Mock(status_code=HTTPStatus.OK.value) + versions_response.json.return_value = {"data": []} + + # Mock successful "create new version" response + new_version_response = Mock(spec=Response) + new_version_response.status_code = HTTPStatus.CREATED.value + + # First POST is validation, second POST is "create new version" + mock_api.post.side_effect = [validation_response, new_version_response] + mock_api.get.return_value = versions_response + + await pipeline_service.import_async(index_pipeline, config) + + # validation + GET versions + POST versions (new version) + assert mock_api.post.call_count == 2 + assert mock_api.get.call_count == 1 + assert mock_api.patch.call_count == 0 + + # Check create-version POST call + create_version_call = mock_api.post.call_args_list[1] + assert create_version_call.kwargs["endpoint"] == "pipelines/test_pipeline_overwrite/versions" + assert "config_yaml" in create_version_call.kwargs["json"] + @pytest.mark.asyncio async def test_import_pipeline_with_overwrite_fallback_to_create( self, pipeline_service: PipelineService, index_pipeline: Pipeline, mock_api: AsyncMock