mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
test(tasks): cover the PR-error envelope end-to-end; neutral no-more-results toast
Round-7 review: the two literal gh-utils mocks lacked classifyListPrsError (a PR-side rejection in those suites would TypeError instead of assert), and the producer half of the errors.prs contract had no main-side test — added both, plus a classifier contract test pinning the search-window phrase the renderer keys on. The refused-clamp toast now reads the committed count via a synchronous ref mirror instead of the click-time closure, and says 'No more results' — nothing failed on that branch. Both toast keys plus the new one are translated in es/ja/ko/zh.
This commit is contained in:
@@ -63,7 +63,8 @@ vi.mock('./gh-utils', () => ({
|
||||
release: releaseMock,
|
||||
_resetOwnerRepoCache: vi.fn(),
|
||||
classifyGhError: (stderr: string) => ({ type: 'unknown', message: stderr }),
|
||||
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr })
|
||||
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr }),
|
||||
classifyListPrsError: (stderr: string) => ({ type: 'unknown', message: stderr })
|
||||
}))
|
||||
|
||||
vi.mock('../git/runner', () => ({
|
||||
@@ -266,6 +267,22 @@ describe('listWorkItems query paging', () => {
|
||||
expect(items.map((item) => item.number)).toEqual([4, 3])
|
||||
})
|
||||
|
||||
it('lifts a swallowed PR-side failure onto errors.prs instead of reading as end-of-data', async () => {
|
||||
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
// Non-availability failure (plain 403): swallowed into [], must still surface.
|
||||
ghExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 403: Forbidden'))
|
||||
|
||||
const envelope = await listWorkItems('/repo-root', 10, 'is:pr is:open', 2)
|
||||
|
||||
expect(envelope.items).toEqual([])
|
||||
expect(envelope.errors?.issues).toBeUndefined()
|
||||
expect(envelope.errors?.prs).toEqual({
|
||||
type: 'unknown',
|
||||
message: expect.stringContaining('HTTP 403')
|
||||
})
|
||||
})
|
||||
|
||||
it('filters pull request rows out of issue Search API results', async () => {
|
||||
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
|
||||
@@ -55,7 +55,8 @@ vi.mock('./gh-utils', () => ({
|
||||
release: releaseMock,
|
||||
_resetOwnerRepoCache: vi.fn(),
|
||||
classifyGhError: (stderr: string) => ({ type: 'unknown', message: stderr }),
|
||||
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr })
|
||||
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr }),
|
||||
classifyListPrsError: (stderr: string) => ({ type: 'unknown', message: stderr })
|
||||
}))
|
||||
|
||||
vi.mock('../git/runner', () => ({
|
||||
|
||||
@@ -795,4 +795,16 @@ describe('gh error classification', () => {
|
||||
message: 'Issues are disabled on this repository.'
|
||||
})
|
||||
})
|
||||
|
||||
// Why: the renderer detects the Search API 1000-result window by matching
|
||||
// /first 1000 search results/i against this message (#11485) — trimming the
|
||||
// raw stderr out of the validation_error copy would silently downgrade every
|
||||
// window 422 to a generic failure. This pins the contract.
|
||||
it('keeps the search-window phrase in validation_error list messages', () => {
|
||||
const stderr =
|
||||
'Command failed: gh api --hostname github.com search/issues\nValidation Failed: Only the first 1000 search results are available (HTTP 422)'
|
||||
const classified = classifyListIssuesError(stderr)
|
||||
expect(classified.type).toBe('validation_error')
|
||||
expect(classified.message).toMatch(/first 1000 search results/i)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3823,6 +3823,9 @@ export default function TaskPage(): React.JSX.Element {
|
||||
// can't resurrect proven-unreachable pages, nor be pinned by a speculative
|
||||
// withdrawal (see deriveAdvertisedTotalPages).
|
||||
const [provenPageLimit, setProvenPageLimit] = useState<number | null>(null)
|
||||
// Why: synchronous mirror of countedTotalPages — the empty-page branch needs
|
||||
// the committed value, not a click-time closure, and refs update immediately.
|
||||
const countedTotalPagesRef = useRef<number | null>(null)
|
||||
const fetchWorkItemsNextPage = useAppStore((s) => s.fetchWorkItemsNextPage)
|
||||
const countWorkItemsAcrossRepos = useAppStore((s) => s.countWorkItemsAcrossRepos)
|
||||
|
||||
@@ -6228,23 +6231,24 @@ export default function TaskPage(): React.JSX.Element {
|
||||
{ id: 'work-items-page-load-failed' }
|
||||
)
|
||||
} else {
|
||||
// Why: with a real count the clamp is refused, so without a toast
|
||||
// the click would look dead — the count mis-advertised the page.
|
||||
// countedTotalPages is a click-time closure read; worst case the
|
||||
// toast decision is off in a race, which is benign.
|
||||
if (countedTotalPages !== null && countedTotalPages > 0) {
|
||||
toast.error(
|
||||
// Why: with a real count the clamp is refused, so without feedback
|
||||
// the click would look dead — the count over-advertised; nothing
|
||||
// failed, so the copy stays neutral. The ref carries the committed
|
||||
// count, immune to the click-time closure race.
|
||||
const committedCount = countedTotalPagesRef.current
|
||||
if (committedCount !== null && committedCount > 0) {
|
||||
toast(
|
||||
translate(
|
||||
'auto.components.TaskPage.loadPageFailed',
|
||||
'Page {{value0}} could not be loaded from GitHub.',
|
||||
'auto.components.TaskPage.loadPageNoMoreResults',
|
||||
'No more results on page {{value0}}.',
|
||||
{ value0: String(target + 1) }
|
||||
),
|
||||
{ id: 'work-items-page-load-failed' }
|
||||
{ id: 'work-items-page-no-more-results' }
|
||||
)
|
||||
}
|
||||
setCountedTotalPages((previous) =>
|
||||
applyEmptyPageClamp(previous, { target, failedCount, errorTypes })
|
||||
)
|
||||
const next = applyEmptyPageClamp(committedCount, { target, failedCount, errorTypes })
|
||||
countedTotalPagesRef.current = next
|
||||
setCountedTotalPages(next)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -6270,7 +6274,6 @@ export default function TaskPage(): React.JSX.Element {
|
||||
paginationLoading,
|
||||
selectedRepos,
|
||||
currentPage,
|
||||
countedTotalPages,
|
||||
appliedTaskSearch,
|
||||
fetchWorkItemsNextPage,
|
||||
githubPageSize,
|
||||
@@ -6354,6 +6357,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setPages([page0])
|
||||
setCurrentPage(0)
|
||||
setCountedTotalPages(null)
|
||||
countedTotalPagesRef.current = null
|
||||
setProvenPageLimit(null)
|
||||
setTasksError(null)
|
||||
setFailedCount(0) // reset so a prior failure banner doesn't linger
|
||||
@@ -6462,6 +6466,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
// Why: the count overwrites unconditionally — proven window limits live
|
||||
// in provenPageLimit, so a late count can't be pinned by a speculative
|
||||
// end-of-data withdrawal, and can't resurrect proven-dead pages either.
|
||||
countedTotalPagesRef.current = countedPages
|
||||
setCountedTotalPages(countedPages)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1891,7 +1891,8 @@
|
||||
"noGithubSourceDetectedHint": "it may have no GitHub remote, or the source could not be resolved.",
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.",
|
||||
"loadPageUnreachable": "Page {{value0}} is beyond what GitHub search can return.",
|
||||
"loadPageFailed": "Page {{value0}} could not be loaded from GitHub."
|
||||
"loadPageFailed": "Page {{value0}} could not be loaded from GitHub.",
|
||||
"loadPageNoMoreResults": "No more results on page {{value0}}."
|
||||
},
|
||||
"Terminal": {
|
||||
"73768427cf": "Close",
|
||||
|
||||
@@ -1865,7 +1865,10 @@
|
||||
"75a38d7df8": "GitHub data is temporarily unavailable. Its API may be down, rate-limited, or unreachable. Please try again shortly.",
|
||||
"noGithubSourceDetected": "No GitHub source detected for",
|
||||
"noGithubSourceDetectedHint": "it may have no GitHub remote, or the source could not be resolved.",
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again."
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.",
|
||||
"loadPageUnreachable": "La página {{value0}} está más allá de lo que la búsqueda de GitHub puede devolver.",
|
||||
"loadPageFailed": "No se pudo cargar la página {{value0}} desde GitHub.",
|
||||
"loadPageNoMoreResults": "No hay más resultados en la página {{value0}}."
|
||||
},
|
||||
"Terminal": {
|
||||
"73768427cf": "Cerrar",
|
||||
|
||||
@@ -1865,7 +1865,10 @@
|
||||
"75a38d7df8": "GitHub data is temporarily unavailable. Its API may be down, rate-limited, or unreachable. Please try again shortly.",
|
||||
"noGithubSourceDetected": "No GitHub source detected for",
|
||||
"noGithubSourceDetectedHint": "it may have no GitHub remote, or the source could not be resolved.",
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again."
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.",
|
||||
"loadPageUnreachable": "ページ {{value0}} は GitHub 検索が返せる範囲を超えています。",
|
||||
"loadPageFailed": "GitHub からページ {{value0}} を読み込めませんでした。",
|
||||
"loadPageNoMoreResults": "ページ {{value0}} にはこれ以上結果がありません。"
|
||||
},
|
||||
"Terminal": {
|
||||
"73768427cf": "閉じる",
|
||||
|
||||
@@ -1865,7 +1865,10 @@
|
||||
"75a38d7df8": "GitHub data is temporarily unavailable. Its API may be down, rate-limited, or unreachable. Please try again shortly.",
|
||||
"noGithubSourceDetected": "No GitHub source detected for",
|
||||
"noGithubSourceDetectedHint": "it may have no GitHub remote, or the source could not be resolved.",
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again."
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.",
|
||||
"loadPageUnreachable": "{{value0}} 페이지는 GitHub 검색이 반환할 수 있는 범위를 벗어났습니다.",
|
||||
"loadPageFailed": "GitHub에서 {{value0}} 페이지를 불러오지 못했습니다.",
|
||||
"loadPageNoMoreResults": "{{value0}} 페이지에 더 이상 결과가 없습니다."
|
||||
},
|
||||
"Terminal": {
|
||||
"73768427cf": "닫기",
|
||||
|
||||
@@ -1865,7 +1865,10 @@
|
||||
"75a38d7df8": "GitHub data is temporarily unavailable. Its API may be down, rate-limited, or unreachable. Please try again shortly.",
|
||||
"noGithubSourceDetected": "No GitHub source detected for",
|
||||
"noGithubSourceDetectedHint": "it may have no GitHub remote, or the source could not be resolved.",
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again."
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.",
|
||||
"loadPageUnreachable": "第 {{value0}} 页超出了 GitHub 搜索可返回的范围。",
|
||||
"loadPageFailed": "无法从 GitHub 加载第 {{value0}} 页。",
|
||||
"loadPageNoMoreResults": "第 {{value0}} 页没有更多结果。"
|
||||
},
|
||||
"Terminal": {
|
||||
"73768427cf": "关闭",
|
||||
|
||||
Reference in New Issue
Block a user