Show GitHub PR reviewer names in task rows (#2309)

This commit is contained in:
Jinjing
2026-05-18 23:06:54 -07:00
committed by GitHub
parent 94f47c3e32
commit a6ca940d0f
16 changed files with 1257 additions and 369 deletions
+40 -26
View File
@@ -114,7 +114,16 @@ describe('listWorkItems', () => {
author: { login: 'octocat' },
isDraft: false,
headRefName: 'feature/add-feature',
baseRefName: 'main'
baseRefName: 'main',
reviewRequests: [
{
requestedReviewer: {
login: 'AmethystLiang',
name: 'Amethyst Liang',
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4'
}
}
]
}
])
})
@@ -147,7 +156,7 @@ describe('listWorkItems', () => {
'--limit',
'10',
'--json',
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner',
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner,reviewRequests',
'--repo',
'acme/widgets',
'--assignee',
@@ -157,7 +166,7 @@ describe('listWorkItems', () => {
)
const prListFields = ghExecFileAsyncMock.mock.calls[1][0].join(',')
expect(prListFields).not.toContain('statusCheckRollup')
expect(prListFields).not.toContain('reviewRequests')
expect(prListFields).toContain('reviewRequests')
expect(prListFields).not.toContain('mergeStateStatus')
expect(items).toEqual([
{
@@ -182,7 +191,14 @@ describe('listWorkItems', () => {
updatedAt: '2026-03-28T00:00:00Z',
author: 'octocat',
branchName: 'feature/add-feature',
baseRefName: 'main'
baseRefName: 'main',
reviewRequests: [
{
login: 'AmethystLiang',
name: 'Amethyst Liang',
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4'
}
]
}
])
})
@@ -215,7 +231,7 @@ describe('listWorkItems', () => {
'--limit',
'10',
'--json',
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner',
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner,reviewRequests',
'--repo',
'acme/widgets',
'--state',
@@ -315,7 +331,7 @@ describe('listWorkItems', () => {
'--limit',
'10',
'--json',
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner',
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner,reviewRequests',
'--repo',
'acme/widgets',
'--state',
@@ -354,26 +370,24 @@ describe('listWorkItems', () => {
it('marks fork PRs as cross-repository when REST payload only includes head.label', async () => {
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'stablyai', repo: 'orca' })
ghExecFileAsyncMock
.mockResolvedValueOnce({ stdout: '[]' })
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 1849,
title: 'Fork PR with missing head repo',
state: 'open',
html_url: 'https://github.com/stablyai/orca/pull/1849',
updated_at: '2026-04-01T00:00:00Z',
user: { login: 'contributor' },
head: {
ref: 'feat/onboarding-model-choice-782',
repo: null,
label: 'contributor:feat/onboarding-model-choice-782'
},
base: { ref: 'main' }
}
])
})
ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: '[]' }).mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 1849,
title: 'Fork PR with missing head repo',
state: 'open',
html_url: 'https://github.com/stablyai/orca/pull/1849',
updated_at: '2026-04-01T00:00:00Z',
user: { login: 'contributor' },
head: {
ref: 'feat/onboarding-model-choice-782',
repo: null,
label: 'contributor:feat/onboarding-model-choice-782'
},
base: { ref: 'main' }
}
])
})
const { items } = await listWorkItems('/repo-root', 10)
expect(items).toEqual([
+35 -5
View File
@@ -84,6 +84,7 @@ vi.mock('./rate-limit', () => ({
import {
getPRComments,
getPRForBranch,
getWorkItem,
getPullRequestPushTarget,
mergePR,
resolveReviewThread,
@@ -824,17 +825,46 @@ describe('getPRForBranch', () => {
remoteName: 'origin',
branchName: 'feature/test'
})
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
1,
['api', 'repos/fork/orca/pulls/1849'],
{ cwd: '/repo-root' }
)
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(1, ['api', 'repos/fork/orca/pulls/1849'], {
cwd: '/repo-root'
})
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
2,
['api', 'repos/stablyai/orca/pulls/1849'],
{ cwd: '/repo-root' }
)
})
it('normalizes reviewer avatars from REST pull request payloads', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 42,
title: 'Review me',
state: 'open',
html_url: 'https://github.com/acme/widgets/pull/42',
labels: [],
updated_at: '2026-03-28T00:00:00Z',
user: { login: 'author' },
draft: false,
requested_reviewers: [
{
login: 'AmethystLiang',
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4'
}
]
})
})
await expect(getWorkItem('/repo-root', 42, 'pr')).resolves.toMatchObject({
reviewRequests: [
{
login: 'AmethystLiang',
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4'
}
]
})
})
})
describe('GitHub GraphQL rate-limit guard', () => {
+11 -5
View File
@@ -249,11 +249,12 @@ export async function getAuthenticatedViewer(): Promise<GitHubViewer | null> {
type MainWorkItem = Omit<GitHubWorkItem, 'repoId'>
const WORK_ITEM_PR_LIST_JSON_FIELDS =
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner'
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner,reviewRequests'
// Why: these fields are intentionally excluded from `gh pr list` because
// statusCheckRollup/review/merge metadata fan out into expensive GraphQL work
// across every row. Fetch them only for single-PR detail surfaces.
// statusCheckRollup/review decision/merge metadata fan out into expensive
// GraphQL work across every row. Requested reviewers are kept in the list
// payload because the Tasks table renders that column on first paint.
const WORK_ITEM_PR_DETAIL_JSON_FIELDS =
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRepositoryOwner,additions,deletions,changedFiles,reviewDecision,reviewRequests,latestReviews,assignees,statusCheckRollup,mergeable,mergeStateStatus,maintainerCanModify'
@@ -342,7 +343,12 @@ function userFromUnknown(
return {
login,
name: typeof raw.name === 'string' ? raw.name : null,
avatarUrl: typeof raw.avatarUrl === 'string' ? raw.avatarUrl : ''
avatarUrl:
typeof raw.avatarUrl === 'string'
? raw.avatarUrl
: typeof raw.avatar_url === 'string'
? raw.avatar_url
: ''
}
}
@@ -2504,7 +2510,7 @@ export async function requestPRReviewers(
): Promise<{ ok: true } | { ok: false; error: string }> {
const logins = reviewers.map((reviewer) => reviewer.trim()).filter(Boolean)
if (logins.length === 0) {
return { ok: false, error: 'Enter at least one reviewer login' }
return { ok: false, error: 'Enter at least one reviewer' }
}
const ghOptions = ghRepoExecOptions(githubRepoContext(repoPath, connectionId))
const ownerRepo = await getOwnerRepo(repoPath, connectionId)