Files
deepseek-harness/packages/sdk/plugin-fetch/tests/pacote-fetcher.spec.ts
imccyu 825a63ab01 feat(sdk): add dsh-plugin-fetch source + fetcher seam
Greenfield #2 "建插件" modules for the forthcoming `dsh-sdk create <source>`
command, in a new foundation-independent package so it touches none of the
dsh-scripts / dsh-helper / create-sdk hotspots the foundation refactor edits.

- `resolvePluginSource(spec)` parses `owner/repo[/subdir]#ref` (github) or
  `pkg@version` (npm) into a `PluginSource` discriminated union, failing loud on
  an ambiguous or malformed spec.
- `PluginFetcher<S>` seam + `fetchPlugin` tag dispatch returning a common
  `FetchedPlugin` (temp dir + immutable provenance).
- `GigetFetcher` (github) over @bluwy/giget-core: resolve `#ref` to a commit SHA
  first, download that SHA; provenance pins the SHA. Chosen over unjs/giget for
  its single runtime dep and absent install/action surface.
- `PacoteFetcher` (npm) over pacote: resolve the manifest, then extract the
  tarball verified against its registry integrity. Registry-only is enforced by
  the source resolver; extract runs no lifecycle scripts.
- Branded `CommitSha`/`Integrity`; network + temp-dir boundaries are injected so
  the logic is unit-tested at 100% per-file coverage without network.

Wiring (package.json pin, cordis.yml via ProjectEditSession with a confirmed
diff, install --ignore-scripts) and the launcher command registration land
later with the foundation.
2026-07-18 16:08:12 +08:00

109 lines
4.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { extract as pacoteExtract, manifest as pacoteManifest } from 'pacote'
import {
createPacoteFetcher,
NPM_TEMP_PREFIX,
PacoteFetcher,
type PacoteApi,
type PacoteFetcherDeps,
} from '../src/pacote-fetcher.ts'
import type { NpmSource } from '../src/source.ts'
vi.mock('pacote', () => ({ manifest: vi.fn(), extract: vi.fn() }))
const INTEGRITY = 'sha512-abcABC123+/=='
const RESOLVED = 'https://registry.npmjs.org/plugin/-/plugin-1.2.3.tgz'
function fakePacote(): PacoteApi {
return {
manifest: vi.fn(async () => ({ _resolved: RESOLVED, _integrity: INTEGRITY, version: '1.2.3' })),
extract: vi.fn(async () => ({ resolved: RESOLVED, integrity: INTEGRITY })),
}
}
function deps(overrides: Partial<PacoteFetcherDeps> = {}): PacoteFetcherDeps {
return {
pacote: fakePacote(),
createTempDir: vi.fn(async () => '/tmp/dsh-plugin-npm-abc'),
...overrides,
}
}
const SOURCE: NpmSource = { kind: 'npm', name: 'plugin', version: '^1.0.0' }
describe('PacoteFetcher.fetch', () => {
it('resolves the manifest, extracts with integrity, and reports provenance', async () => {
const d = deps()
const result = await new PacoteFetcher(d).fetch(SOURCE)
expect(d.pacote.manifest).toHaveBeenCalledWith('plugin@^1.0.0', {})
expect(d.createTempDir).toHaveBeenCalledWith(NPM_TEMP_PREFIX)
expect(d.pacote.extract).toHaveBeenCalledWith('plugin@^1.0.0', '/tmp/dsh-plugin-npm-abc', {
resolved: RESOLVED,
integrity: INTEGRITY,
})
expect(result).toEqual({
dir: '/tmp/dsh-plugin-npm-abc',
source: SOURCE,
provenance: { kind: 'npm', version: '1.2.3', resolved: RESOLVED, integrity: INTEGRITY },
})
})
it('forwards a configured registry to both manifest and extract', async () => {
const d = deps({ registry: 'https://npm.internal/' })
await new PacoteFetcher(d).fetch(SOURCE)
expect(d.pacote.manifest).toHaveBeenCalledWith('plugin@^1.0.0', { registry: 'https://npm.internal/' })
expect(d.pacote.extract).toHaveBeenCalledWith('plugin@^1.0.0', '/tmp/dsh-plugin-npm-abc', {
registry: 'https://npm.internal/',
resolved: RESOLVED,
integrity: INTEGRITY,
})
})
it('rejects a registry integrity that is not a valid SRI', async () => {
const pacote = fakePacote()
pacote.extract = vi.fn(async () => ({ resolved: RESOLVED, integrity: 'not-sri' }))
await expect(new PacoteFetcher(deps({ pacote })).fetch(SOURCE)).rejects.toThrow(
/invalid subresource integrity/,
)
})
it('exposes its source kind', () => {
expect(new PacoteFetcher(deps()).kind).toBe('npm')
})
})
describe('createPacoteFetcher', () => {
const manifestMock = vi.mocked(pacoteManifest)
const extractMock = vi.mocked(pacoteExtract)
beforeEach(() => {
manifestMock.mockReset()
extractMock.mockReset()
// The real overloaded pacote manifest returns a much wider shape; the fetcher reads only these fields.
manifestMock.mockResolvedValue(
{ _resolved: RESOLVED, _integrity: INTEGRITY, version: '1.2.3' } as unknown as Awaited<
ReturnType<typeof pacoteManifest>
>,
)
extractMock.mockResolvedValue({ from: 'plugin@1.2.3', resolved: RESOLVED, integrity: INTEGRITY })
})
afterEach(() => vi.clearAllMocks())
it('wires the real pacote resolve/extract surface', async () => {
const result = await createPacoteFetcher().fetch(SOURCE)
expect(manifestMock).toHaveBeenCalledWith('plugin@^1.0.0', {})
expect(extractMock).toHaveBeenCalledWith('plugin@^1.0.0', expect.stringContaining(NPM_TEMP_PREFIX), {
resolved: RESOLVED,
integrity: INTEGRITY,
})
expect(result.provenance).toEqual({ kind: 'npm', version: '1.2.3', resolved: RESOLVED, integrity: INTEGRITY })
})
it('forwards a configured registry through the real surface', async () => {
await createPacoteFetcher({ registry: 'https://npm.internal/' }).fetch(SOURCE)
expect(manifestMock).toHaveBeenCalledWith('plugin@^1.0.0', { registry: 'https://npm.internal/' })
})
})