@aaronshaf/ger 1.2.11 → 2.0.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/.claude-plugin/plugin.json +22 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +83 -0
- package/.github/workflows/claude.yml +50 -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 +105 -0
- package/DEVELOPMENT.md +361 -0
- package/EXAMPLES.md +457 -0
- package/README.md +831 -16
- package/bin/ger +3 -18
- package/biome.json +36 -0
- package/bun.lock +678 -0
- package/bunfig.toml +8 -0
- package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
- package/docs/adr/0002-use-bun-runtime.md +64 -0
- package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
- package/docs/adr/0004-use-commander-for-cli.md +76 -0
- package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
- package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
- package/docs/adr/0007-git-hooks-for-quality.md +94 -0
- package/docs/adr/0008-no-as-typecasting.md +83 -0
- package/docs/adr/0009-file-size-limits.md +82 -0
- package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
- package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
- package/docs/adr/0012-build-status-message-parsing.md +94 -0
- package/docs/adr/0013-git-subprocess-integration.md +98 -0
- package/docs/adr/0014-group-management-support.md +95 -0
- package/docs/adr/0015-batch-comment-processing.md +111 -0
- package/docs/adr/0016-flexible-change-identifiers.md +94 -0
- package/docs/adr/0017-git-worktree-support.md +102 -0
- package/docs/adr/0018-auto-install-commit-hook.md +103 -0
- package/docs/adr/0019-sdk-package-exports.md +95 -0
- package/docs/adr/0020-code-coverage-enforcement.md +105 -0
- package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
- package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
- package/docs/adr/README.md +30 -0
- package/docs/prd/README.md +12 -0
- package/docs/prd/architecture.md +325 -0
- package/docs/prd/commands.md +425 -0
- package/docs/prd/data-model.md +349 -0
- package/docs/prd/overview.md +124 -0
- package/index.ts +219 -0
- package/oxlint.json +24 -0
- package/package.json +82 -15
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/skills/gerrit-workflow/SKILL.md +247 -0
- package/skills/gerrit-workflow/examples.md +572 -0
- package/skills/gerrit-workflow/reference.md +728 -0
- package/src/api/gerrit.ts +696 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/add-reviewer.ts +156 -0
- package/src/cli/commands/build-status.ts +282 -0
- package/src/cli/commands/checkout.ts +422 -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/extract-url.ts +266 -0
- package/src/cli/commands/groups-members.ts +104 -0
- package/src/cli/commands/groups-show.ts +169 -0
- package/src/cli/commands/groups.ts +137 -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/projects.ts +68 -0
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/rebase.ts +52 -0
- package/src/cli/commands/remove-reviewer.ts +123 -0
- package/src/cli/commands/restore.ts +50 -0
- package/src/cli/commands/review.ts +486 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/setup.ts +286 -0
- package/src/cli/commands/show.ts +491 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/submit.ts +108 -0
- package/src/cli/commands/vote.ts +119 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +53 -0
- package/src/cli/register-commands.ts +659 -0
- package/src/cli/register-group-commands.ts +88 -0
- package/src/cli/register-reviewer-commands.ts +97 -0
- package/src/prompts/default-review.md +86 -0
- package/src/prompts/system-inline-review.md +135 -0
- package/src/prompts/system-overall-review.md +206 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +84 -0
- package/src/schemas/gerrit.ts +681 -0
- package/src/services/commit-hook.ts +314 -0
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +250 -0
- package/src/services/git-worktree.ts +342 -0
- package/src/services/review-strategy.ts +292 -0
- package/src/test-utils/mock-generator.ts +138 -0
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -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/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +110 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +271 -0
- package/src/utils/url-parser.ts +118 -0
- package/tests/abandon.test.ts +230 -0
- package/tests/add-reviewer.test.ts +579 -0
- package/tests/build-status-watch.test.ts +344 -0
- package/tests/build-status.test.ts +789 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/checkout/integration.test.ts +653 -0
- package/tests/checkout/parse-input.test.ts +55 -0
- package/tests/checkout/validation.test.ts +178 -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 +708 -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/extract-url.test.ts +517 -0
- package/tests/groups-members.test.ts +256 -0
- package/tests/groups-show.test.ts +323 -0
- package/tests/groups.test.ts +334 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/init.test.ts +70 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +285 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/projects.test.ts +259 -0
- package/tests/rebase.test.ts +271 -0
- package/tests/remove-reviewer.test.ts +357 -0
- package/tests/restore.test.ts +237 -0
- package/tests/review.test.ts +135 -0
- package/tests/search.test.ts +712 -0
- package/tests/setup.test.ts +63 -0
- package/tests/show-auto-detect.test.ts +324 -0
- package/tests/show.test.ts +813 -0
- package/tests/status.test.ts +145 -0
- package/tests/submit.test.ts +316 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/git-branch-detection.test.ts +82 -0
- package/tests/unit/git-worktree.test.ts +55 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
- package/tests/unit/services/review-strategy.test.ts +349 -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/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tests/vote.test.ts +317 -0
- package/tests/workspace.test.ts +295 -0
- package/tsconfig.json +36 -5
- package/src/commands/branch.ts +0 -196
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- package/src/utils.ts +0 -130
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { incomingCommand } from '@/cli/commands/incoming'
|
|
3
|
+
import { generateMockChange } from '@/test-utils/mock-generator'
|
|
4
|
+
import {
|
|
5
|
+
getStatusIndicators,
|
|
6
|
+
getStatusString,
|
|
7
|
+
getLabelValue,
|
|
8
|
+
getLabelColor,
|
|
9
|
+
} from '@/utils/status-indicators'
|
|
10
|
+
import { sanitizeUrlSync, getOpenCommand } from '@/utils/shell-safety'
|
|
11
|
+
|
|
12
|
+
describe('Interactive Incoming Command', () => {
|
|
13
|
+
test('should create command with interactive option', () => {
|
|
14
|
+
const command = incomingCommand({ interactive: true })
|
|
15
|
+
expect(command).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('should create command with interactive and xml options', () => {
|
|
19
|
+
const command = incomingCommand({ interactive: true, xml: true })
|
|
20
|
+
expect(command).toBeDefined()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('should create command without interactive option', () => {
|
|
24
|
+
const command = incomingCommand({})
|
|
25
|
+
expect(command).toBeDefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('Status Indicators Utility', () => {
|
|
29
|
+
test('should generate status indicators for approved change', () => {
|
|
30
|
+
const change = generateMockChange({
|
|
31
|
+
labels: {
|
|
32
|
+
'Code-Review': { approved: { _account_id: 1 }, value: 2 },
|
|
33
|
+
Verified: { approved: { _account_id: 1 }, value: 1 },
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const indicators = getStatusIndicators(change)
|
|
38
|
+
expect(indicators).toContain('✓')
|
|
39
|
+
expect(indicators.length).toBeGreaterThan(0)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('should generate status indicators for rejected change', () => {
|
|
43
|
+
const change = generateMockChange({
|
|
44
|
+
labels: {
|
|
45
|
+
'Code-Review': { rejected: { _account_id: 1 }, value: -2 },
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const indicators = getStatusIndicators(change)
|
|
50
|
+
expect(indicators).toContain('✗')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('should generate padded status string', () => {
|
|
54
|
+
const change = generateMockChange({
|
|
55
|
+
labels: {
|
|
56
|
+
'Code-Review': { recommended: { _account_id: 1 }, value: 1 },
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const statusString = getStatusString(change, undefined, 8)
|
|
61
|
+
expect(statusString).toContain('↑')
|
|
62
|
+
expect(statusString.length).toBe(8)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('should handle empty labels gracefully', () => {
|
|
66
|
+
const change = generateMockChange({ labels: {} })
|
|
67
|
+
const indicators = getStatusIndicators(change)
|
|
68
|
+
expect(indicators).toHaveLength(0)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('should extract label value safely', () => {
|
|
72
|
+
expect(getLabelValue({ value: 2 })).toBe(2)
|
|
73
|
+
expect(getLabelValue({ value: -1 })).toBe(-1)
|
|
74
|
+
expect(getLabelValue({})).toBe(0)
|
|
75
|
+
expect(getLabelValue(null)).toBe(0)
|
|
76
|
+
expect(getLabelValue('invalid')).toBe(0)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('should determine label color correctly', () => {
|
|
80
|
+
expect(getLabelColor(2)).toBe('green')
|
|
81
|
+
expect(getLabelColor(1)).toBe('green')
|
|
82
|
+
expect(getLabelColor(0)).toBe('yellow')
|
|
83
|
+
expect(getLabelColor(-1)).toBe('red')
|
|
84
|
+
expect(getLabelColor(-2)).toBe('red')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('URL Sanitization', () => {
|
|
89
|
+
test('should sanitize valid HTTPS URLs', () => {
|
|
90
|
+
const url = 'https://gerrit.example.com/c/project/+/12345'
|
|
91
|
+
expect(() => sanitizeUrlSync(url)).not.toThrow()
|
|
92
|
+
expect(sanitizeUrlSync(url)).toBe(url)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('should reject HTTP URLs', () => {
|
|
96
|
+
const url = 'http://gerrit.example.com/c/project/+/12345'
|
|
97
|
+
expect(() => sanitizeUrlSync(url)).toThrow('Invalid protocol')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('should reject URLs with dangerous characters', () => {
|
|
101
|
+
const url = 'https://gerrit.example.com/c/project/+/12345;rm -rf /'
|
|
102
|
+
expect(() => sanitizeUrlSync(url)).toThrow('dangerous characters')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('should reject malformed URLs', () => {
|
|
106
|
+
const url = 'not-a-url'
|
|
107
|
+
expect(() => sanitizeUrlSync(url)).toThrow('Invalid URL format')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('should get correct open command for platform', () => {
|
|
111
|
+
const originalPlatform = process.platform
|
|
112
|
+
|
|
113
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' })
|
|
114
|
+
expect(getOpenCommand()).toBe('open')
|
|
115
|
+
|
|
116
|
+
Object.defineProperty(process, 'platform', { value: 'win32' })
|
|
117
|
+
expect(getOpenCommand()).toBe('start')
|
|
118
|
+
|
|
119
|
+
Object.defineProperty(process, 'platform', { value: 'linux' })
|
|
120
|
+
expect(getOpenCommand()).toBe('xdg-open')
|
|
121
|
+
|
|
122
|
+
// Restore original platform
|
|
123
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('Change Data Processing', () => {
|
|
128
|
+
test('should handle changes with various label configurations', () => {
|
|
129
|
+
const changes = [
|
|
130
|
+
generateMockChange({
|
|
131
|
+
_number: 1,
|
|
132
|
+
labels: { 'Code-Review': { value: 2, approved: { _account_id: 1 } } },
|
|
133
|
+
}),
|
|
134
|
+
generateMockChange({
|
|
135
|
+
_number: 2,
|
|
136
|
+
labels: { 'Code-Review': { value: -1, disliked: { _account_id: 1 } } },
|
|
137
|
+
}),
|
|
138
|
+
generateMockChange({
|
|
139
|
+
_number: 3,
|
|
140
|
+
labels: {},
|
|
141
|
+
}),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
changes.forEach((change) => {
|
|
145
|
+
expect(() => getStatusIndicators(change)).not.toThrow()
|
|
146
|
+
expect(() => getStatusString(change)).not.toThrow()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('should group changes by project correctly', () => {
|
|
151
|
+
const changes = [
|
|
152
|
+
generateMockChange({ project: 'project-a', _number: 1 }),
|
|
153
|
+
generateMockChange({ project: 'project-b', _number: 2 }),
|
|
154
|
+
generateMockChange({ project: 'project-a', _number: 3 }),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
const grouped = changes.reduce(
|
|
158
|
+
(acc, change) => {
|
|
159
|
+
if (!acc[change.project]) {
|
|
160
|
+
acc[change.project] = []
|
|
161
|
+
}
|
|
162
|
+
acc[change.project].push(change)
|
|
163
|
+
return acc
|
|
164
|
+
},
|
|
165
|
+
{} as Record<string, typeof changes>,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
expect(Object.keys(grouped)).toHaveLength(2)
|
|
169
|
+
expect(grouped['project-a']).toHaveLength(2)
|
|
170
|
+
expect(grouped['project-b']).toHaveLength(1)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import { Effect, Layer } from 'effect'
|
|
3
|
+
import { HttpResponse, http } from 'msw'
|
|
4
|
+
import { setupServer } from 'msw/node'
|
|
5
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
6
|
+
import { mineCommand } from '@/cli/commands/mine'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { generateMockChange } from '@/test-utils/mock-generator'
|
|
9
|
+
import type { ChangeInfo } from '@/schemas/gerrit'
|
|
10
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
11
|
+
|
|
12
|
+
// Create MSW server
|
|
13
|
+
const server = setupServer(
|
|
14
|
+
// Default handler for auth check
|
|
15
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
16
|
+
const auth = request.headers.get('Authorization')
|
|
17
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
18
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
19
|
+
}
|
|
20
|
+
return HttpResponse.json({
|
|
21
|
+
_account_id: 1000,
|
|
22
|
+
name: 'Test User',
|
|
23
|
+
email: 'test@example.com',
|
|
24
|
+
})
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
describe('mine command', () => {
|
|
29
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
server.close()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockConsoleLog = mock(() => {})
|
|
41
|
+
console.log = mockConsoleLog
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
server.resetHandlers()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('should fetch and display my changes in pretty format', async () => {
|
|
49
|
+
const mockChanges: ChangeInfo[] = [
|
|
50
|
+
generateMockChange({
|
|
51
|
+
_number: 12345,
|
|
52
|
+
subject: 'My test change',
|
|
53
|
+
project: 'test-project',
|
|
54
|
+
branch: 'main',
|
|
55
|
+
status: 'NEW',
|
|
56
|
+
}),
|
|
57
|
+
generateMockChange({
|
|
58
|
+
_number: 12346,
|
|
59
|
+
subject: 'Another change',
|
|
60
|
+
project: 'test-project-2',
|
|
61
|
+
branch: 'develop',
|
|
62
|
+
status: 'MERGED',
|
|
63
|
+
}),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
server.use(
|
|
67
|
+
http.get('*/a/changes/', ({ request }) => {
|
|
68
|
+
const url = new URL(request.url)
|
|
69
|
+
expect(url.searchParams.get('q')).toBe('owner:self status:open')
|
|
70
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
75
|
+
await Effect.runPromise(
|
|
76
|
+
mineCommand({ xml: false }).pipe(
|
|
77
|
+
Effect.provide(GerritApiServiceLive),
|
|
78
|
+
Effect.provide(mockConfigLayer),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
83
|
+
expect(output.length).toBeGreaterThan(0)
|
|
84
|
+
expect(output).toContain('My test change')
|
|
85
|
+
expect(output).toContain('Another change')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('should output XML format when --xml flag is used', async () => {
|
|
89
|
+
const mockChanges: ChangeInfo[] = [
|
|
90
|
+
generateMockChange({
|
|
91
|
+
_number: 12345,
|
|
92
|
+
subject: 'Test change',
|
|
93
|
+
project: 'test-project',
|
|
94
|
+
branch: 'main',
|
|
95
|
+
status: 'NEW',
|
|
96
|
+
}),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
server.use(
|
|
100
|
+
http.get('*/a/changes/', ({ request }) => {
|
|
101
|
+
const url = new URL(request.url)
|
|
102
|
+
expect(url.searchParams.get('q')).toBe('owner:self status:open')
|
|
103
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
108
|
+
await Effect.runPromise(
|
|
109
|
+
mineCommand({ xml: true }).pipe(
|
|
110
|
+
Effect.provide(GerritApiServiceLive),
|
|
111
|
+
Effect.provide(mockConfigLayer),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
116
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
117
|
+
expect(output).toContain('<changes count="1">')
|
|
118
|
+
expect(output).toContain('<change>')
|
|
119
|
+
expect(output).toContain('<number>12345</number>')
|
|
120
|
+
expect(output).toContain('<subject><![CDATA[Test change]]></subject>')
|
|
121
|
+
expect(output).toContain('<project>test-project</project>')
|
|
122
|
+
expect(output).toContain('<branch>main</branch>')
|
|
123
|
+
expect(output).toContain('<status>NEW</status>')
|
|
124
|
+
expect(output).toContain('</change>')
|
|
125
|
+
expect(output).toContain('</changes>')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('should handle no changes gracefully', async () => {
|
|
129
|
+
server.use(
|
|
130
|
+
http.get('*/a/changes/', () => {
|
|
131
|
+
return HttpResponse.text(")]}'\n[]")
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
136
|
+
await Effect.runPromise(
|
|
137
|
+
mineCommand({ xml: false }).pipe(
|
|
138
|
+
Effect.provide(GerritApiServiceLive),
|
|
139
|
+
Effect.provide(mockConfigLayer),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Mine command returns early for empty results, so no output is expected
|
|
144
|
+
expect(mockConsoleLog.mock.calls).toEqual([])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('should handle no changes gracefully in XML format', async () => {
|
|
148
|
+
server.use(
|
|
149
|
+
http.get('*/a/changes/', () => {
|
|
150
|
+
return HttpResponse.text(")]}'\n[]")
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
155
|
+
await Effect.runPromise(
|
|
156
|
+
mineCommand({ xml: true }).pipe(
|
|
157
|
+
Effect.provide(GerritApiServiceLive),
|
|
158
|
+
Effect.provide(mockConfigLayer),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
163
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
164
|
+
expect(output).toContain('<changes count="0">')
|
|
165
|
+
expect(output).toContain('</changes>')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('should handle network failures gracefully', async () => {
|
|
169
|
+
server.use(
|
|
170
|
+
http.get('*/a/changes/', () => {
|
|
171
|
+
return HttpResponse.text('Network error', { status: 500 })
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
176
|
+
const result = await Effect.runPromise(
|
|
177
|
+
Effect.either(
|
|
178
|
+
mineCommand({ xml: false }).pipe(
|
|
179
|
+
Effect.provide(GerritApiServiceLive),
|
|
180
|
+
Effect.provide(mockConfigLayer),
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
expect(result._tag).toBe('Left')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('should handle network failures gracefully in XML format', async () => {
|
|
189
|
+
server.use(
|
|
190
|
+
http.get('*/a/changes/', () => {
|
|
191
|
+
return HttpResponse.text('API error', { status: 500 })
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
196
|
+
const result = await Effect.runPromise(
|
|
197
|
+
Effect.either(
|
|
198
|
+
mineCommand({ xml: true }).pipe(
|
|
199
|
+
Effect.provide(GerritApiServiceLive),
|
|
200
|
+
Effect.provide(mockConfigLayer),
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
expect(result._tag).toBe('Left')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('should properly escape XML special characters', async () => {
|
|
209
|
+
const mockChanges: ChangeInfo[] = [
|
|
210
|
+
generateMockChange({
|
|
211
|
+
_number: 12345,
|
|
212
|
+
subject: 'Test with <special> & "characters"',
|
|
213
|
+
project: 'test-project',
|
|
214
|
+
branch: 'feature/test&update',
|
|
215
|
+
status: 'NEW',
|
|
216
|
+
}),
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
server.use(
|
|
220
|
+
http.get('*/a/changes/', () => {
|
|
221
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
226
|
+
await Effect.runPromise(
|
|
227
|
+
mineCommand({ xml: true }).pipe(
|
|
228
|
+
Effect.provide(GerritApiServiceLive),
|
|
229
|
+
Effect.provide(mockConfigLayer),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
234
|
+
// CDATA sections should preserve special characters
|
|
235
|
+
expect(output).toContain('<![CDATA[Test with <special> & "characters"]]>')
|
|
236
|
+
expect(output).toContain('<branch>feature/test&update</branch>')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('should display changes with proper grouping by project', async () => {
|
|
240
|
+
const mockChanges: ChangeInfo[] = [
|
|
241
|
+
generateMockChange({
|
|
242
|
+
_number: 12345,
|
|
243
|
+
subject: 'Change in project A',
|
|
244
|
+
project: 'project-a',
|
|
245
|
+
branch: 'main',
|
|
246
|
+
status: 'NEW',
|
|
247
|
+
}),
|
|
248
|
+
generateMockChange({
|
|
249
|
+
_number: 12346,
|
|
250
|
+
subject: 'Change in project B',
|
|
251
|
+
project: 'project-b',
|
|
252
|
+
branch: 'main',
|
|
253
|
+
status: 'NEW',
|
|
254
|
+
}),
|
|
255
|
+
generateMockChange({
|
|
256
|
+
_number: 12347,
|
|
257
|
+
subject: 'Another change in project A',
|
|
258
|
+
project: 'project-a',
|
|
259
|
+
branch: 'develop',
|
|
260
|
+
status: 'MERGED',
|
|
261
|
+
}),
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
server.use(
|
|
265
|
+
http.get('*/a/changes/', () => {
|
|
266
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
|
|
267
|
+
}),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
271
|
+
await Effect.runPromise(
|
|
272
|
+
mineCommand({ xml: false }).pipe(
|
|
273
|
+
Effect.provide(GerritApiServiceLive),
|
|
274
|
+
Effect.provide(mockConfigLayer),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
279
|
+
expect(output).toContain('Change in project A')
|
|
280
|
+
expect(output).toContain('Change in project B')
|
|
281
|
+
expect(output).toContain('Another change in project A')
|
|
282
|
+
expect(output).toContain('project-a')
|
|
283
|
+
expect(output).toContain('project-b')
|
|
284
|
+
})
|
|
285
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { HttpResponse, http, type HttpHandler } from 'msw'
|
|
2
|
+
import type { CommentInfo } from '@/schemas/gerrit'
|
|
3
|
+
|
|
4
|
+
export const commentHandlers: HttpHandler[] = [
|
|
5
|
+
// Comments endpoint
|
|
6
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
7
|
+
const mockComments: Record<string, CommentInfo[]> = {
|
|
8
|
+
'/COMMIT_MSG': [
|
|
9
|
+
{
|
|
10
|
+
id: 'comment1',
|
|
11
|
+
message: 'Please update the commit message',
|
|
12
|
+
author: {
|
|
13
|
+
name: 'Reviewer 1',
|
|
14
|
+
email: 'reviewer1@example.com',
|
|
15
|
+
_account_id: 1001,
|
|
16
|
+
},
|
|
17
|
+
updated: '2024-01-15 10:30:00.000000000',
|
|
18
|
+
unresolved: true,
|
|
19
|
+
line: 3,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
'src/main.ts': [
|
|
23
|
+
{
|
|
24
|
+
id: 'comment2',
|
|
25
|
+
message: 'Consider using a more descriptive variable name',
|
|
26
|
+
author: {
|
|
27
|
+
name: 'Reviewer 2',
|
|
28
|
+
email: 'reviewer2@example.com',
|
|
29
|
+
_account_id: 1002,
|
|
30
|
+
},
|
|
31
|
+
updated: '2024-01-15 11:45:00.000000000',
|
|
32
|
+
unresolved: false,
|
|
33
|
+
line: 42,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'comment3',
|
|
37
|
+
message: 'This could be simplified',
|
|
38
|
+
author: {
|
|
39
|
+
name: 'Reviewer 1',
|
|
40
|
+
_account_id: 1001,
|
|
41
|
+
},
|
|
42
|
+
updated: '2024-01-15 12:00:00.000000000',
|
|
43
|
+
line: 67,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
// File diff endpoint
|
|
52
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/files/:filePath/diff', () => {
|
|
53
|
+
const mockDiff = {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
ab: ['function calculateTotal(items) {', ' let total = 0;'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
b: [
|
|
60
|
+
' // TODO: Add validation',
|
|
61
|
+
' for (const item of items) {',
|
|
62
|
+
' total += item.price * item.quantity;',
|
|
63
|
+
' }',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
ab: [' return total;', '}'],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockDiff)}`)
|
|
73
|
+
}),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
export const emptyCommentsHandlers: HttpHandler[] = [
|
|
77
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
78
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
79
|
+
}),
|
|
80
|
+
]
|