"""Tests for per-agent Plane comment authorship (feat: per-agent bot author). Covers: * _headers_for: role -> bot token; None/unknown/empty token -> shared fallback. * add_comment: author is propagated into the POST headers; no author keeps backward-compatible behaviour (shared orchestrator token). GET/PATCH calls are intentionally NOT covered here: they stay on the shared token by design and are unchanged by this feature. """ import os # Set env defaults before importing app modules (same convention as the other # suites) so config/settings load cleanly without a real .env. os.environ.setdefault("ORCH_PLANE_API_TOKEN", "shared-token") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") from unittest.mock import patch, MagicMock # noqa: E402 from src import plane_sync # noqa: E402 # --------------------------------------------------------------------------- # # _headers_for # --------------------------------------------------------------------------- # def test_headers_for_known_role_uses_bot_token(): """A known role with a configured token -> that bot's X-API-Key.""" with patch.dict(plane_sync.PLANE_BOT_TOKENS, {"analyst": "analyst-tok"}, clear=False): assert plane_sync._headers_for("analyst") == {"X-API-Key": "analyst-tok"} def test_headers_for_none_falls_back_to_shared(): """author=None -> shared orchestrator headers.""" assert plane_sync._headers_for(None) is plane_sync.PLANE_HEADERS def test_headers_for_unknown_role_falls_back_to_shared(): """Unknown role -> shared orchestrator headers.""" assert plane_sync._headers_for("nope") is plane_sync.PLANE_HEADERS def test_headers_for_empty_token_falls_back_to_shared(): """Known role but empty/unconfigured token -> shared orchestrator headers.""" with patch.dict(plane_sync.PLANE_BOT_TOKENS, {"tester": ""}, clear=False): assert plane_sync._headers_for("tester") is plane_sync.PLANE_HEADERS def test_headers_for_empty_string_author_falls_back_to_shared(): """author='' -> shared orchestrator headers.""" assert plane_sync._headers_for("") is plane_sync.PLANE_HEADERS # --------------------------------------------------------------------------- # # add_comment # --------------------------------------------------------------------------- # def _mock_post_ok(): resp = MagicMock() resp.raise_for_status.return_value = None return resp def test_add_comment_with_author_posts_with_bot_headers(): """add_comment(author='developer') -> httpx.post called with the developer bot's X-API-Key header.""" with patch.object(plane_sync, "find_issue_id", return_value="issue-uuid"), \ patch.object(plane_sync, "_resolve_project_id", return_value="proj-uuid"), \ patch.dict(plane_sync.PLANE_BOT_TOKENS, {"developer": "dev-tok"}, clear=False), \ patch.object(plane_sync.httpx, "post", return_value=_mock_post_ok()) as mock_post: plane_sync.add_comment("ET-001", "hello", author="developer") assert mock_post.called _, kwargs = mock_post.call_args assert kwargs["headers"] == {"X-API-Key": "dev-tok"} def test_add_comment_without_author_uses_shared_token(): """add_comment without author -> shared orchestrator headers (backward compatible).""" with patch.object(plane_sync, "find_issue_id", return_value="issue-uuid"), \ patch.object(plane_sync, "_resolve_project_id", return_value="proj-uuid"), \ patch.object(plane_sync.httpx, "post", return_value=_mock_post_ok()) as mock_post: plane_sync.add_comment("ET-001", "hello") assert mock_post.called _, kwargs = mock_post.call_args assert kwargs["headers"] is plane_sync.PLANE_HEADERS def test_add_comment_unknown_author_uses_shared_token(): """add_comment with an unknown role -> shared orchestrator headers.""" with patch.object(plane_sync, "find_issue_id", return_value="issue-uuid"), \ patch.object(plane_sync, "_resolve_project_id", return_value="proj-uuid"), \ patch.object(plane_sync.httpx, "post", return_value=_mock_post_ok()) as mock_post: plane_sync.add_comment("ET-001", "hello", author="ghost") assert mock_post.called _, kwargs = mock_post.call_args assert kwargs["headers"] is plane_sync.PLANE_HEADERS