benchmark-npm-resolution.spec.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import {
  6. benchmarkNpmResolution,
  7. buildRegistryIndex,
  8. parseBenchmarkOptions,
  9. publishWorkspaceRange,
  10. resolveNpmPackageLock,
  11. runCommandWithTimeout,
  12. type RegistryIndex,
  13. } from './benchmark-npm-resolution.ts'
  14. const roots: string[] = []
  15. afterEach(() => {
  16. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  17. })
  18. function writeJson(root: string, path: string, value: unknown): void {
  19. const absolute = join(root, path)
  20. mkdirSync(dirname(absolute), { recursive: true })
  21. writeFileSync(absolute, `${JSON.stringify(value, null, 2)}\n`)
  22. }
  23. function processCanExecute(pid: number): boolean {
  24. try {
  25. process.kill(pid, 0)
  26. } catch (error) {
  27. if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false
  28. throw error
  29. }
  30. if (process.platform !== 'linux') return true
  31. try {
  32. const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
  33. const state = stat.slice(stat.lastIndexOf(')') + 2).split(/\s+/, 1)[0]
  34. return !/^[ZXx]$/.test(state ?? '')
  35. } catch (error) {
  36. if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
  37. throw error
  38. }
  39. }
  40. describe('npm resolution benchmark', () => {
  41. it('parses repeat, timeout, threshold, and ref options', () => {
  42. expect(parseBenchmarkOptions([])).toEqual({ runs: 1, timeoutMs: 300_000 })
  43. expect(parseBenchmarkOptions([
  44. '--runs', '3', '--timeout-ms', '45000', '--max-ms', '20000', '--ref', 'master',
  45. ])).toEqual({ runs: 3, timeoutMs: 45_000, maxMs: 20_000, ref: 'master' })
  46. expect(parseBenchmarkOptions(['--', '--runs', '2'])).toEqual({ runs: 2, timeoutMs: 300_000 })
  47. expect(() => parseBenchmarkOptions(['--runs', '0'])).toThrow('--runs must be a positive integer')
  48. })
  49. it('projects workspace protocols to published ranges', () => {
  50. expect(publishWorkspaceRange('workspace:^', '1.2.3')).toBe('^1.2.3')
  51. expect(publishWorkspaceRange('workspace:~', '1.2.3')).toBe('~1.2.3')
  52. expect(publishWorkspaceRange('workspace:*', '1.2.3')).toBe('1.2.3')
  53. expect(publishWorkspaceRange('^4.0.0', '1.2.3')).toBe('^4.0.0')
  54. })
  55. it('combines installed metadata with current publishable workspace fields', () => {
  56. const root = mkdtempSync(join(tmpdir(), 'dsh-npm-registry-index-'))
  57. roots.push(root)
  58. writeJson(root, 'node_modules/.pnpm/external@2.0.0/node_modules/external/package.json', {
  59. name: 'external',
  60. version: '2.0.0',
  61. dependencies: { child: '^1.0.0' },
  62. devDependencies: { ignored: '^1.0.0' },
  63. })
  64. writeJson(root, 'apps/cli/package.json', {
  65. name: '@deepseek-ai/dsh',
  66. version: '0.1.0',
  67. dependencies: { '@deepseek-ai/dsh-child': 'workspace:^', external: '^2.0.0' },
  68. devDependencies: { ignored: 'workspace:^' },
  69. })
  70. writeJson(root, 'packages/core/child/package.json', {
  71. name: '@deepseek-ai/dsh-child',
  72. version: '0.1.0',
  73. })
  74. const index = buildRegistryIndex(root)
  75. expect(index.get('external')?.get('2.0.0')).toMatchObject({ dependencies: { child: '^1.0.0' } })
  76. expect(index.get('@deepseek-ai/dsh')?.get('0.1.0')).toEqual({
  77. name: '@deepseek-ai/dsh',
  78. version: '0.1.0',
  79. dependencies: { '@deepseek-ai/dsh-child': '^0.1.0', external: '^2.0.0' },
  80. })
  81. })
  82. it('runs npm against the local registry without requesting an archive', async () => {
  83. const index: RegistryIndex = new Map([[
  84. '@deepseek-ai/dsh',
  85. new Map([['0.1.0', { name: '@deepseek-ai/dsh', version: '0.1.0' }]]),
  86. ]])
  87. const result = await benchmarkNpmResolution(index, '0.1.0', 10_000)
  88. expect(result.durationMs).toBeGreaterThan(0)
  89. expect(result.registryRequests).toBeGreaterThan(0)
  90. expect(result.archiveRequests).toBe(0)
  91. expect(result.unknownPackages).toEqual([])
  92. })
  93. it('returns npm placement for two aliased package versions without requesting archives', async () => {
  94. const index: RegistryIndex = new Map([[
  95. '@deepseek-ai/dsh',
  96. new Map([
  97. ['0.1.0', { name: '@deepseek-ai/dsh', version: '0.1.0' }],
  98. ['0.2.0', { name: '@deepseek-ai/dsh', version: '0.2.0' }],
  99. ]),
  100. ]])
  101. const result = await resolveNpmPackageLock(index, {
  102. '@deepseek-ai/dsh': '0.2.0',
  103. 'dsh-previous': 'npm:@deepseek-ai/dsh@0.1.0',
  104. }, 10_000)
  105. expect(result.archiveRequests).toBe(0)
  106. expect(result.packageLock.packages['node_modules/@deepseek-ai/dsh']?.version).toBe('0.2.0')
  107. expect(result.packageLock.packages['node_modules/dsh-previous']).toMatchObject({
  108. name: '@deepseek-ai/dsh',
  109. version: '0.1.0',
  110. })
  111. })
  112. it('isolates peer resolution from inherited npm configuration', async () => {
  113. const root = mkdtempSync(join(tmpdir(), 'dsh-hostile-npm-config-'))
  114. roots.push(root)
  115. const userConfig = join(root, 'user.npmrc')
  116. writeFileSync(userConfig, '@deepseek-ai:registry=http://127.0.0.1:1/\nlegacy-peer-deps=true\nomit=peer\n')
  117. const previous = {
  118. userConfig: process.env.npm_config_userconfig,
  119. legacyPeerDeps: process.env.npm_config_legacy_peer_deps,
  120. omit: process.env.npm_config_omit,
  121. }
  122. process.env.npm_config_userconfig = userConfig
  123. process.env.npm_config_legacy_peer_deps = 'true'
  124. process.env.npm_config_omit = 'peer'
  125. try {
  126. const index: RegistryIndex = new Map([
  127. ['@deepseek-ai/dsh', new Map([['0.1.0', {
  128. name: '@deepseek-ai/dsh',
  129. version: '0.1.0',
  130. peerDependencies: { '@deepseek-ai/dsh-peer': '1.0.0' },
  131. }]])],
  132. ['@deepseek-ai/dsh-peer', new Map([['1.0.0', {
  133. name: '@deepseek-ai/dsh-peer',
  134. version: '1.0.0',
  135. }]])],
  136. ])
  137. const result = await resolveNpmPackageLock(index, { '@deepseek-ai/dsh': '0.1.0' }, 10_000)
  138. expect(result.archiveRequests).toBe(0)
  139. expect(result.packageLock.packages['node_modules/@deepseek-ai/dsh-peer']?.version).toBe('1.0.0')
  140. } finally {
  141. if (previous.userConfig === undefined) delete process.env.npm_config_userconfig
  142. else process.env.npm_config_userconfig = previous.userConfig
  143. if (previous.legacyPeerDeps === undefined) delete process.env.npm_config_legacy_peer_deps
  144. else process.env.npm_config_legacy_peer_deps = previous.legacyPeerDeps
  145. if (previous.omit === undefined) delete process.env.npm_config_omit
  146. else process.env.npm_config_omit = previous.omit
  147. }
  148. })
  149. it.skipIf(process.platform === 'win32')('force-kills a timed-out process tree', async () => {
  150. const source = [
  151. "const { spawn } = require('node:child_process')",
  152. "process.on('SIGTERM', () => {})",
  153. 'const child = spawn(process.execPath, [\'-e\', "process.on(\'SIGTERM\', () => {}); setInterval(() => {}, 1000)"], { stdio: \'ignore\' })',
  154. 'console.log(child.pid)',
  155. 'setInterval(() => {}, 1000)',
  156. ].join(';')
  157. let descendantPid: number | undefined
  158. try {
  159. const result = await runCommandWithTimeout(process.execPath, ['-e', source], {
  160. cwd: process.cwd(),
  161. env: process.env,
  162. timeoutMs: 1_000,
  163. terminationGraceMs: 100,
  164. })
  165. const reportedPid = Number.parseInt(result.output.trim(), 10)
  166. if (!Number.isSafeInteger(reportedPid)) throw new Error(`child reported invalid pid ${result.output.trim()}`)
  167. descendantPid = reportedPid
  168. expect(result.timedOut).toBe(true)
  169. expect(result.signal).toBe('SIGKILL')
  170. await expect.poll(() => processCanExecute(reportedPid), { timeout: 5_000 }).toBe(false)
  171. } finally {
  172. if (descendantPid !== undefined && Number.isSafeInteger(descendantPid)) {
  173. try {
  174. process.kill(descendantPid, 'SIGKILL')
  175. } catch (error) {
  176. if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
  177. }
  178. }
  179. }
  180. })
  181. })