/** * test/http/gh.test.ts (W3 PR + CI status chip) — pure parsers + getPrStatus. * * Two layers (mirrors diff.test.ts): * 1. Pure, deterministic: summarizeChecks / parsePrView / classifyGhFailure fed * canned gh output — the bulk of gh.ts logic, fully covered without spawning * gh. Never throws; ' const s = parsePrView(JSON.stringify({ number: 1, state: 'OPEN', title: evil })) expect(s.availability).toBe('ok') expect(s.title).toBe(evil) }) }) // ── classifyGhFailure ───────────────────────────────────────────────────────── describe('classifyGhFailure', () => { it('maps a spawn ENOENT to not-installed', () => { expect(classifyGhFailure({ code: 'ENOENT', stderr: '' })).toBe('not-installed') }) it('maps auth-related stderr to unauthenticated', () => { for (const stderr of [ 'run gh auth login to authenticate', 'you are not logged into any GitHub hosts', 'authentication required', 'HTTP 401: Bad credentials', ]) { expect(classifyGhFailure({ code: 1, stderr })).toBe('unauthenticated') } }) it('maps no-PR / no-remote stderr to no-pr', () => { for (const stderr of [ 'no pull requests found for branch "feature/x"', 'no default remote repository has been set', 'no git remotes found', ]) { expect(classifyGhFailure({ code: 1, stderr })).toBe('no-pr') } }) it('maps any other non-zero exit (incl. maxBuffer overflow) to error', () => { expect(classifyGhFailure({ code: 1, stderr: 'something exploded' })).toBe('error') expect(classifyGhFailure({ code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER', stderr: '' })).toBe('error') expect(classifyGhFailure({ code: undefined, stderr: '' })).toBe('error') }) }) // ── getPrStatus (cache / dedupe / branch-key / disabled) ────────────────────── /** A runner that resolves canned exec results and records call count. */ function fakeRunner(result: GhExecResult): { runner: GhRunner; calls: () => number } { const spy = vi.fn(async () => result) return { runner: spy, calls: () => spy.mock.calls.length } } async function writeHead(repoPath: string, branch: string): Promise { await fs.writeFile(path.join(repoPath, '.git', 'HEAD'), `ref: refs/heads/${branch}\n`, 'utf8') } describe('getPrStatus', () => { let repoPath: string beforeEach(async () => { _clearPrCache() repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-gh-test-')) await fs.mkdir(path.join(repoPath, '.git'), { recursive: true }) await writeHead(repoPath, 'main') }) afterEach(async () => { _clearPrCache() await fs.rm(repoPath, { recursive: true, force: true }) }) it('resolves availability:disabled WITHOUT invoking the runner when ghEnabled:false', async () => { const { runner, calls } = fakeRunner({ ok: true, stdout: okJson, stderr: '', code: 0 }) const s = await getPrStatus(repoPath, { ...CFG, ghEnabled: false }, runner) expect(s).toEqual({ availability: 'disabled' }) expect(calls()).toBe(0) }) it('returns parsed status and caches it (second call does not re-run gh)', async () => { const { runner, calls } = fakeRunner({ ok: true, stdout: okJson, stderr: '', code: 0 }) const first = await getPrStatus(repoPath, CFG, runner) expect(first.availability).toBe('ok') expect(first.number).toBe(12) await getPrStatus(repoPath, CFG, runner) expect(calls()).toBe(1) // served from cache }) it('shares one in-flight run across two concurrent calls for the same path', async () => { const { runner, calls } = fakeRunner({ ok: true, stdout: okJson, stderr: '', code: 0 }) const [a, b] = await Promise.all([ getPrStatus(repoPath, CFG, runner), getPrStatus(repoPath, CFG, runner), ]) expect(a.availability).toBe('ok') expect(b.availability).toBe('ok') expect(calls()).toBe(1) }) it('re-runs gh after _clearPrCache', async () => { const { runner, calls } = fakeRunner({ ok: true, stdout: okJson, stderr: '', code: 0 }) await getPrStatus(repoPath, CFG, runner) _clearPrCache() await getPrStatus(repoPath, CFG, runner) expect(calls()).toBe(2) }) it('busts the cache when the branch (.git/HEAD) changes', async () => { const { runner, calls } = fakeRunner({ ok: true, stdout: okJson, stderr: '', code: 0 }) await getPrStatus(repoPath, CFG, runner) await writeHead(repoPath, 'other-branch') await getPrStatus(repoPath, CFG, runner) expect(calls()).toBe(2) // key includes the branch }) it('degrades to no-pr when gh exits non-zero with a no-PR stderr (never throws)', async () => { const { runner } = fakeRunner({ ok: false, stdout: '', stderr: 'no pull requests found for branch "main"', code: 1, }) const s = await getPrStatus(repoPath, CFG, runner) expect(s).toEqual({ availability: 'no-pr' }) }) it('degrades to not-installed on a spawn ENOENT', async () => { const { runner } = fakeRunner({ ok: false, stdout: '', stderr: '', code: 'ENOENT' }) const s = await getPrStatus(repoPath, CFG, runner) expect(s).toEqual({ availability: 'not-installed' }) }) })