feat(plane): per-agent bot authorship for comments
add_comment now accepts an optional author (agent role) and POSTs under the matching Plane bot token via _headers_for(), so Plane shows the real author (Analyst/Architect/Developer/Reviewer/Tester/Deployer/Stream) instead of a single shared account. Unknown/empty roles or missing tokens fall back to the shared orchestrator token (autonomy preserved). GET/PATCH (find_issue_id, set_state) are unchanged and stay on the shared token. Call sites in stage_engine, launcher, webhooks/plane and the plane_sync notify helpers now pass author by stage role; stage transitions use stream. Adds tests/test_plane_author.py.
This commit is contained in:
99
tests/test_plane_author.py
Normal file
99
tests/test_plane_author.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user