Files
deepseek-harness/packages/boot/plugin-manager/tests/git-transport.spec.ts
T
LogosVeritas 0939fd6549 fix(test): retry busy git transport fixture cleanup
Bound temporary-directory removal retries after probes and HTTP fixtures
finish, preserving cleanup errors when retries are exhausted.
2026-10-26 13:45:06 +08:00

155 lines
7.8 KiB
TypeScript

/** Real Git access probes that choose the transport of a self-hosted repository installation. */
import { once } from 'node:events'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import * as subprocess from 'execa'
import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest'
import { chooseGitTransport } from '../src/git-transport.ts'
import { parseInstallSpec } from '../src/install-spec.ts'
import { isolateGitCommandLineConfig } from './git-environment.ts'
vi.mock('execa', async (original) => {
const module = await original<typeof import('execa')>()
return { ...module, execa: vi.fn(module.execa) }
})
// These cases spawn real Git, so the host's command-line configuration group must not reach its children.
let restoreGitCommandLineConfig: () => void
beforeAll(() => { restoreGitCommandLineConfig = isolateGitCommandLineConfig() })
afterAll(() => { restoreGitCommandLineConfig() })
const SPEC = `git+https://git.example/team/sub/plugin.git#${'a'.repeat(40)}&path:/packages/a`
const SSH_SPEC = `git+ssh://[email protected]/team/sub/plugin.git#${'a'.repeat(40)}&path:/packages/a`
async function fixture() {
const dir = await mkdtemp(join(tmpdir(), 'dsh-git-transport-'))
// Retry Windows directory-busy errors after awaited probe cleanup.
onTestFinished(() => rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }))
const config = join(dir, 'git.config')
await writeFile(config, '')
const env = { GIT_CONFIG_GLOBAL: config, GIT_CONFIG_NOSYSTEM: '1' }
const git = (...args: string[]) => subprocess.execa('git', args, { env })
const repository = join(dir, 'plugin.git')
await git('init', '--bare', repository)
// ls-remote --exit-code needs a HEAD that names a commit.
const work = join(dir, 'work')
await git('init', work)
await git('-C', work, '-c', 'user.name=t', '-c', 'user.email=t@t', '-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'init')
await git('-C', work, 'push', repository, 'HEAD:refs/heads/main')
await git('-C', repository, 'symbolic-ref', 'HEAD', 'refs/heads/main')
const controller = new AbortController()
const tasks: Promise<unknown>[] = []
// Registered after the directory removal, so it runs first: no probe outlives the directory it runs in.
onTestFinished(async () => { controller.abort(); await Promise.allSettled(tasks) })
const choose = (spec: string, timeoutMs = 5000) => {
const task = chooseGitTransport(parseInstallSpec(spec), dir, { timeoutMs, signal: controller.signal, env })
tasks.push(task)
return task
}
return { dir, config, repository: pathToFileURL(repository).href, controller, choose }
}
/** An HTTP Git endpoint that answers every request with `handler`. */
async function server(handler: (request: IncomingMessage, response: ServerResponse) => void): Promise<string> {
const http = createServer(handler)
onTestFinished(async () => {
http.closeAllConnections()
await new Promise<void>((resolve, reject) => { http.close((error) => { if (error) reject(error); else resolve() }) })
})
http.listen(0, '127.0.0.1')
await once(http, 'listening')
const address = http.address()
if (address === null || typeof address === 'string') throw new Error('server did not bind a TCP port')
return `http://127.0.0.1:${String(address.port)}/`
}
const unauthorized = (_request: IncomingMessage, response: ServerResponse): void => {
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="fixture"' })
response.end()
}
it('keeps the HTTPS spec when HTTPS can read the repository', async () => {
const { config, repository, choose } = await fixture()
await writeFile(config, `[url "${repository}"]\n insteadOf = https://git.example/team/sub/plugin.git\n insteadOf = ssh://[email protected]/team/sub/plugin.git\n`)
expect(await choose(SPEC)).toBe(SPEC)
})
it('installs over SSH when HTTPS asks for credentials that SSH does not need', async () => {
const { config, repository, choose } = await fixture()
const http = await server(unauthorized)
await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n insteadOf = http://git.example/\n[url "${repository}"]\n insteadOf = ssh://[email protected]/team/sub/plugin.git\n[http]\n proxy =\n`)
expect(await choose(SPEC)).toBe(SSH_SPEC)
// The fragment is copied as written, not as URL serialization would encode it.
expect(await choose('git+https://git.example/team/sub/plugin.git#semver:<2 >1')).toBe('git+ssh://[email protected]/team/sub/plugin.git#semver:<2 >1')
expect(await choose('git+http://git.example/team/sub/plugin.git')).toBe('git+ssh://[email protected]/team/sub/plugin.git')
})
it('keeps the HTTPS spec when neither transport can read the repository', async () => {
const { config, choose } = await fixture()
const http = await server(unauthorized)
await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n[url "file:///nonexistent/plugin.git"]\n insteadOf = ssh://[email protected]/\n[http]\n proxy =\n`)
expect(await choose(SPEC)).toBe(SPEC)
})
it('lets configured credential helpers answer the HTTPS probe', async () => {
const { dir, config, repository, choose } = await fixture()
const expected = `Basic ${Buffer.from('person:token').toString('base64')}`
const http = await server((request, response) => {
if (request.headers.authorization === expected) {
// Any reply Git can parse as a ref advertisement is enough; a dumb-HTTP info/refs listing is the smallest.
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end(`${'b'.repeat(40)}\tHEAD\n`)
} else unauthorized(request, response)
})
const helper = join(dir, 'credential-helper.cjs')
await writeFile(helper, 'process.stdout.write("username=person\\npassword=token\\n")')
await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n[url "${repository}"]\n insteadOf = ssh://[email protected]/team/sub/plugin.git\n[http]\n proxy =\n`)
await subprocess.execa('git', ['config', '--file', config, 'credential.helper', `!"${process.execPath.replaceAll('\\', '/')}" "${helper.replaceAll('\\', '/')}"`])
expect(await choose(SPEC)).toBe(SPEC)
})
it.each([
'@acme/plugin',
'github:acme/plugin',
'https://gitlab.com/acme/plugin.git',
'git+https://github.com/acme/plugin.git#main',
'git+ssh://[email protected]/team/plugin.git',
'[email protected]:team/plugin.git',
'git+https://person:[email protected]/team/plugin.git',
'git+https://git.example/team/plugin.git?ref=x',
])('leaves %s to pnpm without probing', async (spec) => {
const { choose } = await fixture()
const run = vi.mocked(subprocess.execa)
run.mockClear()
expect(await choose(spec)).toBe(spec)
expect(run).not.toHaveBeenCalled()
})
it('ends an in-flight HTTPS probe on cancellation and keeps the spec', async () => {
const { config, controller, choose } = await fixture()
const requested = Promise.withResolvers<undefined>()
// Never answers, so only cancellation ends the probe.
const http = await server(() => { requested.resolve(undefined) })
await writeFile(config, `[url "${http}"]\n insteadOf = https://git.example/\n[http]\n proxy =\n`)
const run = vi.mocked(subprocess.execa)
run.mockClear()
const choosing = choose(SPEC, 60000)
await requested.promise
controller.abort()
expect(await choosing).toBe(SPEC)
expect(run).toHaveBeenCalledTimes(1)
expect(run.mock.calls[0]?.[1]).toEqual(['-c', 'core.askPass=', 'ls-remote', '--exit-code', '--', 'https://git.example/team/sub/plugin.git', 'HEAD'])
})
it('does not probe SSH after cancellation', async () => {
const { controller, choose } = await fixture()
const run = vi.mocked(subprocess.execa)
run.mockClear()
controller.abort()
expect(await choose(SPEC)).toBe(SPEC)
expect(run).toHaveBeenCalledTimes(1)
})