diff --git a/src/specfact_cli/adapters/backlog_base.py b/src/specfact_cli/adapters/backlog_base.py index 139372a8..7639f949 100644 --- a/src/specfact_cli/adapters/backlog_base.py +++ b/src/specfact_cli/adapters/backlog_base.py @@ -287,6 +287,14 @@ def _dedupe_imported_change_id( tool_name: str, existing_proposals: dict[str, ChangeProposal], ) -> str: + existing_change_id = self._find_existing_imported_change_id_by_source( + item_data, + tool_name, + existing_proposals, + ) + if existing_change_id: + return existing_change_id + existing_proposal = existing_proposals.get(candidate) if existing_proposal is None: return candidate or raw_change_id or "unknown" @@ -304,6 +312,19 @@ def _dedupe_imported_change_id( return deduped_candidate return deduped_candidate + @beartype + @ensure(lambda result: result is None or isinstance(result, str), "Must return change id or None") + def _find_existing_imported_change_id_by_source( + self, + item_data: dict[str, Any], + tool_name: str, + existing_proposals: dict[str, ChangeProposal], + ) -> str | None: + for change_id, proposal in existing_proposals.items(): + if self._matches_existing_import_source(proposal, item_data, tool_name): + return change_id + return None + @beartype @ensure(lambda result: isinstance(result, str), "Must return source URL string") def _get_import_source_url(self, item_data: dict[str, Any]) -> str: diff --git a/tests/unit/adapters/test_ado.py b/tests/unit/adapters/test_ado.py index 47c56f28..016513a8 100644 --- a/tests/unit/adapters/test_ado.py +++ b/tests/unit/adapters/test_ado.py @@ -965,3 +965,71 @@ def test_import_artifact_ado_work_item_reimport_keeps_original_slug( assert "add-feature-x" in project_bundle.change_tracking.proposals assert "add-feature-x-123" not in project_bundle.change_tracking.proposals assert len(project_bundle.change_tracking.proposals) == 1 + + @beartype + @patch.object(AdoAdapter, "_get_work_item_comments", return_value=[]) + def test_import_artifact_ado_work_item_reimport_with_renamed_title_keeps_original_slug( + self, + _mock_get_comments: MagicMock, + ado_adapter: AdoAdapter, + tmp_path: Path, + ) -> None: + """Re-importing the same work item with a new title should keep the original proposal name.""" + project_bundle = MagicMock() + project_bundle.change_tracking = ChangeTracking( + proposals={ + "add-feature-x": ChangeProposal( + name="add-feature-x", + title="Add Feature X", + description="Existing", + rationale="Existing rationale", + timeline=None, + owner=None, + status="proposed", + created_at="2025-01-01T10:00:00+00:00", + applied_at=None, + archived_at=None, + source_tracking=SourceTracking( + tool="ado", + source_metadata={ + "source_id": 123, + "source_url": "https://dev.azure.com/test-org/test-project/_workitems/edit/123", + "source_type": "ado", + "backlog_entries": [ + { + "source_id": "123", + "source_url": "https://dev.azure.com/test-org/test-project/_workitems/edit/123", + "source_type": "ado", + } + ], + }, + ), + ) + } + ) + project_bundle.bundle_dir = tmp_path + + work_item_data = { + "id": 123, + "fields": { + "System.Title": "Rename Feature X", + "System.Description": "## Why\n\nNeeded\n\n## What Changes\n\nImplement", + "System.State": "New", + "System.CreatedDate": "2025-01-01T10:00:00Z", + "System.WorkItemType": "User Story", + }, + "_links": { + "html": {"href": "https://dev.azure.com/test-org/test-project/_workitems/edit/123"}, + }, + } + + ado_adapter.import_artifact( + artifact_key="ado_work_item", + artifact_path=work_item_data, + project_bundle=project_bundle, + ) + + assert "add-feature-x" in project_bundle.change_tracking.proposals + assert "rename-feature-x" not in project_bundle.change_tracking.proposals + assert "rename-feature-x-123" not in project_bundle.change_tracking.proposals + assert len(project_bundle.change_tracking.proposals) == 1