Skip to content
Merged
8 changes: 8 additions & 0 deletions docs/plugins/generated/github-step-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,14 @@ Select open inline threads worth AI analysis.

**Available to later steps:** `thread_review_candidates (List[ThreadReviewCandidate])`

**Inputs (from ctx.data)**

| Name | Type | Description |
|------|------|-------------|
| review_threads (List[UICommentThread]) | - | - |
| review_pr (UIPullRequest) | - | - |
| review_current_user (str) | - | Authenticated GitHub login running Titan |

**Outputs (saved to ctx.data)**

| Name | Type | Description |
Expand Down
21 changes: 21 additions & 0 deletions docs/plugins/github/client-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,27 @@ client.get_pr_commit_sha(123)

- `pr_number`: Required. Pull request number.

### Read referenced commit context

Returns compact remote context for a commit SHA mentioned in review discussion,
including changed files and a truncated patch excerpt suitable for AI prompts.

**Call:**

```python
client.get_commit_review_context(
"343e2e9",
max_files=3,
max_patch_chars=4000,
)
```

**Parameters:**

- `commit_ref`: Required. Full or short commit SHA resolvable in the current repository.
- `max_files`: Optional. Maximum number of changed files to include in the returned context.
- `max_patch_chars`: Optional. Maximum combined patch excerpt size before truncation.

### Merge a pull request

Merges a pull request using the selected merge strategy.
Expand Down
6 changes: 5 additions & 1 deletion docs/plugins/github/workflow-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,11 @@ How to read these contracts:

**Inputs (from ctx.data)**

None documented.
| Name | Type | Description |
|------|------|-------------|
| review_threads (List[UICommentThread]) | - | - |
| review_pr (UIPullRequest) | - | - |
| review_current_user (str) | - | Authenticated GitHub login running Titan |

**Outputs (saved to ctx.data)**

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from titan_plugin_github.models.review_models import ReferencedCommitContext, ThreadReviewContext
from titan_plugin_github.models.view import UIComment, UICommentThread
from titan_plugin_github.operations.thread_resolution_operations import (
build_thread_resolution_prompt,
build_thread_review_candidates,
)


def _make_thread(*, main_author: str, reply_author: str) -> UICommentThread:
return UICommentThread(
thread_id=f"thread_{main_author}_{reply_author}",
main_comment=UIComment(
id=10,
body="Please fix this",
author_login=main_author,
author_name=main_author,
formatted_date="",
path="src/main.py",
line=42,
),
replies=[
UIComment(
id=11,
body="Fixed",
author_login=reply_author,
author_name=reply_author,
formatted_date="",
path="src/main.py",
line=42,
)
],
is_resolved=False,
is_outdated=False,
)


def test_build_thread_review_candidates_only_includes_current_users_threads():
candidates = build_thread_review_candidates(
[
_make_thread(main_author="current-reviewer", reply_author="pr-author"),
_make_thread(main_author="other-reviewer", reply_author="pr-author"),
],
pr_author="pr-author",
reviewer_login="current-reviewer",
)

assert len(candidates) == 1
assert candidates[0].main_comment_author == "current-reviewer"


def test_build_thread_review_candidates_excludes_threads_without_pr_author_last_reply():
candidates = build_thread_review_candidates(
[_make_thread(main_author="current-reviewer", reply_author="other-reviewer")],
pr_author="pr-author",
reviewer_login="current-reviewer",
)

assert candidates == []


def test_build_thread_resolution_prompt_includes_referenced_commit_context():
context = ThreadReviewContext(
thread_id="thread_123",
comment_id=10,
path="src/buttons.kt",
line=543,
main_comment_body="Please remove the default dialog state.",
main_comment_author="reviewer",
all_replies=[{"author": "author", "body": "Fixed in 343e2e9"}],
current_code_hunk="@@ -1,2 +1,2 @@\n-old\n+new\n",
referenced_commits=[
ReferencedCommitContext(
sha="343e2e9d7402d0afccfd35a9ecc8e6ea341031c6",
abbreviated_sha="343e2e9",
message="remove default state value",
changed_files=["src/BaseDialog.kt"],
patch_excerpt="diff --git a/src/BaseDialog.kt b/src/BaseDialog.kt\n@@ -1 +1 @@\n-old\n+new\n",
)
],
)

prompt = build_thread_resolution_prompt([context])

assert "Referenced commits from replies" in prompt
assert "343e2e9" in prompt
assert "src/BaseDialog.kt" in prompt
assert "remove default state value" in prompt
64 changes: 64 additions & 0 deletions plugins/titan-plugin-github/tests/services/test_pr_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,67 @@ def test_merge_pr_failure(pr_service, mock_gh_network):
assert result.data.merged is False
assert result.data.status_icon == "❌"
assert "not mergeable" in result.data.message.lower()


def test_get_commit_review_context_success(pr_service, mock_gh_network):
"""Test commit context retrieval for a referenced SHA."""
mock_gh_network.run_command.return_value = json.dumps(
{
"sha": "343e2e9d7402d0afccfd35a9ecc8e6ea341031c6",
"commit": {"message": "remove default state value"},
"files": [
{
"filename": "freyja-core/src/main/kotlin/.../BaseDialog.kt",
"status": "modified",
"patch": "@@ -36,7 +35,7 @@\n-fun BaseDialog(dialogState: DialogState = remember { DialogState() })\n+fun BaseDialog(dialogState: DialogState)\n",
}
],
}
)

result = pr_service.get_commit_review_context("343e2e9")

assert isinstance(result, ClientSuccess)
assert result.data.abbreviated_sha == "343e2e9"
assert result.data.changed_files == ["freyja-core/src/main/kotlin/.../BaseDialog.kt"]
assert "remove default state value" == result.data.message
assert "diff --git a/freyja-core/src/main/kotlin/.../BaseDialog.kt" in result.data.patch_excerpt
assert "repos/test-owner/test-repo/commits/343e2e9" in mock_gh_network.run_command.call_args[0][0][1]


def test_get_commit_review_context_uses_head_repo_when_provided(pr_service, mock_gh_network):
"""Test that repo_owner/repo_name override the configured base repo (fork PRs)."""
mock_gh_network.run_command.return_value = json.dumps(
{
"sha": "343e2e9d7402d0afccfd35a9ecc8e6ea341031c6",
"commit": {"message": "remove default state value"},
"files": [],
}
)

result = pr_service.get_commit_review_context(
"343e2e9", repo_owner="fork-owner", repo_name="fork-repo"
)

assert isinstance(result, ClientSuccess)
assert "repos/fork-owner/fork-repo/commits/343e2e9" in mock_gh_network.run_command.call_args[0][0][1]


def test_get_commit_review_context_returns_parse_error_on_invalid_json(pr_service, mock_gh_network):
"""Test invalid commit payload handling."""
mock_gh_network.run_command.return_value = "not json"

result = pr_service.get_commit_review_context("343e2e9")

assert isinstance(result, ClientError)
assert result.error_code == "PARSE_ERROR"
Comment thread
finxo marked this conversation as resolved.


def test_get_commit_review_context_returns_api_error_on_network_failure(pr_service, mock_gh_network):
"""Test GitHub API failure handling."""
mock_gh_network.run_command.side_effect = GitHubAPIError("Not Found")

result = pr_service.get_commit_review_context("343e2e9")

assert isinstance(result, ClientError)
assert result.error_code == "API_ERROR"
Loading