@aaronshaf/ger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.ast-grep/rules/no-as-casting.yml +13 -0
  2. package/.eslintrc.js +12 -0
  3. package/.github/workflows/ci-simple.yml +53 -0
  4. package/.github/workflows/ci.yml +171 -0
  5. package/.github/workflows/claude-code-review.yml +78 -0
  6. package/.github/workflows/claude.yml +64 -0
  7. package/.github/workflows/dependency-update.yml +84 -0
  8. package/.github/workflows/release.yml +166 -0
  9. package/.github/workflows/security-scan.yml +113 -0
  10. package/.github/workflows/security.yml +96 -0
  11. package/.husky/pre-commit +16 -0
  12. package/.husky/pre-push +25 -0
  13. package/.lintstagedrc.json +6 -0
  14. package/.tool-versions +1 -0
  15. package/CLAUDE.md +103 -0
  16. package/DEVELOPMENT.md +361 -0
  17. package/LICENSE +21 -0
  18. package/README.md +325 -0
  19. package/bin/ger +3 -0
  20. package/biome.json +36 -0
  21. package/bun.lock +688 -0
  22. package/bunfig.toml +8 -0
  23. package/oxlint.json +24 -0
  24. package/package.json +55 -0
  25. package/scripts/check-coverage.ts +69 -0
  26. package/scripts/check-file-size.ts +38 -0
  27. package/scripts/fix-test-mocks.ts +55 -0
  28. package/src/api/gerrit.ts +466 -0
  29. package/src/cli/commands/abandon.ts +65 -0
  30. package/src/cli/commands/comment.ts +460 -0
  31. package/src/cli/commands/comments.ts +85 -0
  32. package/src/cli/commands/diff.ts +71 -0
  33. package/src/cli/commands/incoming.ts +226 -0
  34. package/src/cli/commands/init.ts +164 -0
  35. package/src/cli/commands/mine.ts +115 -0
  36. package/src/cli/commands/open.ts +57 -0
  37. package/src/cli/commands/review.ts +593 -0
  38. package/src/cli/commands/setup.ts +230 -0
  39. package/src/cli/commands/show.ts +303 -0
  40. package/src/cli/commands/status.ts +35 -0
  41. package/src/cli/commands/workspace.ts +200 -0
  42. package/src/cli/index.ts +420 -0
  43. package/src/prompts/default-review.md +80 -0
  44. package/src/prompts/system-inline-review.md +88 -0
  45. package/src/prompts/system-overall-review.md +152 -0
  46. package/src/schemas/config.test.ts +245 -0
  47. package/src/schemas/config.ts +75 -0
  48. package/src/schemas/gerrit.ts +455 -0
  49. package/src/services/ai-enhanced.ts +167 -0
  50. package/src/services/ai.ts +182 -0
  51. package/src/services/config.test.ts +414 -0
  52. package/src/services/config.ts +206 -0
  53. package/src/test-utils/mock-generator.ts +73 -0
  54. package/src/utils/comment-formatters.ts +153 -0
  55. package/src/utils/diff-context.ts +103 -0
  56. package/src/utils/diff-formatters.ts +141 -0
  57. package/src/utils/formatters.ts +85 -0
  58. package/src/utils/message-filters.ts +26 -0
  59. package/src/utils/shell-safety.ts +117 -0
  60. package/src/utils/status-indicators.ts +100 -0
  61. package/src/utils/url-parser.test.ts +123 -0
  62. package/src/utils/url-parser.ts +91 -0
  63. package/tests/abandon.test.ts +163 -0
  64. package/tests/ai-service.test.ts +489 -0
  65. package/tests/comment-batch-advanced.test.ts +431 -0
  66. package/tests/comment-gerrit-api-compliance.test.ts +414 -0
  67. package/tests/comment.test.ts +707 -0
  68. package/tests/comments.test.ts +323 -0
  69. package/tests/config-service-simple.test.ts +100 -0
  70. package/tests/diff.test.ts +419 -0
  71. package/tests/helpers/config-mock.ts +27 -0
  72. package/tests/incoming.test.ts +357 -0
  73. package/tests/interactive-incoming.test.ts +173 -0
  74. package/tests/mine.test.ts +318 -0
  75. package/tests/mocks/fetch-mock.ts +139 -0
  76. package/tests/mocks/msw-handlers.ts +80 -0
  77. package/tests/open.test.ts +233 -0
  78. package/tests/review.test.ts +669 -0
  79. package/tests/setup.ts +13 -0
  80. package/tests/show.test.ts +439 -0
  81. package/tests/unit/schemas/gerrit.test.ts +85 -0
  82. package/tests/unit/test-utils/mock-generator.test.ts +154 -0
  83. package/tests/unit/utils/comment-formatters.test.ts +415 -0
  84. package/tests/unit/utils/diff-context.test.ts +171 -0
  85. package/tests/unit/utils/diff-formatters.test.ts +165 -0
  86. package/tests/unit/utils/formatters.test.ts +411 -0
  87. package/tests/unit/utils/message-filters.test.ts +227 -0
  88. package/tests/unit/utils/prompt-helpers.test.ts +175 -0
  89. package/tests/unit/utils/shell-safety.test.ts +230 -0
  90. package/tests/unit/utils/status-indicators.test.ts +137 -0
  91. package/tsconfig.json +40 -0
