coverage-partitions.spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import { access, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { dirname, join } from 'node:path'
  4. import { afterEach, describe, expect, it, vi } from 'vitest'
  5. import {
  6. COVERAGE_PARTITION_MODE_ENV,
  7. COVERAGE_PARTITIONS_ENV,
  8. COVERAGE_TEST_TIMEOUT_ENV,
  9. CoveragePartitionCoordinator,
  10. assignWeightedPartitions,
  11. collectPartitionDurations,
  12. coverageTestTimeoutArgs,
  13. forwardedCoverageArgs,
  14. parseCoveragePartitionCount,
  15. parseListOutput,
  16. readFileDurations,
  17. writeFileDurations,
  18. type CoverageCommand,
  19. type CoverageCommandResult,
  20. type CoveragePartitionCoordinatorOptions,
  21. } from './coverage-partitions.ts'
  22. const passed: CoverageCommandResult = { exitCode: 0, signalCode: null }
  23. /** Every temporary root created by this file, removed after each test. */
  24. const roots: string[] = []
  25. afterEach(async () => {
  26. vi.restoreAllMocks()
  27. for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
  28. })
  29. async function writeBlob(command: CoverageCommand): Promise<void> {
  30. if (command.blobPath === undefined) return
  31. await mkdir(dirname(command.blobPath), { recursive: true })
  32. await writeFile(command.blobPath, '{}')
  33. }
  34. async function temporaryRoot(): Promise<string> {
  35. const root = await mkdtemp(join(tmpdir(), 'dsh-coverage-partitions-'))
  36. roots.push(root)
  37. return root
  38. }
  39. /** Write a Vitest results cache under a temporary root. */
  40. async function writeVitestCache(root: string, entries: Array<[string, { duration: number }]>): Promise<void> {
  41. const cacheDir = join(root, 'node_modules/.vite/vitest/cache-hash')
  42. await mkdir(cacheDir, { recursive: true })
  43. await writeFile(join(cacheDir, 'results.json'), JSON.stringify({ version: '4.1.8', results: entries }))
  44. }
  45. /** Run the coordinator and capture every partition config's source. */
  46. async function runCoordinatorReadingConfigs(
  47. root: string,
  48. options: Omit<CoveragePartitionCoordinatorOptions, 'root' | 'pnpmEntrypoint' | 'runCommand'>,
  49. ): Promise<Map<string, string>> {
  50. const configContents = new Map<string, string>()
  51. const runCommand = vi.fn(async (command: CoverageCommand) => {
  52. const configArgument = command.args.find(argument => argument.startsWith('--config='))
  53. if (configArgument !== undefined) {
  54. configContents.set(command.label, await readFile(join(root, configArgument.slice('--config='.length)), 'utf8'))
  55. }
  56. await writeBlob(command)
  57. return passed
  58. })
  59. const coordinator = new CoveragePartitionCoordinator({
  60. root,
  61. pnpmEntrypoint: '/pnpm.cjs',
  62. runCommand,
  63. ...options,
  64. })
  65. await expect(coordinator.run()).resolves.toBe(0)
  66. return configContents
  67. }
  68. function successfulCommandRecorder(commands: CoverageCommand[]) {
  69. return vi.fn(async (command: CoverageCommand) => {
  70. commands.push(command)
  71. await writeBlob(command)
  72. return passed
  73. })
  74. }
  75. describe('coverage partition count', () => {
  76. it.each([
  77. [undefined, undefined],
  78. ['', undefined],
  79. ['2', 2],
  80. ['3', 3],
  81. ])('parses %j as %j', (raw, expected) => {
  82. expect(parseCoveragePartitionCount(raw)).toBe(expected)
  83. })
  84. it.each(['0', '1', '2.5', '02', 'many'])('rejects %j', (raw) => {
  85. expect(() => parseCoveragePartitionCount(raw))
  86. .toThrow(`${COVERAGE_PARTITIONS_ENV} must be an integer greater than 1`)
  87. })
  88. })
  89. describe('coverage partition timeout', () => {
  90. it('applies one configured timeout to tests, polling, and hooks', () => {
  91. expect(coverageTestTimeoutArgs('30000')).toEqual([
  92. '--testTimeout=30000',
  93. '--expect.poll.timeout=30000',
  94. '--hookTimeout=30000',
  95. ])
  96. })
  97. it('keeps Vitest defaults when the timeout is absent', () => {
  98. expect(coverageTestTimeoutArgs(undefined)).toEqual([])
  99. })
  100. it('rejects invalid timeout input', () => {
  101. expect(() => coverageTestTimeoutArgs('0'))
  102. .toThrow(`${COVERAGE_TEST_TIMEOUT_ENV} must be a positive integer`)
  103. })
  104. })
  105. describe('coverage forwarded arguments', () => {
  106. it('removes one package-script separator', () => {
  107. expect(forwardedCoverageArgs(['--', 'scripts/example.spec.ts'])).toEqual(['scripts/example.spec.ts'])
  108. })
  109. it('preserves direct arguments and a subsequent Vitest separator', () => {
  110. expect(forwardedCoverageArgs(['--testNamePattern=example'])).toEqual(['--testNamePattern=example'])
  111. expect(forwardedCoverageArgs(['--', '--', 'example'])).toEqual(['--', 'example'])
  112. })
  113. })
  114. describe('weighted partition assignment', () => {
  115. it('seeds the heaviest files into different partitions', () => {
  116. const weights = new Map<string, number>([
  117. ['packages/a/tests/heavy-1.spec.ts', 100],
  118. ['packages/a/tests/heavy-2.spec.ts', 90],
  119. ['packages/a/tests/heavy-3.spec.ts', 80],
  120. ['packages/a/tests/light.spec.ts', 1],
  121. ])
  122. const buckets = assignWeightedPartitions([...weights.keys()], weights, 3)
  123. expect(buckets).toHaveLength(3)
  124. for (const bucket of buckets) {
  125. expect(bucket).not.toHaveLength(0)
  126. expect(bucket.filter(file => file.includes('heavy'))).toHaveLength(1)
  127. }
  128. })
  129. it('balances total weight across partitions', () => {
  130. const files = Array.from({ length: 20 }, (_, index) => `packages/a/tests/file-${index}.spec.ts`)
  131. const weights = new Map(files.map((file, index) => [file, (index % 7) + 1]))
  132. const buckets = assignWeightedPartitions(files, weights, 4)
  133. const sums = buckets.map(bucket => bucket.reduce((sum, file) => sum + (weights.get(file) ?? 0), 0))
  134. const spread = Math.max(...sums) - Math.min(...sums)
  135. expect(spread).toBeLessThanOrEqual(7)
  136. })
  137. it('steers assignment by weight, not by file count', () => {
  138. // Weight-aware LPT balances three buckets to 1500 each. A file-count-only
  139. // rule pairs the heaviest file with the fourth (1700), so the assertion
  140. // only passes when recorded weights steer the assignment.
  141. const weights = new Map<string, number>([
  142. ['a.spec.ts', 1000],
  143. ['b.spec.ts', 900],
  144. ['c.spec.ts', 800],
  145. ['d.spec.ts', 700],
  146. ['e.spec.ts', 600],
  147. ['f.spec.ts', 500],
  148. ])
  149. const buckets = assignWeightedPartitions([...weights.keys()], weights, 3)
  150. const sums = buckets.map(bucket => bucket.reduce((sum, file) => sum + (weights.get(file) ?? 0), 0))
  151. expect(Math.max(...sums)).toBeLessThanOrEqual(1550)
  152. })
  153. it('leaves trailing empty buckets when files are scarce', () => {
  154. const files = ['a.spec.ts', 'b.spec.ts']
  155. const buckets = assignWeightedPartitions(files, new Map(), 4)
  156. expect(buckets.map(bucket => bucket.length).sort()).toEqual([0, 0, 1, 1])
  157. })
  158. it('returns one empty bucket per partition for an empty inventory', () => {
  159. expect(assignWeightedPartitions([], new Map(), 3)).toEqual([[], [], []])
  160. })
  161. it('assigns unknown-weight files evenly', () => {
  162. const files = ['a.spec.ts', 'b.spec.ts', 'c.spec.ts', 'd.spec.ts']
  163. const buckets = assignWeightedPartitions(files, new Map(), 2)
  164. expect(buckets.map(bucket => bucket.length).sort()).toEqual([2, 2])
  165. })
  166. })
  167. describe('coverage file inventory', () => {
  168. it('parses vitest list --filesOnly output, keeps project ownership, and drops exempt suites', async () => {
  169. const root = await temporaryRoot()
  170. const exemptDir = join(root, 'packages/experimental/webworker-runtime/tests')
  171. await mkdir(exemptDir, { recursive: true })
  172. await writeFile(join(exemptDir, 'transform-corpus.spec.ts'), '')
  173. const output = [
  174. '[thread-safe] packages/a/tests/a.spec.ts',
  175. '[process-bound] packages/b/tests/b.spec.ts',
  176. '[thread-safe] packages/experimental/webworker-runtime/tests/transform-corpus.spec.ts',
  177. 'not a test line',
  178. ].join('\n')
  179. const inventory = parseListOutput(output, root)
  180. expect(inventory.files).toEqual([
  181. 'packages/a/tests/a.spec.ts',
  182. 'packages/b/tests/b.spec.ts',
  183. ])
  184. expect(inventory.projectOf.get('packages/a/tests/a.spec.ts')).toBe('thread-safe')
  185. expect(inventory.projectOf.get('packages/b/tests/b.spec.ts')).toBe('process-bound')
  186. })
  187. it('removes every Typert package from instrumented files and project assignments', async () => {
  188. const root = await temporaryRoot()
  189. const typertFiles = [
  190. 'packages/typert/generator/tests/type-model.spec.ts',
  191. 'packages/typert/loader/tests/loader.spec.ts',
  192. 'packages/typert/protocol/tests/protocol.spec.ts',
  193. 'packages/typert/registry/tests/typert.spec.ts',
  194. 'packages/typert/future/tests/nested/client.spec.tsx',
  195. ]
  196. for (const file of typertFiles) {
  197. await mkdir(dirname(join(root, file)), { recursive: true })
  198. await writeFile(join(root, file), '')
  199. }
  200. const retained = 'packages/api/gateway/tests/rpc.spec.ts'
  201. const inventory = parseListOutput(
  202. [...typertFiles, retained].map(file => `[thread-safe] ${file}`).join('\n'),
  203. root,
  204. )
  205. expect(inventory.files).toEqual([retained])
  206. expect([...inventory.projectOf]).toEqual([[retained, 'thread-safe']])
  207. })
  208. it('averages recorded durations per file from the results cache', async () => {
  209. const root = await temporaryRoot()
  210. await writeVitestCache(root, [
  211. ['thread-safe:packages/a/tests/x.spec.ts', { duration: 10 }],
  212. ['process-bound:packages/a/tests/x.spec.ts', { duration: 30 }],
  213. ['thread-safe:packages/a/tests/y.spec.ts', { duration: 5 }],
  214. ])
  215. const durations = readFileDurations(root)
  216. expect(durations.get('packages/a/tests/x.spec.ts')).toBe(20)
  217. expect(durations.get('packages/a/tests/y.spec.ts')).toBe(5)
  218. })
  219. it('prefers the persisted duration file over the vitest cache', async () => {
  220. const root = await temporaryRoot()
  221. await writeVitestCache(root, [['thread-safe:packages/a/tests/x.spec.ts', { duration: 100 }]])
  222. writeFileDurations(root, new Map([['packages/a/tests/x.spec.ts', 42]]))
  223. expect(readFileDurations(root).get('packages/a/tests/x.spec.ts')).toBe(42)
  224. })
  225. it('merges new durations into the persisted file', async () => {
  226. const root = await temporaryRoot()
  227. writeFileDurations(root, new Map([['packages/a/tests/x.spec.ts', 42]]))
  228. writeFileDurations(root, new Map([
  229. ['packages/a/tests/x.spec.ts', 55],
  230. ['packages/a/tests/y.spec.ts', 7],
  231. ]))
  232. const durations = readFileDurations(root)
  233. expect(durations.get('packages/a/tests/x.spec.ts')).toBe(55)
  234. expect(durations.get('packages/a/tests/y.spec.ts')).toBe(7)
  235. })
  236. it('extracts per-file durations from partition json reports', async () => {
  237. const root = await temporaryRoot()
  238. const report = join(root, 'partition-1.report.json')
  239. await writeFile(report, JSON.stringify({
  240. testResults: [
  241. { name: join(root, 'packages/a/tests/x.spec.ts'), startTime: 1000, endTime: 1500 },
  242. { name: 'not-a-spec', startTime: 1, endTime: 2 },
  243. ],
  244. }))
  245. const durations = collectPartitionDurations([report], root)
  246. expect(durations.get('packages/a/tests/x.spec.ts')).toBe(500)
  247. })
  248. })
  249. describe('coverage partition coordinator', () => {
  250. const weightedFiles = ['a.spec.ts', 'b.spec.ts', 'c.spec.ts']
  251. const weightedDurations = new Map([
  252. ['a.spec.ts', 100],
  253. ['b.spec.ts', 50],
  254. ['c.spec.ts', 10],
  255. ])
  256. const weightedProjects = new Map([
  257. ['a.spec.ts', 'thread-safe'],
  258. ['b.spec.ts', 'process-bound'],
  259. ['c.spec.ts', 'process-bound'],
  260. ])
  261. it('runs every single-worker partition before one merged threshold check', async () => {
  262. const root = await temporaryRoot()
  263. const commands: CoverageCommand[] = []
  264. const partitionConfigs: string[] = []
  265. const runCommand = vi.fn(async (command: CoverageCommand) => {
  266. commands.push(command)
  267. const configArgument = command.args.find(argument => argument.startsWith('--config='))
  268. if (configArgument !== undefined) {
  269. partitionConfigs.push(await readFile(join(root, configArgument.slice('--config='.length)), 'utf8'))
  270. }
  271. await writeBlob(command)
  272. return passed
  273. })
  274. const coordinator = new CoveragePartitionCoordinator({
  275. root,
  276. partitions: 3,
  277. pnpmEntrypoint: '/pnpm.cjs',
  278. vitestArgs: ['--testTimeout=30000'],
  279. files: ['a.spec.ts', 'b.spec.ts', 'c.spec.ts'],
  280. runCommand,
  281. })
  282. await expect(coordinator.run()).resolves.toBe(0)
  283. expect(commands.map(command => command.label)).toEqual([
  284. 'partition 1/3',
  285. 'partition 2/3',
  286. 'partition 3/3',
  287. 'merged coverage report',
  288. ])
  289. for (const command of commands.slice(0, 3)) {
  290. expect(command.command).toBe(process.execPath)
  291. expect(command.args[0]).toBe('/pnpm.cjs')
  292. expect(command.args).toEqual(expect.arrayContaining([
  293. '--coverage',
  294. '--coverage.reportOnFailure',
  295. '--maxWorkers=1',
  296. '--reporter=default',
  297. '--reporter=blob',
  298. '--reporter=json',
  299. '--testTimeout=30000',
  300. ]))
  301. expect(command.args).not.toContain('--shard=1/3')
  302. expect(command.args.some(argument => argument.startsWith('--config='))).toBe(true)
  303. expect(command.env).toEqual({
  304. [COVERAGE_PARTITIONS_ENV]: undefined,
  305. [COVERAGE_PARTITION_MODE_ENV]: '1',
  306. })
  307. }
  308. // The partition file list travels in a temporary config, not on the
  309. // command line (which exceeds the Windows CreateProcess limit).
  310. expect(partitionConfigs).toHaveLength(3)
  311. const allConfigs = partitionConfigs.join('\n')
  312. expect(allConfigs).toContain('a.spec.ts')
  313. expect(allConfigs).toContain('b.spec.ts')
  314. expect(allConfigs).toContain('c.spec.ts')
  315. for (const source of partitionConfigs) {
  316. expect(source).toContain("from '../../vitest.config.ts'")
  317. }
  318. const mergeCommand = commands[3]
  319. if (mergeCommand === undefined) throw new Error('coverage merge command was not observed')
  320. expect(mergeCommand.args).toContain('--coverage')
  321. expect(mergeCommand.args.some(argument => argument.startsWith('--merge-reports='))).toBe(true)
  322. expect(mergeCommand.env).toEqual({
  323. [COVERAGE_PARTITIONS_ENV]: undefined,
  324. [COVERAGE_PARTITION_MODE_ENV]: undefined,
  325. })
  326. })
  327. it('rejects an empty partition assignment before spawning any command', async () => {
  328. const root = await temporaryRoot()
  329. const runCommand = vi.fn()
  330. const coordinator = new CoveragePartitionCoordinator({
  331. root,
  332. partitions: 3,
  333. pnpmEntrypoint: '/pnpm.cjs',
  334. // One file for three partitions leaves two buckets empty; an empty
  335. // bucket would make Vitest run the whole suite.
  336. files: ['a.spec.ts'],
  337. runCommand,
  338. })
  339. await expect(coordinator.run()).rejects.toThrow('partition 2/3 has no files')
  340. expect(runCommand).not.toHaveBeenCalled()
  341. })
  342. it('starts the heaviest partition first for fail-fast', async () => {
  343. const root = await temporaryRoot()
  344. const configContents = await runCoordinatorReadingConfigs(root, {
  345. partitions: 2,
  346. files: weightedFiles,
  347. weights: weightedDurations,
  348. })
  349. // LPT: p1=[a] sum 100, p2=[b,c] sum 60; sorted heaviest-first makes
  350. // partition 1/2 the heavy one, so its config names a.spec.ts.
  351. expect(configContents.get('partition 1/2')).toContain('a.spec.ts')
  352. expect(configContents.get('partition 1/2')).not.toContain('b.spec.ts')
  353. })
  354. it('gives each project only its own files in the partition config', async () => {
  355. const root = await temporaryRoot()
  356. const configContents = await runCoordinatorReadingConfigs(root, {
  357. partitions: 2,
  358. files: weightedFiles,
  359. weights: weightedDurations,
  360. projectOf: weightedProjects,
  361. })
  362. const allConfigs = [...configContents.values()].join('\n')
  363. // The process-bound project must not receive the thread-safe file and
  364. // vice versa, or plain files would run twice.
  365. expect(allConfigs).toContain("include: project.test.name === 'process-bound' ?")
  366. expect(allConfigs).not.toContain('"a.spec.ts","b.spec.ts","c.spec.ts"')
  367. })
  368. it('runs a native pnpm entrypoint directly', async () => {
  369. const root = await temporaryRoot()
  370. const commands: CoverageCommand[] = []
  371. const runCommand = successfulCommandRecorder(commands)
  372. const coordinator = new CoveragePartitionCoordinator({
  373. root,
  374. partitions: 2,
  375. pnpmEntrypoint: '/tools/pnpm',
  376. files: ['a.spec.ts', 'b.spec.ts'],
  377. runCommand,
  378. })
  379. await expect(coordinator.run()).resolves.toBe(0)
  380. expect(commands).toHaveLength(3)
  381. for (const command of commands) {
  382. expect(command.command).toBe('/tools/pnpm')
  383. expect(command.args[0]).toBe('exec')
  384. }
  385. })
  386. it('merges normal test failures and returns their failed status', async () => {
  387. const root = await temporaryRoot()
  388. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  389. const runCommand = vi.fn(async (command: CoverageCommand) => {
  390. await writeBlob(command)
  391. return command.label === 'partition 2/2'
  392. ? { exitCode: 1, signalCode: null, outputTail: 'specific Vitest failure' }
  393. : passed
  394. })
  395. const coordinator = new CoveragePartitionCoordinator({
  396. root,
  397. partitions: 2,
  398. pnpmEntrypoint: '/pnpm.cjs',
  399. files: ['a.spec.ts', 'b.spec.ts'],
  400. runCommand,
  401. })
  402. await expect(coordinator.run()).resolves.toBe(1)
  403. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 2/2 (exit 1)')
  404. expect(reported).toHaveBeenCalledWith(
  405. 'coverage-partitions: output tail for partition 2/2:\nspecific Vitest failure',
  406. )
  407. expect(runCommand).toHaveBeenCalledTimes(3)
  408. })
  409. it('rejects a missing partition blob before merge', async () => {
  410. const root = await temporaryRoot()
  411. const runCommand = vi.fn(async (command: CoverageCommand) => {
  412. if (command.label !== 'partition 2/2') await writeBlob(command)
  413. return passed
  414. })
  415. const coordinator = new CoveragePartitionCoordinator({
  416. root,
  417. partitions: 2,
  418. pnpmEntrypoint: '/pnpm.cjs',
  419. files: ['a.spec.ts', 'b.spec.ts'],
  420. runCommand,
  421. })
  422. await expect(coordinator.run()).rejects.toThrow('coverage partitions produced')
  423. expect(runCommand).toHaveBeenCalledTimes(2)
  424. })
  425. it('reports signal termination before missing-blob validation', async () => {
  426. const root = await temporaryRoot()
  427. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  428. const runCommand = vi.fn(async (command: CoverageCommand) => {
  429. if (command.label === 'partition 1/2') await writeBlob(command)
  430. return command.label === 'partition 2/2'
  431. ? { exitCode: null, signalCode: 'SIGTERM' as const }
  432. : passed
  433. })
  434. const coordinator = new CoveragePartitionCoordinator({
  435. root,
  436. partitions: 2,
  437. pnpmEntrypoint: '/pnpm.cjs',
  438. files: ['a.spec.ts', 'b.spec.ts'],
  439. runCommand,
  440. })
  441. await expect(coordinator.run()).rejects.toThrow('coverage partitions produced')
  442. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 2/2 (signal SIGTERM)')
  443. })
  444. it('waits for every partition after one spawn failure', async () => {
  445. const root = await temporaryRoot()
  446. const reported = vi.spyOn(console, 'error').mockImplementation(() => undefined)
  447. let secondFinished = false
  448. const runCommand = vi.fn(async (command: CoverageCommand) => {
  449. await writeBlob(command)
  450. if (command.label === 'partition 1/2') {
  451. return { exitCode: null, signalCode: null, error: 'spawn unavailable' }
  452. }
  453. if (command.label === 'partition 2/2') secondFinished = true
  454. return passed
  455. })
  456. const coordinator = new CoveragePartitionCoordinator({
  457. root,
  458. partitions: 2,
  459. pnpmEntrypoint: '/pnpm.cjs',
  460. files: ['a.spec.ts', 'b.spec.ts'],
  461. runCommand,
  462. })
  463. await expect(coordinator.run()).resolves.toBe(1)
  464. expect(reported).toHaveBeenCalledWith('coverage-partitions: FAIL partition 1/2 (spawn unavailable)')
  465. expect(secondFinished).toBe(true)
  466. expect(runCommand).toHaveBeenCalledTimes(3)
  467. })
  468. it('unlinks a link-shaped coverage path without touching its target', async () => {
  469. const root = await temporaryRoot()
  470. const target = await temporaryRoot()
  471. const marker = join(target, 'marker.txt')
  472. await writeFile(marker, 'owned elsewhere')
  473. await symlink(target, join(root, 'coverage'), process.platform === 'win32' ? 'junction' : 'dir')
  474. const runCommand = vi.fn(async (command: CoverageCommand) => {
  475. await writeBlob(command)
  476. return passed
  477. })
  478. const coordinator = new CoveragePartitionCoordinator({
  479. root,
  480. partitions: 2,
  481. pnpmEntrypoint: '/pnpm.cjs',
  482. files: ['a.spec.ts', 'b.spec.ts'],
  483. runCommand,
  484. })
  485. await expect(coordinator.run()).resolves.toBe(0)
  486. await expect(access(marker)).resolves.toBeUndefined()
  487. })
  488. })