Files
deepseek-harness/packages/experimental/forge-github/tests/gh.spec.ts
T

96 lines
3.7 KiB
TypeScript

import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { GhOutputLimitError, GhTimeoutError, GhUnavailableError, ghRunner } from '../src/gh.ts'
let root = ''
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-forge-gh-'))
})
afterEach(async () => {
vi.unstubAllEnvs()
await rm(root, { recursive: true, force: true })
})
/** A POSIX stand-in for gh whose first argument selects its behavior. */
async function fakeGh(): Promise<string> {
const path = join(root, 'gh')
await writeFile(path, [
'#!/bin/sh',
'case "$1" in',
' echo) printf "%s|" "$@"; printf "\\nprompt=%s token=%s pager=[%s]\\n" "$GH_PROMPT_DISABLED" "${GH_TOKEN:-unset}" "$GH_PAGER" ;;',
' input) cat ;;',
' fail) echo oops >&2; exit 3 ;;',
' sleep) sleep 5 ;;',
' big) head -c 5000 /dev/zero | tr "\\0" x ;;',
'esac',
'',
].join('\n'))
await chmod(path, 0o755)
return path
}
describe.skipIf(process.platform === 'win32')('gh runner', () => {
it('delivers review JSON through stdin without interpreting shell syntax', async () => {
const run = ghRunner({ ghCommand: await fakeGh(), timeoutMs: 5_000, maxOutputBytes: 10_000 })
const body = JSON.stringify({ body: 'Check `$(secret)`\n检查边界' })
await expect(run(['input'], new AbortController().signal, body)).resolves.toEqual({ exitCode: 0, stdout: body, stderr: '' })
})
it('runs gh with literal arguments and a scrubbed, non-interactive environment', async () => {
vi.stubEnv('GH_TOKEN', 'secret-token')
const run = ghRunner({ ghCommand: await fakeGh(), timeoutMs: 5_000, maxOutputBytes: 10_000 })
await expect(run(['echo', 'a b', '--flag'], new AbortController().signal)).resolves.toEqual({
exitCode: 0,
stdout: 'echo|a b|--flag|\nprompt=1 token=unset pager=[]',
stderr: '',
})
})
it('returns a nonzero exit code with its stderr', async () => {
const run = ghRunner({ ghCommand: await fakeGh(), timeoutMs: 5_000, maxOutputBytes: 10_000 })
await expect(run(['fail'], new AbortController().signal)).resolves.toEqual({ exitCode: 3, stdout: '', stderr: 'oops' })
})
it('rejects when gh outlives the timeout', async () => {
const run = ghRunner({ ghCommand: await fakeGh(), timeoutMs: 100, maxOutputBytes: 10_000 })
const result = run(['sleep'], new AbortController().signal)
await expect(result).rejects.toBeInstanceOf(GhTimeoutError)
await expect(result).rejects.toThrow('gh sleep timed out after 100ms')
})
it('rejects with the abort reason when cancelled', async () => {
const run = ghRunner({ ghCommand: await fakeGh(), timeoutMs: 5_000, maxOutputBytes: 10_000 })
const controller = new AbortController()
setTimeout(() => {
controller.abort(new Error('stop requested'))
}, 50)
await expect(run(['sleep'], controller.signal)).rejects.toThrow('stop requested')
})
it('rejects output beyond maxOutputBytes', async () => {
const run = ghRunner({ ghCommand: await fakeGh(), timeoutMs: 5_000, maxOutputBytes: 1_000 })
const result = run(['big'], new AbortController().signal)
await expect(result).rejects.toBeInstanceOf(GhOutputLimitError)
await expect(result).rejects.toThrow('gh big output exceeds maxOutputBytes')
})
it('reports an executable that cannot start', async () => {
const run = ghRunner({ ghCommand: join(root, 'missing-gh'), timeoutMs: 5_000, maxOutputBytes: 1_000 })
const result = run([], new AbortController().signal)
await expect(result).rejects.toBeInstanceOf(GhUnavailableError)
await expect(result).rejects.toThrow(/^gh could not start: /)
})
})