upload-diagnostics.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. attachDiagnostic,
  4. buildDebugPayload,
  5. buildDiagnostic,
  6. extractErrorDetails,
  7. formatDiagnosticSummary,
  8. formatRetrySummary,
  9. } from '../../scripts/upload-diagnostics.mjs'
  10. describe('upload diagnostics', () => {
  11. it('extracts network cause details from fetch failures', () => {
  12. const error = new TypeError('fetch failed', {
  13. cause: {
  14. code: 'ECONNRESET',
  15. message: 'socket hang up',
  16. syscall: 'read',
  17. },
  18. })
  19. expect(extractErrorDetails(error)).toMatchObject({
  20. causeCode: 'ECONNRESET',
  21. causeMessage: 'socket hang up',
  22. causeSyscall: 'read',
  23. errorMessage: 'fetch failed',
  24. errorName: 'TypeError',
  25. })
  26. })
  27. it('formats upload failures with attempt, file index and cause code', () => {
  28. const diagnostic = buildDiagnostic({
  29. attempt: 3,
  30. error: new TypeError('fetch failed', {
  31. cause: {
  32. code: 'ECONNRESET',
  33. message: 'socket hang up',
  34. },
  35. }),
  36. fileIndex: 5,
  37. fileName: 'SwitchHosts-v4.3.0.6136-linux-amd64.deb',
  38. maxAttempts: 3,
  39. method: 'POST',
  40. progressSnapshot: {
  41. currentFileBytes: 123,
  42. totalFiles: 24,
  43. totalUploadedBytes: 456,
  44. },
  45. retryable: false,
  46. stage: 'upload-asset',
  47. target: '/upload',
  48. })
  49. expect(formatDiagnosticSummary(diagnostic)).toContain('upload-asset failed')
  50. expect(formatDiagnosticSummary(diagnostic)).toContain('attempt 3/3')
  51. expect(formatDiagnosticSummary(diagnostic)).toContain('file 5/24')
  52. expect(formatDiagnosticSummary(diagnostic)).toContain('cause=ECONNRESET')
  53. })
  54. it('formats dns failures with the underlying cause code', () => {
  55. const diagnostic = buildDiagnostic({
  56. attempt: 2,
  57. error: new TypeError('fetch failed', {
  58. cause: {
  59. code: 'EAI_AGAIN',
  60. hostname: 'api.github.com',
  61. message: 'getaddrinfo EAI_AGAIN api.github.com',
  62. },
  63. }),
  64. maxAttempts: 3,
  65. method: 'GET',
  66. retryable: true,
  67. stage: 'find-release',
  68. target: '/repos/oldj/SwitchHosts/releases?per_page=100&page=1',
  69. })
  70. expect(formatRetrySummary(diagnostic, '1.5s')).toContain('cause=EAI_AGAIN')
  71. expect(formatDiagnosticSummary(diagnostic)).toContain('message=getaddrinfo EAI_AGAIN api.github.com')
  72. })
  73. it('includes http status for api failures', () => {
  74. const diagnostic = buildDiagnostic({
  75. attempt: 3,
  76. error: new Error('GET /repos/... failed: 503 Service Unavailable'),
  77. httpStatus: 503,
  78. maxAttempts: 3,
  79. method: 'GET',
  80. retryable: false,
  81. stage: 'find-release',
  82. target: '/repos/oldj/SwitchHosts/releases?per_page=100&page=1',
  83. })
  84. expect(formatDiagnosticSummary(diagnostic)).toContain('status=503')
  85. expect(formatRetrySummary(diagnostic, '3.0s')).toContain('status=503')
  86. })
  87. it('builds debug payloads with stack and raw cause fields', () => {
  88. const error = new TypeError('fetch failed', {
  89. cause: {
  90. code: 'ECONNRESET',
  91. errno: -54,
  92. message: 'socket hang up',
  93. },
  94. })
  95. const diagnostic = buildDiagnostic({
  96. attempt: 3,
  97. error,
  98. maxAttempts: 3,
  99. method: 'POST',
  100. retryable: false,
  101. stage: 'upload-asset',
  102. target: '/upload',
  103. })
  104. expect(buildDebugPayload(diagnostic, error)).toMatchObject({
  105. diagnostic: {
  106. causeCode: 'ECONNRESET',
  107. stage: 'upload-asset',
  108. },
  109. error: {
  110. cause: {
  111. code: 'ECONNRESET',
  112. errno: -54,
  113. message: 'socket hang up',
  114. },
  115. errorMessage: 'fetch failed',
  116. },
  117. })
  118. })
  119. it('falls back to the top-level error message when no cause exists', () => {
  120. const diagnostic = buildDiagnostic({
  121. attempt: 1,
  122. error: new Error('plain failure'),
  123. maxAttempts: 3,
  124. method: 'DELETE',
  125. retryable: false,
  126. stage: 'delete-asset',
  127. target: '/repos/oldj/SwitchHosts/releases/assets/1',
  128. })
  129. expect(formatDiagnosticSummary(diagnostic)).toContain('message=plain failure')
  130. })
  131. it('attaches diagnostics to error instances for top-level reporting', () => {
  132. const error = new Error('plain failure')
  133. const diagnostic = buildDiagnostic({
  134. attempt: 1,
  135. error,
  136. maxAttempts: 3,
  137. method: 'DELETE',
  138. retryable: false,
  139. stage: 'delete-asset',
  140. target: '/repos/oldj/SwitchHosts/releases/assets/1',
  141. })
  142. const attached = attachDiagnostic(error, diagnostic)
  143. expect(attached.diagnostic).toEqual(diagnostic)
  144. })
  145. })