@@ -0,0 +1,165 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ formatDiffPretty,
4
+ formatFilesList,
5
+ formatDiffSummary,
6
+ extractDiffStats,
7
+ } from '@/utils/diff-formatters'
8
+
9
+ describe('Diff Formatters', () => {
10
+ describe('formatDiffPretty', () => {
11
+ test('should format a unified diff with colors and summary', () => {
12
+ const diff = `diff --git a/src/main.ts b/src/main.ts
13
+ index 1234567..abcdef0 100644
14
+ --- a/src/main.ts
15
+ +++ b/src/main.ts
16
+ @@ -1,5 +1,6 @@
17
+ function main() {
18
+ + // Added comment
19
+ console.log("Hello World")
20
+ return 0
21
+ }`
22
+
23
+ const result = formatDiffPretty(diff)
24
+
25
+ expect(result).toContain('Changes summary:')
26
+ expect(result).toContain('1 file changed')
27
+ expect(result).toContain('+1 addition')
28
+ expect(result).toContain('diff --git')
29
+ expect(result).toContain('function main()')
30
+ expect(result).toContain('// Added comment')
31
+ })
32
+
33
+ test('should handle empty or invalid diff content', () => {
34
+ const emptyResult = formatDiffPretty('')
35
+ expect(emptyResult).toContain('No changes detected')
36
+ expect(emptyResult).toContain('No diff content available')
37
+
38
+ const nullResult = formatDiffPretty(null as unknown as string)
39
+ expect(nullResult).toContain('No changes detected')
40
+ expect(nullResult).toContain('No diff content available')
41
+ })
42
+
43
+ test('should handle multi-file diffs', () => {
44
+ const diff = `diff --git a/file1.ts b/file1.ts
45
+ index abc123..def456 100644
46
+ --- a/file1.ts
47
+ +++ b/file1.ts
48
+ @@ -1,3 +1,4 @@
49
+ line1
50
+ +new line
51
+ line2
52
+ line3
53
+ diff --git a/file2.ts b/file2.ts
54
+ index 111222..333444 100644
55
+ --- a/file2.ts
56
+ +++ b/file2.ts
57
+ @@ -1,2 +1,1 @@
58
+ -removed line
59
+ remaining line`
60
+
61
+ const result = formatDiffPretty(diff)
62
+
63
+ expect(result).toContain('2 files changed')
64
+ expect(result).toContain('+1 addition')
65
+ expect(result).toContain('-1 deletion')
66
+ })
67
+ })
68
+
69
+ describe('formatFilesList', () => {
70
+ test('should format a list of files with header', () => {
71
+ const files = ['src/main.ts', 'src/utils.ts', 'README.md']
72
+ const result = formatFilesList(files)
73
+
74
+ expect(result).toContain('Changed files (3):')
75
+ expect(result).toContain('src/main.ts')
76
+ expect(result).toContain('src/utils.ts')
77
+ expect(result).toContain('README.md')
78
+ expect(result).toContain('•')
79
+ })
80
+
81
+ test('should handle empty files list', () => {
82
+ expect(formatFilesList([])).toBe('No files changed')
83
+ expect(formatFilesList(null as unknown as string[])).toBe('No files changed')
84
+ })
85
+ })
86
+
87
+ describe('formatDiffSummary', () => {
88
+ test('should format summary with files, additions, and deletions', () => {
89
+ const stats = { files: 2, additions: 5, deletions: 3 }
90
+ const result = formatDiffSummary(stats)
91
+
92
+ expect(result).toContain('2 files changed')
93
+ expect(result).toContain('+5 additions')
94
+ expect(result).toContain('-3 deletions')
95
+ })
96
+
97
+ test('should handle singular vs plural correctly', () => {
98
+ const singleStats = { files: 1, additions: 1, deletions: 1 }
99
+ const result = formatDiffSummary(singleStats)
100
+
101
+ expect(result).toContain('1 file changed')
102
+ expect(result).toContain('+1 addition')
103
+ expect(result).toContain('-1 deletion')
104
+ })
105
+
106
+ test('should handle zero changes', () => {
107
+ const emptyStats = { files: 0, additions: 0, deletions: 0 }
108
+ const result = formatDiffSummary(emptyStats)
109
+
110
+ expect(result).toContain('No changes detected')
111
+ })
112
+
113
+ test('should handle only additions', () => {
114
+ const addStats = { files: 1, additions: 3, deletions: 0 }
115
+ const result = formatDiffSummary(addStats)
116
+
117
+ expect(result).toContain('1 file changed')
118
+ expect(result).toContain('+3 additions')
119
+ expect(result).not.toContain('deletion')
120
+ })
121
+ })
122
+
123
+ describe('extractDiffStats', () => {
124
+ test('should extract correct statistics from diff', () => {
125
+ const diff = `diff --git a/file1.ts b/file1.ts
126
+ index abc123..def456 100644
127
+ --- a/file1.ts
128
+ +++ b/file1.ts
129
+ @@ -1,3 +1,4 @@
130
+ line1
131
+ +new line
132
+ -old line
133
+ line2`
134
+
135
+ const stats = extractDiffStats(diff)
136
+
137
+ expect(stats.files).toBe(1)
138
+ expect(stats.additions).toBe(1)
139
+ expect(stats.deletions).toBe(1)
140
+ })
141
+
142
+ test('should handle empty diff content', () => {
143
+ expect(extractDiffStats('')).toEqual({ files: 0, additions: 0, deletions: 0 })
144
+ expect(extractDiffStats(null as unknown as string)).toEqual({
145
+ files: 0,
146
+ additions: 0,
147
+ deletions: 0,
148
+ })
149
+ })
150
+
151
+ test('should count multiple files correctly', () => {
152
+ const diff = `diff --git a/file1.ts b/file1.ts
153
+ +added line 1
154
+ diff --git a/file2.ts b/file2.ts
155
+ +added line 2
156
+ -removed line`
157
+
158
+ const stats = extractDiffStats(diff)
159
+
160
+ expect(stats.files).toBe(2)
161
+ expect(stats.additions).toBe(2)
162
+ expect(stats.deletions).toBe(1)
163
+ })
164
+ })
165
+ })
@@ -0,0 +1,411 @@
1
+ import { describe, expect, test, afterEach, beforeEach } from 'bun:test'
2
+ import { formatDate, getStatusIndicator, colors } from '@/utils/formatters'
3
+ import { generateMockChange } from '@/test-utils/mock-generator'
4
+
5
+ describe('Formatters', () => {
6
+ describe('formatDate', () => {
7
+ let originalDate: typeof Date
8
+
9
+ beforeEach(() => {
10
+ originalDate = Date
11
+ })
12
+
13
+ afterEach(() => {
14
+ global.Date = originalDate
15
+ })
16
+
17
+ test("should format today's date with time", () => {
18
+ // Mock Date to always return a fixed current date when called without args
19
+ const mockCurrentTime = new originalDate('2023-12-01T15:00:00.000Z')
20
+
21
+ // Create a mock Date constructor
22
+ const MockDate = function (this: any, dateString?: any) {
23
+ if (arguments.length === 0) {
24
+ // When called with new Date() - return current time
25
+ return mockCurrentTime
26
+ }
27
+ // When called with new Date(dateString) - return parsed date
28
+ return new originalDate(dateString)
29
+ } as any
30
+
31
+ // Copy static methods
32
+ MockDate.now = () => mockCurrentTime.getTime()
33
+ MockDate.parse = originalDate.parse
34
+ MockDate.UTC = originalDate.UTC
35
+ MockDate.prototype = originalDate.prototype
36
+
37
+ global.Date = MockDate
38
+
39
+ const todayDate = '2023-12-01T12:30:00.000Z'
40
+ const result = formatDate(todayDate)
41
+
42
+ // Should show time only for today
43
+ expect(result).toMatch(/^\d{1,2}:\d{2}\s?(AM|PM)$/i)
44
+ })
45
+
46
+ test("should format this year's date without year", () => {
47
+ // Mock Date to always return a fixed current date when called without args
48
+ const mockCurrentTime = new originalDate('2023-12-01T15:00:00.000Z')
49
+
50
+ const MockDate = function (this: any, dateString?: any) {
51
+ if (arguments.length === 0) {
52
+ return mockCurrentTime
53
+ }
54
+ return new originalDate(dateString)
55
+ } as any
56
+
57
+ MockDate.now = () => mockCurrentTime.getTime()
58
+ MockDate.parse = originalDate.parse
59
+ MockDate.UTC = originalDate.UTC
60
+ MockDate.prototype = originalDate.prototype
61
+
62
+ global.Date = MockDate
63
+
64
+ const thisYearDate = '2023-06-15T10:30:00.000Z'
65
+ const result = formatDate(thisYearDate)
66
+
67
+ // Should show month and day for this year
68
+ expect(result).toMatch(/^[A-Za-z]{3}\s\d{2}$/)
69
+ expect(result).not.toContain('2023')
70
+ })
71
+
72
+ test("should format previous year's date with year", () => {
73
+ // Mock current time to be 2023-12-01
74
+ const mockNow = new Date('2023-12-01T15:00:00.000Z').getTime()
75
+ Date.now = () => mockNow
76
+
77
+ const previousYearDate = '2022-06-15T10:30:00.000Z'
78
+ const result = formatDate(previousYearDate)
79
+
80
+ // Should show month, day, and year for previous years
81
+ expect(result).toMatch(/^[A-Za-z]{3}\s\d{2},\s\d{4}$/)
82
+ expect(result).toContain('2022')
83
+ })
84
+
85
+ test('should handle future dates', () => {
86
+ // Mock current time to be 2023-12-01
87
+ const mockNow = new Date('2023-12-01T15:00:00.000Z').getTime()
88
+ Date.now = () => mockNow
89
+
90
+ const futureDate = '2024-03-15T10:30:00.000Z'
91
+ const result = formatDate(futureDate)
92
+
93
+ // Should show month, day, and year for future dates
94
+ expect(result).toMatch(/^[A-Za-z]{3}\s\d{2},\s\d{4}$/)
95
+ expect(result).toContain('2024')
96
+ })
97
+
98
+ test('should handle edge case of exact midnight', () => {
99
+ // Mock Date to always return a fixed current date when called without args
100
+ const mockCurrentTime = new originalDate('2023-12-01T00:00:00.000Z')
101
+
102
+ const MockDate = function (this: any, dateString?: any) {
103
+ if (arguments.length === 0) {
104
+ return mockCurrentTime
105
+ }
106
+ return new originalDate(dateString)
107
+ } as any
108
+
109
+ MockDate.now = () => mockCurrentTime.getTime()
110
+ MockDate.parse = originalDate.parse
111
+ MockDate.UTC = originalDate.UTC
112
+ MockDate.prototype = originalDate.prototype
113
+
114
+ global.Date = MockDate
115
+
116
+ const sameDate = '2023-12-01T00:00:00.000Z'
117
+ const result = formatDate(sameDate)
118
+
119
+ // Should still be treated as today
120
+ expect(result).toMatch(/^\d{1,2}:\d{2}\s?(AM|PM)$/i)
121
+ })
122
+
123
+ test('should handle different timezones correctly', () => {
124
+ // Mock Date to always return a fixed current date when called without args
125
+ const mockCurrentTime = new originalDate('2023-12-01T12:00:00.000Z')
126
+
127
+ const MockDate = function (this: any, dateString?: any) {
128
+ if (arguments.length === 0) {
129
+ return mockCurrentTime
130
+ }
131
+ return new originalDate(dateString)
132
+ } as any
133
+
134
+ MockDate.now = () => mockCurrentTime.getTime()
135
+ MockDate.parse = originalDate.parse
136
+ MockDate.UTC = originalDate.UTC
137
+ MockDate.prototype = originalDate.prototype
138
+
139
+ global.Date = MockDate
140
+
141
+ // Test with various timezone formats
142
+ const utcDate = '2023-12-01T08:30:00.000Z'
143
+ const result = formatDate(utcDate)
144
+
145
+ // Should be treated as today regardless of timezone
146
+ expect(result).toMatch(/^\d{1,2}:\d{2}\s?(AM|PM)$/i)
147
+ })
148
+ })
149
+
150
+ describe('getStatusIndicator', () => {
151
+ test('should return empty string for change without labels', () => {
152
+ const change = generateMockChange({ labels: undefined })
153
+ const result = getStatusIndicator(change)
154
+ expect(result).toBe('')
155
+ })
156
+
157
+ test('should return empty string for change with empty labels', () => {
158
+ const change = generateMockChange({ labels: {} })
159
+ const result = getStatusIndicator(change)
160
+ expect(result).toBe('')
161
+ })
162
+
163
+ test('should show approved Code-Review indicator', () => {
164
+ const change = generateMockChange({
165
+ labels: {
166
+ 'Code-Review': {
167
+ approved: { _account_id: 123, name: 'Reviewer', email: 'reviewer@example.com' },
168
+ },
169
+ },
170
+ })
171
+ const result = getStatusIndicator(change)
172
+ expect(result).toContain('✓')
173
+ })
174
+
175
+ test('should show approved Code-Review with value +2', () => {
176
+ const change = generateMockChange({
177
+ labels: {
178
+ 'Code-Review': { value: 2 },
179
+ },
180
+ })
181
+ const result = getStatusIndicator(change)
182
+ expect(result).toContain('✓')
183
+ })
184
+
185
+ test('should show rejected Code-Review indicator', () => {
186
+ const change = generateMockChange({
187
+ labels: {
188
+ 'Code-Review': {
189
+ rejected: { _account_id: 123, name: 'Reviewer', email: 'reviewer@example.com' },
190
+ },
191
+ },
192
+ })
193
+ const result = getStatusIndicator(change)
194
+ expect(result).toContain('✗')
195
+ })
196
+
197
+ test('should show rejected Code-Review with value -2', () => {
198
+ const change = generateMockChange({
199
+ labels: {
200
+ 'Code-Review': { value: -2 },
201
+ },
202
+ })
203
+ const result = getStatusIndicator(change)
204
+ expect(result).toContain('✗')
205
+ })
206
+
207
+ test('should show recommended Code-Review indicator', () => {
208
+ const change = generateMockChange({
209
+ labels: {
210
+ 'Code-Review': {
211
+ recommended: { _account_id: 123, name: 'Reviewer', email: 'reviewer@example.com' },
212
+ },
213
+ },
214
+ })
215
+ const result = getStatusIndicator(change)
216
+ expect(result).toContain('↑')
217
+ })
218
+
219
+ test('should show recommended Code-Review with value +1', () => {
220
+ const change = generateMockChange({
221
+ labels: {
222
+ 'Code-Review': { value: 1 },
223
+ },
224
+ })
225
+ const result = getStatusIndicator(change)
226
+ expect(result).toContain('↑')
227
+ })
228
+
229
+ test('should show disliked Code-Review indicator', () => {
230
+ const change = generateMockChange({
231
+ labels: {
232
+ 'Code-Review': {
233
+ disliked: { _account_id: 123, name: 'Reviewer', email: 'reviewer@example.com' },
234
+ },
235
+ },
236
+ })
237
+ const result = getStatusIndicator(change)
238
+ expect(result).toContain('↓')
239
+ })
240
+
241
+ test('should show disliked Code-Review with value -1', () => {
242
+ const change = generateMockChange({
243
+ labels: {
244
+ 'Code-Review': { value: -1 },
245
+ },
246
+ })
247
+ const result = getStatusIndicator(change)
248
+ expect(result).toContain('↓')
249
+ })
250
+
251
+ test('should show approved Verified indicator', () => {
252
+ const change = generateMockChange({
253
+ labels: {
254
+ Verified: { approved: { _account_id: 123, name: 'Bot', email: 'bot@example.com' } },
255
+ },
256
+ })
257
+ const result = getStatusIndicator(change)
258
+ expect(result).toContain('✓')
259
+ })
260
+
261
+ test('should show approved Verified with value +1', () => {
262
+ const change = generateMockChange({
263
+ labels: {
264
+ Verified: { value: 1 },
265
+ },
266
+ })
267
+ const result = getStatusIndicator(change)
268
+ expect(result).toContain('✓')
269
+ })
270
+
271
+ test('should show rejected Verified indicator', () => {
272
+ const change = generateMockChange({
273
+ labels: {
274
+ Verified: { rejected: { _account_id: 123, name: 'Bot', email: 'bot@example.com' } },
275
+ },
276
+ })
277
+ const result = getStatusIndicator(change)
278
+ expect(result).toContain('✗')
279
+ })
280
+
281
+ test('should show rejected Verified with value -1', () => {
282
+ const change = generateMockChange({
283
+ labels: {
284
+ Verified: { value: -1 },
285
+ },
286
+ })
287
+ const result = getStatusIndicator(change)
288
+ expect(result).toContain('✗')
289
+ })
290
+
291
+ test('should show submittable indicator', () => {
292
+ const change = generateMockChange({ submittable: true })
293
+ const result = getStatusIndicator(change)
294
+ expect(result).toContain('🚀')
295
+ })
296
+
297
+ test('should show work in progress indicator', () => {
298
+ const change = generateMockChange({ work_in_progress: true })
299
+ const result = getStatusIndicator(change)
300
+ expect(result).toContain('🚧')
301
+ })
302
+
303
+ test('should combine multiple indicators', () => {
304
+ const change = generateMockChange({
305
+ labels: {
306
+ 'Code-Review': { value: 2 },
307
+ Verified: { value: 1 },
308
+ },
309
+ submittable: true,
310
+ })
311
+ const result = getStatusIndicator(change)
312
+ expect(result).toContain('✓')
313
+ expect(result).toContain('✓')
314
+ expect(result).toContain('🚀')
315
+ })
316
+
317
+ test('should handle mixed positive and negative reviews', () => {
318
+ const change = generateMockChange({
319
+ labels: {
320
+ 'Code-Review': { value: 1 },
321
+ Verified: { value: -1 },
322
+ },
323
+ })
324
+ const result = getStatusIndicator(change)
325
+ expect(result).toContain('↑')
326
+ expect(result).toContain('✗')
327
+ })
328
+
329
+ test('should handle WIP with other indicators', () => {
330
+ const change = generateMockChange({
331
+ labels: {
332
+ 'Code-Review': { value: 2 },
333
+ },
334
+ work_in_progress: true,
335
+ })
336
+ const result = getStatusIndicator(change)
337
+ expect(result).toContain('✓')
338
+ expect(result).toContain('🚧')
339
+ })
340
+
341
+ test('should handle zero values (no indicators)', () => {
342
+ const change = generateMockChange({
343
+ labels: {
344
+ 'Code-Review': { value: 0 },
345
+ Verified: { value: 0 },
346
+ },
347
+ })
348
+ const result = getStatusIndicator(change)
349
+ expect(result).toBe('')
350
+ })
351
+
352
+ test('should handle custom label names', () => {
353
+ const change = generateMockChange({
354
+ labels: {
355
+ 'Custom-Label': { value: 1 },
356
+ },
357
+ })
358
+ const result = getStatusIndicator(change)
359
+ // Should not show indicators for unknown labels
360
+ expect(result).toBe('')
361
+ })
362
+
363
+ test('should prioritize boolean flags over numeric values', () => {
364
+ const change = generateMockChange({
365
+ labels: {
366
+ 'Code-Review': {
367
+ approved: { _account_id: 123, name: 'Reviewer', email: 'reviewer@example.com' },
368
+ value: 1, // Should use approved flag, not value
369
+ },
370
+ },
371
+ })
372
+ const result = getStatusIndicator(change)
373
+ expect(result).toContain('✓')
374
+ })
375
+ })
376
+
377
+ describe('colors', () => {
378
+ test('should export color constants', () => {
379
+ expect(colors.green).toBe('\x1b[32m')
380
+ expect(colors.yellow).toBe('\x1b[33m')
381
+ expect(colors.red).toBe('\x1b[31m')
382
+ expect(colors.blue).toBe('\x1b[34m')
383
+ expect(colors.cyan).toBe('\x1b[36m')
384
+ expect(colors.reset).toBe('\x1b[0m')
385
+ expect(colors.bold).toBe('\x1b[1m')
386
+ expect(colors.dim).toBe('\x1b[2m')
387
+ })
388
+
389
+ test('should have correct ANSI escape sequences', () => {
390
+ // Test that colors are proper ANSI escape sequences
391
+ expect(colors.green.startsWith('\x1b[')).toBe(true)
392
+ expect(colors.reset).toBe('\x1b[0m')
393
+ expect(colors.bold).toBe('\x1b[1m')
394
+ })
395
+
396
+ test('colors should be used in status indicators', () => {
397
+ const change = generateMockChange({
398
+ labels: {
399
+ 'Code-Review': { value: 2 },
400
+ Verified: { value: -1 },
401
+ },
402
+ })
403
+ const result = getStatusIndicator(change)
404
+
405
+ // Should contain color codes
406
+ expect(result).toContain(colors.green)
407
+ expect(result).toContain(colors.red)
408
+ expect(result).toContain(colors.reset)
409
+ })
410
+ })
411
+ })