Guard GitHub metadata selectors

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-12 20:16:21 -07:00
co-authored by Orca
parent 65e1baf7a3
commit bad7711930
5 changed files with 57 additions and 5 deletions
@@ -231,3 +231,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-12: Scoped GitHub Tasks partial-failure retry ownership to the selected source cache scope instead of `repoPath`, so same-project/same-path multi-host failures cannot make the wrong source banner look in-flight; verified with `task-page-cache-selectors.test.ts`, targeted oxlint, and full `pnpm run typecheck`.
- [x] 2026-06-12: Routed GitHub issue reads, PR check reads/details, PR comments, review replies, and review-thread resolution through explicit `TaskSourceContext` in the store/preload/main IPC boundary, and source-scoped their comment/check caches; verified with GitHub slice regressions (106 tests), focused oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-12: Guarded remaining GitLab path-only fallbacks by making `workItemByPath` use the same repoId/source-context selector as the rest of GitLab IPC, passing repoId from URL picker and right-sidebar GitLab review actions, and verifying source-host routing with GitLab IPC regressions plus focused oxlint and full `pnpm run typecheck`.
- [x] 2026-06-12: Guarded GitHub label and assignable-user metadata lookups with the same repo/source-context selector as task mutations, so drawer edit metadata cannot bypass source-host validation; verified with GitHub IPC regressions (17 tests), focused oxlint, and full `pnpm run typecheck`.
+42
View File
@@ -8,6 +8,8 @@ const {
getIssueMock,
listIssuesMock,
listWorkItemsMock,
listLabelsMock,
listAssignableUsersMock,
getAuthenticatedViewerMock,
mergePRMock,
setPRAutoMergeMock,
@@ -22,6 +24,8 @@ const {
getIssueMock: vi.fn(),
listIssuesMock: vi.fn(),
listWorkItemsMock: vi.fn(),
listLabelsMock: vi.fn(),
listAssignableUsersMock: vi.fn(),
getAuthenticatedViewerMock: vi.fn(),
mergePRMock: vi.fn(),
setPRAutoMergeMock: vi.fn(),
@@ -46,6 +50,8 @@ vi.mock('../github/client', () => ({
getIssue: getIssueMock,
listIssues: listIssuesMock,
listWorkItems: listWorkItemsMock,
listLabels: listLabelsMock,
listAssignableUsers: listAssignableUsersMock,
getAuthenticatedViewer: getAuthenticatedViewerMock,
mergePR: mergePRMock,
setPRAutoMerge: setPRAutoMergeMock,
@@ -74,6 +80,7 @@ describe('registerGitHubHandlers', () => {
badgeColor: string
addedAt: number
connectionId?: string | null
executionHostId?: string | null
issueSourcePreference?: 'origin' | 'upstream'
}
let repos: FixtureRepo[] = []
@@ -91,6 +98,8 @@ describe('registerGitHubHandlers', () => {
getIssueMock.mockReset()
listIssuesMock.mockReset()
listWorkItemsMock.mockReset()
listLabelsMock.mockReset()
listAssignableUsersMock.mockReset()
getAuthenticatedViewerMock.mockReset()
mergePRMock.mockReset()
setPRAutoMergeMock.mockReset()
@@ -175,6 +184,39 @@ describe('registerGitHubHandlers', () => {
expect(listWorkItemsMock).not.toHaveBeenCalled()
})
it('guards label metadata lookups with source host context', async () => {
listLabelsMock.mockResolvedValue(['bug'])
repos = [
...repos,
{
id: 'repo-ssh',
path: '/workspace/remote-repo',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'openclaw-2',
executionHostId: 'ssh:openclaw-2'
}
]
registerGitHubHandlers(store as never, stats as never)
await expect(
handlers['gh:listLabels'](null, {
repoPath: '/workspace/remote-repo',
repoId: 'repo-ssh',
sourceContext: {
kind: 'task-source',
provider: 'github',
projectId: 'project-1',
hostId: 'ssh:openclaw-2',
repoId: 'repo-ssh'
}
})
).resolves.toEqual(['bug'])
expect(listLabelsMock).toHaveBeenCalledWith('/workspace/remote-repo', undefined, 'openclaw-2')
})
it('forwards listIssues for registered repositories and unwraps items', async () => {
listIssuesMock.mockResolvedValue({ items: [] })
+2 -2
View File
@@ -939,12 +939,12 @@ export function registerGitHubHandlers(store: Store, stats: StatsCollector): voi
}
)
ipcMain.handle('gh:listLabels', (_event, args: { repoPath: string }) => {
ipcMain.handle('gh:listLabels', (_event, args: RepoScopedArgs) => {
const repo = assertRegisteredRepo(args, store)
return listLabels(repo.path, repo.issueSourcePreference, repoConnectionId(repo))
})
ipcMain.handle('gh:listAssignableUsers', (_event, args: { repoPath: string }) => {
ipcMain.handle('gh:listAssignableUsers', (_event, args: RepoScopedArgs) => {
const repo = assertRegisteredRepo(args, store)
return listAssignableUsers(repo.path, repo.issueSourcePreference, repoConnectionId(repo))
})
+6 -1
View File
@@ -1314,10 +1314,15 @@ export type PreloadApi = {
sourceContext?: TaskSourceContext | null
}
) => Promise<GitHubCommentResult>
listLabels: (args: { repoPath: string; repoId?: string }) => Promise<string[]>
listLabels: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
}) => Promise<string[]>
listAssignableUsers: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
}) => Promise<GitHubAssignableUser[]>
/**
* Subscribe to local-mutation broadcasts. Used by the work-item-drawer
+6 -2
View File
@@ -1169,12 +1169,16 @@ const api = {
body: string
}): Promise<GitHubCommentResult> => ipcRenderer.invoke('gh:addPRReviewComment', args),
listLabels: (args: { repoPath: string; repoId?: string }): Promise<string[]> =>
ipcRenderer.invoke('gh:listLabels', args),
listLabels: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
}): Promise<string[]> => ipcRenderer.invoke('gh:listLabels', args),
listAssignableUsers: (args: {
repoPath: string
repoId?: string
sourceContext?: TaskSourceContext | null
}): Promise<GitHubAssignableUser[]> => ipcRenderer.invoke('gh:listAssignableUsers', args),
// Why: every renderer subscribes to local mutation broadcasts so each