/**
* 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' })
})
})