@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.
- package/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.eslintrc.js +12 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +78 -0
- package/.github/workflows/claude.yml +64 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +103 -0
- package/DEVELOPMENT.md +361 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/bin/ger +3 -0
- package/biome.json +36 -0
- package/bun.lock +688 -0
- package/bunfig.toml +8 -0
- package/oxlint.json +24 -0
- package/package.json +55 -0
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/src/api/gerrit.ts +466 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/review.ts +593 -0
- package/src/cli/commands/setup.ts +230 -0
- package/src/cli/commands/show.ts +303 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +420 -0
- package/src/prompts/default-review.md +80 -0
- package/src/prompts/system-inline-review.md +88 -0
- package/src/prompts/system-overall-review.md +152 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +75 -0
- package/src/schemas/gerrit.ts +455 -0
- package/src/services/ai-enhanced.ts +167 -0
- package/src/services/ai.ts +182 -0
- package/src/services/config.test.ts +414 -0
- package/src/services/config.ts +206 -0
- package/src/test-utils/mock-generator.ts +73 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +123 -0
- package/src/utils/url-parser.ts +91 -0
- package/tests/abandon.test.ts +163 -0
- package/tests/ai-service.test.ts +489 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +707 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +318 -0
- package/tests/mocks/fetch-mock.ts +139 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/review.test.ts +669 -0
- package/tests/setup.ts +13 -0
- package/tests/show.test.ts +439 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/prompt-helpers.test.ts +175 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- 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
|
+
})
|