From 1e7076e6bbe6d454d9dc2a0dad809a4637623a36 Mon Sep 17 00:00:00 2001 From: witbrock Date: Thu, 10 Sep 2026 23:14:53 -0400 Subject: [PATCH] fix(jira): map actor-only imports to the governed account owner --- .../integrations/internal_mcp/catalogue.py | 19 ++++- .../test_internal_mcp_task_parity_tools.py | 76 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/backend/integrations/internal_mcp/catalogue.py b/src/backend/integrations/internal_mcp/catalogue.py index a4b01f64c..c9f772ab2 100644 --- a/src/backend/integrations/internal_mcp/catalogue.py +++ b/src/backend/integrations/internal_mcp/catalogue.py @@ -36126,7 +36126,11 @@ def _normalise_jira_labels(raw_labels: Any) -> list[str]: def _task_import_jira_issues(**kwargs): """Import Jira issues into Von tasks with dry-run and idempotent reruns.""" - from .jira_proxy_mcp import get_jira_proxy, JiraProxyError + from .jira_proxy_mcp import ( + get_jira_proxy, + jira_resource_binding_for_user, + JiraProxyError, + ) from ...services.jira_task_import_service import ( import_jira_issues_to_tasks, list_imported_jira_issue_keys, @@ -36250,12 +36254,19 @@ async def _fetch_jira_issues() -> dict[str, Any]: if ( auto_map_namespace_to_jira_user - and isinstance(namespace_value, str) - and namespace_value.strip() and hasattr(proxy, "get_myself") ): try: - myself_payload = await proxy.get_myself() + # Bulk imports supply an actor without a legacy namespace. + # Map that actor only when the governed account binding proves + # ownership; a local operator can also import for somebody else. + map_current_user = bool(namespace_value) or bool( + actor_concept_id + and jira_resource_binding_for_user(actor_concept_id) + ) + myself_payload = ( + await proxy.get_myself() if map_current_user else None + ) account_id_value = ( myself_payload.get("accountId") if isinstance(myself_payload, Mapping) diff --git a/tests/backend/test_internal_mcp_task_parity_tools.py b/tests/backend/test_internal_mcp_task_parity_tools.py index 6b2b74cd5..fab597af6 100644 --- a/tests/backend/test_internal_mcp_task_parity_tools.py +++ b/tests/backend/test_internal_mcp_task_parity_tools.py @@ -7,6 +7,8 @@ from __future__ import annotations +import pytest + from src.backend.integrations.internal_mcp import build_default_catalogue from src.backend.integrations.internal_mcp.gateway import InternalMCPGateway from src.backend.integrations.internal_mcp.schemas import validate_payload @@ -635,6 +637,80 @@ def _fake_import_jira_issues_to_tasks(**kwargs): _assert_schema_conformance(gateway, "task_import_jira_issues", payload) +@pytest.mark.parametrize( + "owns_account,auto_map,explicit_mapping,expected", + [ + (True, True, None, "#V#current_user"), + (False, True, None, None), + (True, False, None, None), + (True, True, "#V#recorded_person", "#V#recorded_person"), + ], +) +def test_source_import_maps_actor_without_namespace_only_for_account_owner( + monkeypatch, owns_account, auto_map, explicit_mapping, expected +): + captured = {} + myself_calls = [] + + class Proxy: + async def get_myself(self): + myself_calls.append(True) + return {"accountId": "jira-current-user"} + + async def get_proxy(): + return Proxy() + + async def capture_issue(*_args, **_kwargs): + return {"complete": True}, { + "key": "TEST-1", + "id": "1", + "fields": {"project": {"key": "TEST"}}, + } + + def import_issues(**kwargs): + captured.update(kwargs) + return {"success": True, "dry_run": True, "summary": {}, "issues": []} + + monkeypatch.setattr( + "src.backend.integrations.internal_mcp.jira_proxy_mcp.get_jira_proxy", + get_proxy, + ) + monkeypatch.setattr( + "src.backend.integrations.internal_mcp.jira_proxy_mcp.jira_resource_binding_for_user", + lambda actor: "jira-owned-account" if owns_account and actor == "#V#current_user" else None, + ) + monkeypatch.setattr( + "src.backend.services.jira_source_retention_service.capture_issue", + capture_issue, + ) + monkeypatch.setattr( + "src.backend.services.jira_task_import_service.import_jira_issues_to_tasks", + import_issues, + ) + monkeypatch.setattr( + "src.backend.services.workflow_event_integration_service.resolve_event_actor_context", + lambda **_kwargs: ("#V#current_user", None), + ) + payload = _build_gateway().invoke( + "task_import_jira_issues", + { + "issue_keys": ["TEST-1"], + "actor_concept_id": "#V#current_user", + "preserve_source": True, + "dry_run": True, + "auto_map_namespace_to_jira_user": auto_map, + "jira_account_id_to_concept_id": ( + {"jira-current-user": explicit_mapping} if explicit_mapping else {} + ), + }, + ).payload + assert payload.get("success") is True + assert captured["jira_account_id_to_concept_id"].get("jira-current-user") == expected + assert bool(myself_calls) == (owns_account and auto_map) + assert captured["actor_concept_id"] == "#V#current_user" + assert captured["namespace"] is None + + def test_task_import_jira_issues_gateway_resolves_org_scope_for_backfill_and_import( monkeypatch, ):