run-gates.spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import { describe, expect, it, vi } from 'vitest'
  2. import {
  3. defaultConcurrency,
  4. formatGateResultReason,
  5. gatesForMode,
  6. runGate,
  7. runGates,
  8. type Gate,
  9. type GateResult,
  10. } from './run-gates.ts'
  11. function gate(id: string, options: Partial<Gate> = {}): Gate {
  12. return {
  13. id,
  14. label: id,
  15. displayCommand: `run ${id}`,
  16. command: process.execPath,
  17. args: ['-e', ''],
  18. ...options,
  19. }
  20. }
  21. function resultFor(subject: Gate, status: GateResult['status'] = 'passed'): GateResult {
  22. return {
  23. gate: subject,
  24. status,
  25. durationMs: 10,
  26. output: [],
  27. exitCode: status === 'passed' ? 0 : 1,
  28. signalCode: null,
  29. }
  30. }
  31. function withPnpmEntrypoint<T>(action: () => T, entrypoint = '/private/pnpm.cjs'): T {
  32. const previous = process.env.npm_execpath
  33. process.env.npm_execpath = entrypoint
  34. try {
  35. return action()
  36. } finally {
  37. if (previous === undefined) Reflect.deleteProperty(process.env, 'npm_execpath')
  38. else process.env.npm_execpath = previous
  39. }
  40. }
  41. function withEnv<T>(name: string, value: string | undefined, action: () => T): T {
  42. const previous = process.env[name]
  43. if (value === undefined) Reflect.deleteProperty(process.env, name)
  44. else process.env[name] = value
  45. try {
  46. return action()
  47. } finally {
  48. if (previous === undefined) Reflect.deleteProperty(process.env, name)
  49. else process.env[name] = previous
  50. }
  51. }
  52. describe('gate graph validation', () => {
  53. it.each([
  54. 'ci-primary',
  55. 'ci-linux-primary',
  56. 'ci-static',
  57. 'ci-lint-contracts-ready',
  58. 'ci-coverage',
  59. 'ci-snapshot',
  60. 'ci-artifacts',
  61. 'ci-consumers',
  62. 'ci-windows-blocking',
  63. 'ci-windows-complete',
  64. 'ci-windows-observational',
  65. 'node-compat',
  66. 'check-all',
  67. 'hygiene',
  68. 'doc-sync',
  69. 'doc-quick',
  70. ] as const)('constructs and executes preflight for a valid non-empty %s graph', async (mode) => {
  71. const subject = withPnpmEntrypoint(() => gatesForMode(mode))
  72. const execute = vi.fn(async (item: Gate) => resultFor(item))
  73. await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length)
  74. })
  75. it('keeps the public repository link policy in the documentation gate', () => {
  76. const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
  77. expect(ids).toContain('public-repository-links')
  78. })
  79. it('keeps package-group subsystem ownership in the documentation gate', () => {
  80. const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
  81. expect(ids).toContain('subsystem-pages')
  82. })
  83. it('derives the quick documentation aggregate from marked doc-sync leaves', () => {
  84. const full = withPnpmEntrypoint(() => gatesForMode('doc-sync'))
  85. const quick = withPnpmEntrypoint(() => gatesForMode('doc-quick'))
  86. expect(quick).toEqual(full.filter(gate => gate.quick === true))
  87. })
  88. it('keeps the hygiene aggregate aligned with the package script checks', () => {
  89. const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
  90. expect(ids).toEqual([
  91. 'rescope-vendor', 'publint', 'constraints', 'package-dependencies', 'application-entrypoints',
  92. 'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
  93. 'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'cordis-config',
  94. 'runtime-closure', 'vendored-links',
  95. ])
  96. expect(defaultConcurrency('hygiene', ids.length, 8)).toEqual({
  97. workers: 4,
  98. source: '8 available CPU(s), hygiene cap 4',
  99. })
  100. })
  101. it('schedules the longest documentation leaves before short checks', () => {
  102. const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
  103. expect(ids.slice(0, 10)).toEqual([
  104. 'doc-typecheck', 'docs-site-build', 'doc-graphs', 'markdown-links', 'type-equivalence',
  105. 'cordis-catalog', 'cordis-inspect-catalog', 'mermaid', 'scoped-events', 'translation-pairing',
  106. ])
  107. })
  108. it('launches a native pnpm entrypoint directly', () => {
  109. const entrypoint = String.raw`C:\Program Files\pnpm\pnpm.exe`
  110. const subject = withPnpmEntrypoint(() => gatesForMode('ci-windows-blocking')[0], entrypoint)
  111. expect(subject).toMatchObject({
  112. command: entrypoint,
  113. args: ['run', 'build'],
  114. })
  115. })
  116. it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
  117. 'keeps the DSH package license policy in %s',
  118. (mode) => {
  119. const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
  120. expect(ids).toContain('dsh-package-licenses')
  121. },
  122. )
  123. it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
  124. 'keeps package dependency enforcement in %s',
  125. (mode) => {
  126. const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
  127. expect(ids).toContain('package-dependencies')
  128. },
  129. )
  130. it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
  131. 'keeps the client dependency policy in %s',
  132. (mode) => {
  133. const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
  134. expect(ids).toContain('client-packages')
  135. },
  136. )
  137. it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
  138. 'keeps hard-coded Client UI copy enforcement in %s',
  139. (mode) => {
  140. const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
  141. expect(ids).toContain('client-ui-i18n')
  142. },
  143. )
  144. it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
  145. 'keeps application entrypoint enforcement in %s',
  146. (mode) => {
  147. const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
  148. expect(ids).toContain('application-entrypoints')
  149. },
  150. )
  151. it('keeps native Windows coverage blocking and behind the complete build', () => {
  152. const complete = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
  153. const observational = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational'))
  154. .filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build')
  155. const byId = new Map(complete.map(subject => [subject.id, subject]))
  156. expect(byId.get('coverage')?.allowFailure).not.toBe(true)
  157. expect(byId.get('coverage')?.needs).toContain('build')
  158. expect(byId.get('coverage-exempt-heavy')?.allowFailure).not.toBe(true)
  159. expect(byId.get('coverage')?.needs).toContain('build')
  160. expect(byId.get('coverage-exempt-heavy')?.needs).toContain('build')
  161. expect(byId.get('coverage-exempt-heavy')?.args).toContain(
  162. 'packages/experimental/webworker-packer/tests/image-loadable.spec.ts',
  163. )
  164. expect(observational).not.toHaveLength(0)
  165. for (const gate of observational) {
  166. const completeGate = byId.get(gate.id)
  167. expect(completeGate?.allowFailure).toBe(true)
  168. expect(completeGate?.after).toEqual(expect.arrayContaining([
  169. 'coverage',
  170. 'coverage-exempt-heavy',
  171. ]))
  172. expect(completeGate?.needs).toEqual(gate.needs)
  173. }
  174. })
  175. it('runs the Windows built-bin smoke after other observational gates settle', () => {
  176. const observational = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational'))
  177. const builtBin = observational.find(gate => gate.id === 'built-bin-smoke')
  178. expect(builtBin?.after).toEqual(
  179. observational.filter(gate => gate.id !== 'built-bin-smoke').map(gate => gate.id),
  180. )
  181. const completeBuiltBin = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
  182. .find(gate => gate.id === 'built-bin-smoke')
  183. expect(completeBuiltBin?.after).toContain('windows-site')
  184. expect(completeBuiltBin?.after).not.toContain('docs-site-build')
  185. })
  186. it('applies one configured test, polling, and hook timeout to both coverage gates', () => {
  187. const gates = withEnv('DSH_COVERAGE_TEST_TIMEOUT_MS', '15000', () =>
  188. withPnpmEntrypoint(() => gatesForMode('ci-windows-complete')))
  189. for (const id of ['coverage', 'coverage-exempt-heavy']) {
  190. expect(gates.find(subject => subject.id === id)?.args).toEqual(expect.arrayContaining([
  191. '--testTimeout=15000',
  192. '--expect.poll.timeout=15000',
  193. '--hookTimeout=15000',
  194. ]))
  195. }
  196. })
  197. it('keeps Vitest timeout defaults when the coverage override is absent', () => {
  198. const gates = withEnv('DSH_COVERAGE_TEST_TIMEOUT_MS', undefined, () =>
  199. withPnpmEntrypoint(() => gatesForMode('ci-windows-complete')))
  200. for (const id of ['coverage', 'coverage-exempt-heavy']) {
  201. expect(gates.find(subject => subject.id === id)?.args).not.toEqual(expect.arrayContaining([
  202. expect.stringMatching(/^--(?:testTimeout|expect\.poll\.timeout|hookTimeout)=/),
  203. ]))
  204. }
  205. })
  206. it('rejects an invalid coverage timeout before starting a gate', () => {
  207. expect(() => withEnv('DSH_COVERAGE_TEST_TIMEOUT_MS', '0', () =>
  208. withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))))
  209. .toThrow('DSH_COVERAGE_TEST_TIMEOUT_MS must be a positive integer')
  210. })
  211. it('selects partitioned coverage only when explicitly configured', () => {
  212. const coverage = withEnv('DSH_COVERAGE_PARTITIONS', '3', () =>
  213. withPnpmEntrypoint(() => gatesForMode('ci-windows-complete').find(subject => subject.id === 'coverage')))
  214. expect(coverage).toMatchObject({
  215. displayCommand: 'DSH_COVERAGE_PARTITIONS=3 pnpm run test:coverage:partitioned',
  216. args: ['/private/pnpm.cjs', 'run', 'test:coverage:partitioned'],
  217. env: { DSH_COVERAGE_EXEMPT_HEAVY: '1' },
  218. streamOutput: true,
  219. })
  220. })
  221. it('rejects an invalid coverage partition count before starting a gate', () => {
  222. expect(() => withEnv('DSH_COVERAGE_PARTITIONS', '1', () =>
  223. withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))))
  224. .toThrow('DSH_COVERAGE_PARTITIONS must be an integer greater than 1')
  225. })
  226. it.each([
  227. ['empty', [], /gate graph has no gates/],
  228. ['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/],
  229. ['unknown dependencies', [gate('subject', { needs: ['missing'] })], /depends on unknown gate "missing"/],
  230. ['unknown ordering predecessors', [gate('subject', { after: ['missing'] })], /waits for unknown gate "missing"/],
  231. ['cycles', [gate('first', { needs: ['second'] }), gate('second', { needs: ['first'] })], /dependency cycle: first -> second -> first/],
  232. ['mixed cycles', [gate('first', { after: ['second'] }), gate('second', { needs: ['first'] })], /dependency cycle: first -> second -> first/],
  233. ] as const)('rejects %s before starting a child', async (_label, invalid, message) => {
  234. const execute = vi.fn(async (subject: Gate) => resultFor(subject))
  235. await expect(runGates([...invalid], 1, execute)).rejects.toThrow(message)
  236. expect(execute).not.toHaveBeenCalled()
  237. })
  238. it('rejects an invalid worker count before starting a child', async () => {
  239. const execute = vi.fn(async (subject: Gate) => resultFor(subject))
  240. await expect(runGates([gate('subject')], 0, execute)).rejects.toThrow('max concurrency must be a positive integer')
  241. expect(execute).not.toHaveBeenCalled()
  242. })
  243. it('skips dependents after their prerequisite fails', async () => {
  244. const dependent = gate('dependent', { needs: ['root'] })
  245. const root = gate('root')
  246. const execute = vi.fn(async (subject: Gate) => resultFor(subject, 'failed'))
  247. const results = await runGates([dependent, root], 1, execute)
  248. expect(execute).toHaveBeenCalledOnce()
  249. expect(execute).toHaveBeenCalledWith(root)
  250. expect(results[0]).toMatchObject({ gate: dependent, status: 'skipped', error: 'dependency failed or skipped: root' })
  251. })
  252. it('runs an ordered follower after its predecessor fails', async () => {
  253. const follower = gate('follower', { after: ['root'] })
  254. const root = gate('root')
  255. const execute = vi.fn(async (subject: Gate) => resultFor(subject, subject === root ? 'failed' : 'passed'))
  256. const results = await runGates([follower, root], 2, execute)
  257. expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['root', 'follower'])
  258. expect(results.map(result => result.status)).toEqual(['passed', 'failed'])
  259. })
  260. it('runs an ordered follower after its predecessor is skipped', async () => {
  261. const follower = gate('follower', { after: ['dependent'] })
  262. const dependent = gate('dependent', { needs: ['root'] })
  263. const root = gate('root')
  264. const execute = vi.fn(async (subject: Gate) => resultFor(subject, subject === root ? 'failed' : 'passed'))
  265. const results = await runGates([follower, dependent, root], 2, execute)
  266. expect(execute.mock.calls.map(([subject]) => subject.id)).toEqual(['root', 'follower'])
  267. expect(results.map(result => result.status)).toEqual(['passed', 'skipped', 'failed'])
  268. })
  269. })
  270. describe('Oxlint gate', () => {
  271. it('uses the package script when no worker bound is configured', () => {
  272. const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
  273. withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
  274. expect(subject).toMatchObject({
  275. id: 'lint',
  276. displayCommand: 'pnpm run lint:contracts-ready',
  277. command: process.execPath,
  278. args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
  279. })
  280. })
  281. it('surfaces the configured worker bound on the shared package script', () => {
  282. const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
  283. withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
  284. expect(subject).toMatchObject({
  285. id: 'lint',
  286. displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint:contracts-ready',
  287. command: process.execPath,
  288. args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
  289. })
  290. })
  291. })
  292. describe('Typert contract preparation', () => {
  293. it('prepares primary source consumers once before they run', () => {
  294. const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
  295. withPnpmEntrypoint(() => gatesForMode('ci-primary')))
  296. expect(subject.find(item => item.id === 'typert-contracts')).toMatchObject({
  297. displayCommand: 'pnpm run build:lib:host',
  298. args: ['/private/pnpm.cjs', 'run', 'build:lib:host'],
  299. })
  300. for (const [id, script] of [
  301. ['typecheck', 'typecheck:contracts-ready'],
  302. ['lint', 'lint:contracts-ready'],
  303. ['doc-typecheck', 'doc-typecheck:contracts-ready'],
  304. ] as const) {
  305. expect(subject.find(item => item.id === id)).toMatchObject({
  306. displayCommand: `pnpm run ${script}`,
  307. args: ['/private/pnpm.cjs', 'run', script],
  308. needs: ['typert-contracts'],
  309. })
  310. }
  311. expect(subject.find(item => item.id === 'build')?.needs).toEqual([
  312. 'typecheck',
  313. 'lint',
  314. 'doc-typecheck',
  315. ])
  316. })
  317. it('reuses contracts from the validated consumer build', () => {
  318. const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
  319. expect(subject.find(item => item.id === 'lint-and-duplication')).toMatchObject({
  320. displayCommand: 'pnpm run check:ci:lint:contracts-ready',
  321. args: ['/private/pnpm.cjs', 'run', 'check:ci:lint:contracts-ready'],
  322. })
  323. expect(subject.find(item => item.id === 'doc-typecheck')).toMatchObject({
  324. displayCommand: 'pnpm run doc-typecheck:contracts-ready',
  325. args: ['/private/pnpm.cjs', 'run', 'doc-typecheck:contracts-ready'],
  326. })
  327. })
  328. it('keeps standalone doc sync responsible for preparation', () => {
  329. const docTypecheck = withPnpmEntrypoint(() =>
  330. gatesForMode('doc-sync').find(item => item.id === 'doc-typecheck'))
  331. expect(docTypecheck?.displayCommand).toBe('pnpm run doc-typecheck')
  332. })
  333. })
  334. describe('Node compatibility graph', () => {
  335. it('runs the jsdom environment smoke on every advertised Node line', () => {
  336. const subject = withPnpmEntrypoint(() => gatesForMode('node-compat'))
  337. expect(subject.find(item => item.id === 'vitest-jsdom-smoke')).toMatchObject({
  338. label: 'Vitest jsdom smoke',
  339. args: [
  340. '/private/pnpm.cjs',
  341. 'exec',
  342. 'vitest',
  343. 'run',
  344. 'scripts/vitest-environment.compat.spec.ts',
  345. ],
  346. })
  347. })
  348. })
  349. describe('Node 24 lane ownership', () => {
  350. it('keeps the static lane source-only', () => {
  351. const subject = withPnpmEntrypoint(() => gatesForMode('ci-static'))
  352. expect(subject.map(item => item.id)).not.toContain('build')
  353. expect(subject.map(item => item.id)).not.toContain('doc-typecheck')
  354. })
  355. it('owns the build and orders its artifact consumers', () => {
  356. const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
  357. expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({
  358. workers: 11,
  359. source: 'ci-consumers gate count',
  360. })
  361. expect(subject.map(item => item.id)).toEqual([
  362. 'build',
  363. 'node-compat',
  364. 'publint',
  365. 'built-package-invariants',
  366. 'lint-and-duplication',
  367. 'snapshot',
  368. 'expected-output',
  369. 'web-snapshot',
  370. 'doc-typecheck',
  371. 'node-next-types',
  372. 'built-bin-smoke',
  373. ])
  374. expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
  375. expect(subject.find(item => item.id === 'build')?.env).toEqual({
  376. DSH_BUILD_CLIENT_PROFILE: 'official',
  377. })
  378. expect(subject.find(item => item.id === 'node-compat')?.env).toEqual({
  379. DSH_BUILD_CLIENT_PROFILE: 'official',
  380. })
  381. expect(subject.find(item => item.id === 'built-package-invariants')?.needs).toEqual(['build'])
  382. expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['built-package-invariants'])
  383. for (const id of [
  384. 'snapshot',
  385. 'expected-output',
  386. 'web-snapshot',
  387. 'doc-typecheck',
  388. 'node-next-types',
  389. 'built-bin-smoke',
  390. ]) {
  391. expect(subject.find(item => item.id === id)?.needs).toEqual(['built-package-invariants'])
  392. }
  393. expect(subject.find(item => item.id === 'snapshot')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
  394. expect(subject.find(item => item.id === 'expected-output')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
  395. expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({
  396. DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
  397. })
  398. expect(subject.find(item => item.id === 'built-bin-smoke')?.args).toEqual(
  399. expect.arrayContaining([
  400. 'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
  401. 'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
  402. 'packages/experimental/agent-team/tests/built-lib.e2e.ts',
  403. ]),
  404. )
  405. expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
  406. displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
  407. env: { DSH_SNAPSHOT: 'replay' },
  408. after: [
  409. 'publint',
  410. 'lint-and-duplication',
  411. 'snapshot',
  412. 'expected-output',
  413. 'doc-typecheck',
  414. 'node-next-types',
  415. 'built-bin-smoke',
  416. ],
  417. })
  418. })
  419. })
  420. describe('Linux primary graph', () => {
  421. it('adds the same compare-only web gate after built client artifacts', () => {
  422. const subject = withPnpmEntrypoint(() => gatesForMode('ci-linux-primary'))
  423. const web = subject.find(item => item.id === 'web-snapshot')
  424. expect(web).toMatchObject({
  425. displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
  426. env: { DSH_SNAPSHOT: 'replay' },
  427. needs: ['built-package-invariants'],
  428. })
  429. })
  430. })
  431. describe('gate process outcomes', () => {
  432. it('streams selected gate output without retaining it', async () => {
  433. const write = vi.spyOn(process.stdout, 'write').mockReturnValue(true)
  434. try {
  435. const result = await runGate(gate('streamed', {
  436. args: ['-e', "process.stdout.write('live output')"],
  437. streamOutput: true,
  438. }))
  439. expect(result.status).toBe('passed')
  440. expect(result.output).toEqual([])
  441. expect(write).toHaveBeenCalledWith('live output')
  442. } finally {
  443. write.mockRestore()
  444. }
  445. })
  446. it.skipIf(process.platform === 'win32')('reports signal termination independently from exit status', async () => {
  447. const result = await runGate(gate('terminated', {
  448. args: ['-e', "process.kill(process.pid, 'SIGTERM')"],
  449. }))
  450. expect(result.status).toBe('failed')
  451. expect(result.exitCode).toBeNull()
  452. expect(result.signalCode).toBe('SIGTERM')
  453. expect(formatGateResultReason(result)).toBe('signal SIGTERM')
  454. })
  455. })