Files
orca/tests/e2e/github-cli-stall-repro.spec.ts
T
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00

133 lines
4.4 KiB
TypeScript

import { execSync } from 'node:child_process'
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test as base, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
const fakeGhDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-fake-gh-'))
const fakeGhBody = `#!/usr/bin/env node
const args = process.argv.slice(2)
const joined = args.join(' ')
if (args[0] === 'auth' && args[1] === 'status') {
console.error('github.com\\n ✓ Logged in to github.com account e2e (GITHUB_TOKEN)')
process.exit(0)
}
if (args[0] === 'api' && args[1] === 'user') {
console.log(JSON.stringify({ login: 'e2e' }))
process.exit(0)
}
if (args[0] === 'api' && args.includes('rate_limit')) {
console.log(JSON.stringify({ resources: { core: { limit: 5000, remaining: 5000, reset: 0 }, graphql: { limit: 5000, remaining: 5000, reset: 0 }, search: { limit: 30, remaining: 30, reset: 0 } } }))
process.exit(0)
}
if (args[0] === 'issue' && args[1] === 'list') {
console.log('[]')
process.exit(0)
}
if (args[0] === 'pr' && args[1] === 'list') {
console.log('[]')
process.exit(0)
}
if (args[0] === 'api' && (args[1] === 'graphql' || joined.includes('repos/acme/repo/issues/5388') || joined.includes('repos/acme/repo/pulls/5388'))) {
setTimeout(() => {}, 60_000)
return
}
console.error('fake gh: unhandled ' + joined)
process.exit(1)
`
const fakeGhPath = path.join(fakeGhDir, process.platform === 'win32' ? 'gh.cmd' : 'gh')
if (process.platform === 'win32') {
writeFileSync(fakeGhPath, '@echo off\nnode "%~dp0\\fake-gh.js" %*\n')
writeFileSync(path.join(fakeGhDir, 'fake-gh.js'), fakeGhBody)
} else {
writeFileSync(fakeGhPath, fakeGhBody)
chmodSync(fakeGhPath, 0o755)
}
const test = base.extend({
launchEnv: [
{
PATH: `${fakeGhDir}${path.delimiter}${process.env.PATH ?? ''}`,
ORCA_GH_EXEC_TIMEOUT_MS: '1000'
},
{ option: true }
]
})
test.afterAll(() => {
rmSync(fakeGhDir, { recursive: true, force: true })
})
function configureGitHubRemote(repoPath: string): void {
try {
execSync('git remote remove origin', { cwd: repoPath, stdio: 'ignore' })
} catch {
// Missing origin is fine for the disposable E2E repo.
}
try {
execSync('git remote remove upstream', { cwd: repoPath, stdio: 'ignore' })
} catch {
// Missing upstream is fine for the disposable E2E repo.
}
execSync('git remote add origin https://github.com/acme/repo.git', {
cwd: repoPath,
stdio: 'pipe'
})
execSync('git remote add upstream https://github.com/acme/repo.git', {
cwd: repoPath,
stdio: 'pipe'
})
}
test('GitHub Tasks drawer recovers when gh stalls on issue details', async ({
orcaPage,
testRepoPath
}) => {
configureGitHubRemote(testRepoPath)
await waitForSessionReady(orcaPage)
const { repoId } = await orcaPage.evaluate((repoPath) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const repo = store.getState().repos.find((candidate) => candidate.path === repoPath)
if (!repo) {
throw new Error(`Expected repo to be loaded: ${repoPath}`)
}
const item = {
id: 'issue-5388',
type: 'issue',
number: 5388,
title: 'Issue detail fetch that hangs in gh',
state: 'open',
url: 'https://github.com/acme/repo/issues/5388',
labels: [],
updatedAt: '2026-06-15T20:00:00.000Z',
author: 'octocat',
repoId: repo.id
}
store.getState().openTaskPage({ taskSource: 'github', openGitHubWorkItem: item })
return { repoId: repo.id }
}, testRepoPath)
const drawer = orcaPage
.getByRole('dialog')
.filter({ hasText: 'Issue detail fetch that hangs in gh' })
.last()
await expect(drawer).toBeVisible()
// Why: this is the user-visible regression signal. Before ghExecFileAsync had
// a default timeout, the drawer's pending details promise never settled and
// the conversation pane stayed stuck in its loading shell. The main GitHub
// details service degrades failed detail fetches to an empty shell, so the
// stable visible proof is that the drawer becomes usable and stops spinning.
await expect(drawer.getByText('No description provided.')).toBeVisible({ timeout: 5_000 })
await expect(drawer.getByText('No comments yet.')).toBeVisible()
await expect(drawer.locator('.animate-spin')).toHaveCount(0)
expect(repoId).toBeTruthy()
})