Files
orca/src/main/ipc/github-wsl-runtime-routing.test.ts
T
Kelvin Amoaba 75b658c278 feat(native-chat): hover cards for GitHub PR and issue links (#35199)
* feat(native-chat): hover cards for GitHub PR and issue links

Links to the chat's own repo (origin or upstream) open a card with the
state, title, author, update time, and +/- or comment count. Bare #123
references in agent replies now link too. Cards use a light one-call
lookup; older hosts ignore the flag and answer with the full lookup.

* test(github): expect the preview options argument in runtime owner/repo lookups

* fix(native-chat): route GitHub link cards by the chat's own host and retry failed repo lookups

The repo lookup took the first worktree with the chat's id, so a twin on
another host could answer, and the preview cache keyed by repo id alone.
A failed slug lookup also left cards off until the chat remounted.
2026-10-29 15:29:32 +00:00

613 lines
16 KiB
TypeScript

import { EventEmitter } from 'node:events'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = await vi.hoisted(async () => {
const { createGitHubIpcMocks } = await import('./github-ipc-module-mocks')
return createGitHubIpcMocks()
})
vi.mock('electron', () => mocks.electron)
vi.mock('../github/client', () => mocks.client)
vi.mock('../github/work-item-details', () => mocks.workItemDetails)
vi.mock('../github/pr-refresh-coordinator', () => mocks.prRefresh)
vi.mock('../telemetry/client', () => mocks.telemetry)
vi.mock('../telemetry/cohort-classifier', () => mocks.cohort)
vi.mock('./ui', () => mocks.ui)
// Routing is under test here, in a running distro; the stopped case lives in github-wsl-stopped-distro.
vi.mock('../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
readWslDistroRunningState: async () => 'running'
}))
import { registerGitHubHandlers } from './github'
import { createGitHubIpcHarness, setPlatform } from './github-ipc-test-harness'
const {
getPRForBranch: getPRForBranchMock,
getIssue: getIssueMock,
getWorkItem: getWorkItemMock,
getWorkItemByOwnerRepo: getWorkItemByOwnerRepoMock,
listIssues: listIssuesMock,
listWorkItems: listWorkItemsMock,
countWorkItems: countWorkItemsMock,
createIssue: createIssueMock,
updateIssue: updateIssueMock,
addIssueComment: addIssueCommentMock,
listLabels: listLabelsMock,
listAssignableUsers: listAssignableUsersMock,
getPRChecks: getPRChecksMock,
getPRCheckDetails: getPRCheckDetailsMock,
getPRComments: getPRCommentsMock,
setPRCommentReaction: setPRCommentReactionMock,
resolveReviewThread: resolveReviewThreadMock,
setPRFileViewed: setPRFileViewedMock,
addPRReviewComment: addPRReviewCommentMock,
addPRReviewCommentReply: addPRReviewCommentReplyMock,
updatePRTitle: updatePRTitleMock,
mergePR: mergePRMock,
setPRAutoMerge: setPRAutoMergeMock,
updatePRState: updatePRStateMock,
markPRReadyForReview: markPRReadyForReviewMock,
rerunPRChecks: rerunPRChecksMock,
requestPRReviewers: requestPRReviewersMock,
removePRReviewers: removePRReviewersMock
} = mocks.client
const { getWorkItemDetails: getWorkItemDetailsMock, getPRFileContents: getPRFileContentsMock } =
mocks.workItemDetails
const { reportVisiblePRRefreshCandidates: reportVisiblePRRefreshCandidatesMock } = mocks.prRefresh
describe('registerGitHubHandlers', () => {
const harness = createGitHubIpcHarness(mocks)
const { handlers, store, stats } = harness
beforeEach(harness.reset)
it('routes local WSL project GitHub issue and work-item IPC through project git options', async () => {
setPlatform('win32')
harness.projects = [
{
id: 'project-1',
displayName: 'repo',
badgeColor: 'blue',
sourceRepoIds: ['repo-1'],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' },
createdAt: 0,
updatedAt: 0
}
]
listIssuesMock.mockResolvedValue({ items: [] })
listWorkItemsMock.mockResolvedValue({ items: [] })
countWorkItemsMock.mockResolvedValue(0)
getIssueMock.mockResolvedValue(null)
createIssueMock.mockResolvedValue({ ok: true, number: 1, url: 'https://example.com/1' })
updateIssueMock.mockResolvedValue({ ok: true })
addIssueCommentMock.mockResolvedValue({ ok: true })
listLabelsMock.mockResolvedValue([])
listAssignableUsersMock.mockResolvedValue([])
getPRForBranchMock.mockResolvedValue(null)
registerGitHubHandlers(store as never, stats as never)
const localGitOptions = { wslDistro: 'Ubuntu' }
await handlers['gh:prForBranch'](null, {
repoPath: '/workspace/repo',
branch: 'feature/wsl'
})
await handlers['gh:reportVisiblePRRefreshCandidates'](
{ sender: Object.assign(new EventEmitter(), { id: 7 }) },
{
generation: 1,
candidates: [
{
cacheKey: '/workspace/repo::feature/wsl',
repoPath: '/workspace/repo',
branch: 'feature/wsl',
repoKind: 'git',
repoId: 'repo-1'
}
]
}
)
await handlers['gh:listWorkItems'](null, {
repoPath: '/workspace/repo',
limit: 10,
query: 'is:open',
page: 2,
noCache: true
})
await handlers['gh:countWorkItems'](null, {
repoPath: '/workspace/repo',
query: 'is:issue'
})
await handlers['gh:listIssues'](null, { repoPath: '/workspace/repo', limit: 5 })
await handlers['gh:issue'](null, { repoPath: '/workspace/repo', number: 7 })
await handlers['gh:createIssue'](null, {
repoPath: '/workspace/repo',
title: 'Title',
body: 'Body',
labels: ['bug']
})
await handlers['gh:updateIssue'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
number: 7,
updates: { body: 'Updated' }
}
)
await handlers['gh:addIssueComment'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
number: 7,
body: 'Comment'
}
)
await handlers['gh:listLabels'](null, { repoPath: '/workspace/repo' })
await handlers['gh:listAssignableUsers'](null, { repoPath: '/workspace/repo' })
const metadataRepo = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
await handlers['gh:listLabels'](null, {
repoPath: '/workspace/repo',
ownerRepo: metadataRepo
})
await handlers['gh:listAssignableUsers'](null, {
repoPath: '/workspace/repo',
ownerRepo: metadataRepo
})
expect(getPRForBranchMock).toHaveBeenCalledWith(
'/workspace/repo',
'feature/wsl',
null,
null,
null,
{ localGitExecOptions: localGitOptions }
)
expect(reportVisiblePRRefreshCandidatesMock).toHaveBeenCalledWith(
[
expect.objectContaining({
repoPath: '/workspace/repo',
repoId: 'repo-1',
localGitOptions
})
],
1,
7
)
expect(listWorkItemsMock).toHaveBeenCalledWith(
'/workspace/repo',
10,
'is:open',
2,
undefined,
null,
true,
localGitOptions
)
expect(countWorkItemsMock).toHaveBeenCalledWith(
'/workspace/repo',
'is:issue',
undefined,
null,
localGitOptions
)
expect(listIssuesMock).toHaveBeenCalledWith(
'/workspace/repo',
5,
undefined,
null,
localGitOptions
)
expect(getIssueMock).toHaveBeenCalledWith('/workspace/repo', 7, null, localGitOptions)
expect(createIssueMock).toHaveBeenCalledWith(
'/workspace/repo',
'Title',
'Body',
undefined,
null,
{ labels: ['bug'], assignees: undefined },
localGitOptions
)
expect(updateIssueMock).toHaveBeenCalledWith(
'/workspace/repo',
7,
{ body: 'Updated' },
null,
localGitOptions,
undefined
)
expect(addIssueCommentMock).toHaveBeenCalledWith(
'/workspace/repo',
7,
'Comment',
null,
null,
localGitOptions,
undefined
)
expect(listLabelsMock).toHaveBeenCalledWith('/workspace/repo', undefined, null, localGitOptions)
expect(listLabelsMock).toHaveBeenLastCalledWith(
'/workspace/repo',
undefined,
null,
localGitOptions,
metadataRepo
)
expect(listAssignableUsersMock).toHaveBeenLastCalledWith(
'/workspace/repo',
undefined,
null,
localGitOptions,
metadataRepo
)
expect(listAssignableUsersMock).toHaveBeenCalledWith(
'/workspace/repo',
undefined,
null,
localGitOptions
)
})
it('routes local WSL project GitHub PR detail and action IPC through project git options', async () => {
setPlatform('win32')
harness.projects = [
{
id: 'project-1',
displayName: 'repo',
badgeColor: 'blue',
sourceRepoIds: ['repo-1'],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' },
createdAt: 0,
updatedAt: 0
}
]
const prRepo = { owner: 'acme', repo: 'orca', host: 'github.acme-corp.com' }
const localGitOptions = { wslDistro: 'Ubuntu' }
getWorkItemMock.mockResolvedValue(null)
getWorkItemByOwnerRepoMock.mockResolvedValue(null)
getWorkItemDetailsMock.mockResolvedValue(null)
getPRFileContentsMock.mockResolvedValue({ original: '', modified: '' })
getPRChecksMock.mockResolvedValue([])
getPRCheckDetailsMock.mockResolvedValue(null)
getPRCommentsMock.mockResolvedValue([])
setPRCommentReactionMock.mockResolvedValue(true)
resolveReviewThreadMock.mockResolvedValue(true)
setPRFileViewedMock.mockResolvedValue(true)
addPRReviewCommentReplyMock.mockResolvedValue({ ok: true })
addPRReviewCommentMock.mockResolvedValue({ ok: true })
updatePRTitleMock.mockResolvedValue(true)
mergePRMock.mockResolvedValue({ ok: true })
setPRAutoMergeMock.mockResolvedValue({ ok: true })
updatePRStateMock.mockResolvedValue({ ok: true })
markPRReadyForReviewMock.mockResolvedValue({ ok: true })
rerunPRChecksMock.mockResolvedValue({ ok: true, count: 1 })
requestPRReviewersMock.mockResolvedValue({ ok: true })
removePRReviewersMock.mockResolvedValue({ ok: true })
registerGitHubHandlers(store as never, stats as never)
await handlers['gh:workItem'](null, { repoPath: '/workspace/repo', number: 42, type: 'pr' })
await handlers['gh:workItemByOwnerRepo'](null, {
repoPath: '/workspace/repo',
owner: 'acme',
repo: 'orca',
host: prRepo.host,
number: 42,
type: 'pr'
})
await handlers['gh:workItemDetails'](null, {
repoPath: '/workspace/repo',
number: 42,
type: 'pr'
})
await handlers['gh:prFileContents'](null, {
repoPath: '/workspace/repo',
prNumber: 42,
prRepo,
path: 'src/app.ts',
status: 'modified',
headSha: 'head-sha',
baseSha: 'base-sha'
})
await handlers['gh:prChecks'](null, {
repoPath: '/workspace/repo',
prNumber: 42,
headSha: 'head-sha',
prRepo,
noCache: true
})
await handlers['gh:prCheckDetails'](null, {
repoPath: '/workspace/repo',
checkRunId: 9,
workflowRunId: 8,
checkName: 'lint',
url: 'https://example.com/check',
prRepo
})
await handlers['gh:prComments'](null, {
repoPath: '/workspace/repo',
prNumber: 42,
prRepo,
noCache: true
})
await handlers['gh:setPRCommentReaction'](null, {
repoPath: '/workspace/repo',
reactionSubjectId: ' IC_1 ',
content: '+1',
reacted: true,
prRepo
})
await handlers['gh:resolveReviewThread'](null, {
repoPath: '/workspace/repo',
threadId: 'thread-1',
resolve: true,
prRepo
})
await handlers['gh:setPRFileViewed'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
prRepo,
pullRequestId: 'PR_kw',
path: 'src/app.ts',
viewed: true
}
)
await handlers['gh:addPRReviewCommentReply'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
prRepo,
commentId: 11,
body: ' Reply ',
threadId: 'thread-1',
path: 'src/app.ts',
line: 10
}
)
await handlers['gh:addPRReviewComment'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
prRepo,
commitId: ' head-sha ',
path: 'src/app.ts',
line: 10,
body: ' Inline '
}
)
await handlers['gh:updatePRTitle'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
title: 'New title',
prRepo
}
)
await handlers['gh:mergePR'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
method: 'squash',
prRepo
}
)
await handlers['gh:setPRAutoMerge'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
enabled: true,
method: 'squash',
prRepo
}
)
await handlers['gh:updatePRState'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
updates: { state: 'closed' },
prRepo
}
)
await handlers['gh:markPRReadyForReview'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
prRepo
}
)
await handlers['gh:rerunPRChecks'](null, {
repoPath: '/workspace/repo',
prNumber: 42,
headSha: 'head-sha',
failedOnly: true,
prRepo
})
await handlers['gh:requestPRReviewers'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
reviewers: ['octo'],
prRepo
}
)
await handlers['gh:removePRReviewers'](
{ sender: { id: 1 } },
{
repoPath: '/workspace/repo',
prNumber: 42,
reviewers: ['octo'],
prRepo
}
)
expect(getWorkItemMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
'pr',
null,
localGitOptions,
undefined
)
expect(getWorkItemByOwnerRepoMock).toHaveBeenCalledWith(
'/workspace/repo',
prRepo,
42,
'pr',
null,
localGitOptions,
{ preview: false }
)
expect(getWorkItemDetailsMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
'pr',
null,
localGitOptions,
undefined
)
expect(getPRFileContentsMock).toHaveBeenCalledWith(
expect.objectContaining({ repoPath: '/workspace/repo', localGitOptions, prRepo })
)
expect(getPRChecksMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
'head-sha',
prRepo,
{ noCache: true },
null,
localGitOptions
)
expect(getPRCheckDetailsMock).toHaveBeenCalledWith(
'/workspace/repo',
{
checkRunId: 9,
workflowRunId: 8,
checkName: 'lint',
url: 'https://example.com/check',
prRepo
},
null,
localGitOptions
)
expect(getPRCommentsMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
{ noCache: true, prRepo },
null,
localGitOptions
)
expect(setPRCommentReactionMock).toHaveBeenCalledWith(
'/workspace/repo',
'IC_1',
'+1',
true,
null,
prRepo,
localGitOptions
)
expect(resolveReviewThreadMock).toHaveBeenCalledWith(
'/workspace/repo',
'thread-1',
true,
null,
prRepo,
localGitOptions
)
expect(setPRFileViewedMock).toHaveBeenCalledWith(
expect.objectContaining({ repoPath: '/workspace/repo', localGitOptions, prRepo })
)
expect(addPRReviewCommentReplyMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
11,
'Reply',
'thread-1',
'src/app.ts',
10,
null,
prRepo,
localGitOptions
)
expect(addPRReviewCommentMock).toHaveBeenCalledWith(
expect.objectContaining({
repoPath: '/workspace/repo',
commitId: 'head-sha',
body: 'Inline',
prRepo,
localGitOptions
})
)
expect(updatePRTitleMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
'New title',
null,
prRepo,
localGitOptions
)
expect(mergePRMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
'squash',
null,
prRepo,
localGitOptions
)
expect(setPRAutoMergeMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
true,
'squash',
null,
prRepo,
localGitOptions
)
expect(updatePRStateMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
{ state: 'closed' },
null,
prRepo,
localGitOptions
)
expect(markPRReadyForReviewMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
null,
prRepo,
localGitOptions
)
expect(rerunPRChecksMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
{ headSha: 'head-sha', failedOnly: true, prRepo },
null,
localGitOptions
)
expect(requestPRReviewersMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
['octo'],
null,
prRepo,
localGitOptions
)
expect(removePRReviewersMock).toHaveBeenCalledWith(
'/workspace/repo',
42,
['octo'],
null,
prRepo,
localGitOptions
)
})
})