ratchet-eslint-rules.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  2. import os from 'node:os'
  3. import path from 'node:path'
  4. import { afterEach, describe, expect, it, vi } from 'vitest'
  5. import { runRatchet } from '../ratchet-eslint-rules'
  6. const studioRoot = path.resolve(__dirname, '../..')
  7. const repoRoot = path.resolve(studioRoot, '..', '..')
  8. const scriptArgvPlaceholder = path.resolve(studioRoot, 'scripts', 'ratchet-eslint-rules.ts')
  9. const tempDirs: string[] = []
  10. afterEach(() => {
  11. vi.restoreAllMocks()
  12. while (tempDirs.length) {
  13. const dir = tempDirs.pop()
  14. if (dir) {
  15. rmSync(dir, { recursive: true, force: true })
  16. }
  17. }
  18. })
  19. describe('ratchet-eslint-rules integration', () => {
  20. it('captures per-file counts when initializing baselines', () => {
  21. const tmp = createTempDir()
  22. const metadataPath = path.join(tmp, 'baseline.json')
  23. const eslintResults = buildEslintResults([
  24. { filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 1 } },
  25. { filePath: repoPath('apps/studio/src/b.ts'), rules: { 'no-console': 2 } },
  26. ])
  27. const result = invokeRatchet(
  28. ['--metadata', metadataPath, '--rule', 'no-console', '--init'],
  29. eslintResults
  30. )
  31. expect(result).toBe(0)
  32. const metadata = JSON.parse(readFileSync(metadataPath, 'utf8'))
  33. expect(metadata.rules['no-console']).toBe(3)
  34. expect(metadata.ruleFiles['no-console']).toEqual({
  35. [relativeToCwd('apps/studio/src/a.ts')]: 1,
  36. [relativeToCwd('apps/studio/src/b.ts')]: 2,
  37. })
  38. })
  39. it('reports offending files when regressions occur and metadata has per-file data', () => {
  40. const tmp = createTempDir()
  41. const metadataPath = path.join(tmp, 'baseline.json')
  42. writeFileSync(
  43. metadataPath,
  44. JSON.stringify(
  45. {
  46. rules: { 'no-console': 2 },
  47. ruleFiles: {
  48. 'no-console': {
  49. [relativeToCwd('apps/studio/src/a.ts')]: 2,
  50. },
  51. },
  52. },
  53. null,
  54. 2
  55. )
  56. )
  57. const eslintResults = buildEslintResults([
  58. { filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 3 } },
  59. { filePath: repoPath('apps/studio/src/b.ts'), rules: { 'no-console': 1 } },
  60. ])
  61. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
  62. const result = invokeRatchet(
  63. ['--metadata', metadataPath, '--rule', 'no-console'],
  64. eslintResults
  65. )
  66. expect(result).toBe(1)
  67. const combinedErrors = errorSpy.mock.calls.map((args) => args.join(' ')).join('\n')
  68. expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/a.ts')} (+1)`)
  69. expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/b.ts')} (+1)`)
  70. })
  71. it('falls back gracefully when baseline is missing per-file data', () => {
  72. const tmp = createTempDir()
  73. const metadataPath = path.join(tmp, 'baseline.json')
  74. writeFileSync(
  75. metadataPath,
  76. JSON.stringify(
  77. {
  78. rules: { 'no-console': 1 },
  79. },
  80. null,
  81. 2
  82. )
  83. )
  84. const eslintResults = buildEslintResults([
  85. { filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 2 } },
  86. ])
  87. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
  88. const result = invokeRatchet(
  89. ['--metadata', metadataPath, '--rule', 'no-console'],
  90. eslintResults
  91. )
  92. expect(result).toBe(1)
  93. const combinedErrors = errorSpy.mock.calls.map((args) => args.join(' ')).join('\n')
  94. expect(combinedErrors).toContain('baseline missing file breakdown')
  95. expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/a.ts')} (2 current)`)
  96. })
  97. })
  98. function buildEslintResults(
  99. files: Array<{ filePath: string; rules: Record<string, number> }>
  100. ): unknown[] {
  101. return files.map(({ filePath, rules }) => ({
  102. filePath,
  103. messages: Object.entries(rules).flatMap(([ruleId, count]) =>
  104. Array.from({ length: count }, () => ({ ruleId }))
  105. ),
  106. }))
  107. }
  108. function createTempDir(): string {
  109. const dir = mkdtempSync(path.join(os.tmpdir(), 'ratchet-eslint'))
  110. tempDirs.push(dir)
  111. return dir
  112. }
  113. function repoPath(relPath: string): string {
  114. return path.join(repoRoot, relPath)
  115. }
  116. function invokeRatchet(args: string[], eslintResults: unknown[]): number {
  117. const argv = ['node', scriptArgvPlaceholder, ...args]
  118. return runRatchet(argv, () => ({
  119. results: eslintResults as any,
  120. stderr: '',
  121. }))
  122. }
  123. function relativeToCwd(relPath: string): string {
  124. return path.relative(process.cwd(), repoPath(relPath)).split(path.sep).join('/')
  125. }