Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/specfact_cli/adapters/backlog_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/adapters/test_ado.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading