@aaronshaf/ger 1.2.10 → 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 -180
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- package/src/utils.ts +0 -130
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach, mock } from 'bun:test'
|
|
2
|
+
import { setupServer } from 'msw/node'
|
|
3
|
+
import { http, HttpResponse } from 'msw'
|
|
4
|
+
import { Effect, Layer } from 'effect'
|
|
5
|
+
import { showCommand } from '@/cli/commands/show'
|
|
6
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { generateMockChange } from '@/test-utils/mock-generator'
|
|
9
|
+
import type { MessageInfo } from '@/schemas/gerrit'
|
|
10
|
+
|
|
11
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
12
|
+
const server = setupServer(
|
|
13
|
+
// Default handler for auth check
|
|
14
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
15
|
+
const auth = request.headers.get('Authorization')
|
|
16
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
17
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
18
|
+
}
|
|
19
|
+
return HttpResponse.json({
|
|
20
|
+
_account_id: 1000,
|
|
21
|
+
name: 'Test User',
|
|
22
|
+
email: 'test@example.com',
|
|
23
|
+
})
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Store captured output
|
|
28
|
+
let capturedLogs: string[] = []
|
|
29
|
+
let capturedErrors: string[] = []
|
|
30
|
+
let capturedStdout: string[] = []
|
|
31
|
+
|
|
32
|
+
// Mock console.log and console.error
|
|
33
|
+
const mockConsoleLog = mock((...args: any[]) => {
|
|
34
|
+
capturedLogs.push(args.join(' '))
|
|
35
|
+
})
|
|
36
|
+
const mockConsoleError = mock((...args: any[]) => {
|
|
37
|
+
capturedErrors.push(args.join(' '))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Mock process.stdout.write to capture JSON output and handle callbacks
|
|
41
|
+
const mockStdoutWrite = mock((chunk: any, callback?: any) => {
|
|
42
|
+
capturedStdout.push(String(chunk))
|
|
43
|
+
// Call the callback synchronously if provided
|
|
44
|
+
if (typeof callback === 'function') {
|
|
45
|
+
callback()
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Store original methods
|
|
51
|
+
const originalConsoleLog = console.log
|
|
52
|
+
const originalConsoleError = console.error
|
|
53
|
+
const originalStdoutWrite = process.stdout.write
|
|
54
|
+
|
|
55
|
+
beforeAll(() => {
|
|
56
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
console.log = mockConsoleLog
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
console.error = mockConsoleError
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
process.stdout.write = mockStdoutWrite
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
afterAll(() => {
|
|
66
|
+
server.close()
|
|
67
|
+
console.log = originalConsoleLog
|
|
68
|
+
console.error = originalConsoleError
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
process.stdout.write = originalStdoutWrite
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
server.resetHandlers()
|
|
75
|
+
mockConsoleLog.mockClear()
|
|
76
|
+
mockConsoleError.mockClear()
|
|
77
|
+
mockStdoutWrite.mockClear()
|
|
78
|
+
capturedLogs = []
|
|
79
|
+
capturedErrors = []
|
|
80
|
+
capturedStdout = []
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('show command', () => {
|
|
84
|
+
const mockChange = generateMockChange({
|
|
85
|
+
_number: 12345,
|
|
86
|
+
change_id: 'I123abc456def',
|
|
87
|
+
subject: 'Fix authentication bug',
|
|
88
|
+
status: 'NEW',
|
|
89
|
+
project: 'test-project',
|
|
90
|
+
branch: 'main',
|
|
91
|
+
created: '2024-01-15 10:00:00.000000000',
|
|
92
|
+
updated: '2024-01-15 12:00:00.000000000',
|
|
93
|
+
owner: {
|
|
94
|
+
_account_id: 1001,
|
|
95
|
+
name: 'John Doe',
|
|
96
|
+
email: 'john@example.com',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const mockDiff = `--- a/src/auth.js
|
|
101
|
+
+++ b/src/auth.js
|
|
102
|
+
@@ -10,7 +10,8 @@ function authenticate(user) {
|
|
103
|
+
if (!user) {
|
|
104
|
+
- return false
|
|
105
|
+
+ throw new Error('User required')
|
|
106
|
+
}
|
|
107
|
+
+ // Added validation
|
|
108
|
+
return validateUser(user)
|
|
109
|
+
}`
|
|
110
|
+
|
|
111
|
+
const mockComments = {
|
|
112
|
+
'src/auth.js': [
|
|
113
|
+
{
|
|
114
|
+
id: 'comment1',
|
|
115
|
+
path: 'src/auth.js',
|
|
116
|
+
line: 12,
|
|
117
|
+
message: 'Good improvement!',
|
|
118
|
+
author: {
|
|
119
|
+
name: 'Jane Reviewer',
|
|
120
|
+
email: 'jane@example.com',
|
|
121
|
+
},
|
|
122
|
+
updated: '2024-01-15 11:30:00.000000000',
|
|
123
|
+
unresolved: false,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'comment2',
|
|
127
|
+
path: 'src/auth.js',
|
|
128
|
+
line: 14,
|
|
129
|
+
message: 'Consider adding JSDoc',
|
|
130
|
+
author: {
|
|
131
|
+
name: 'Bob Reviewer',
|
|
132
|
+
email: 'bob@example.com',
|
|
133
|
+
},
|
|
134
|
+
updated: '2024-01-15 11:45:00.000000000',
|
|
135
|
+
unresolved: true,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
'/COMMIT_MSG': [
|
|
139
|
+
{
|
|
140
|
+
id: 'comment3',
|
|
141
|
+
path: '/COMMIT_MSG',
|
|
142
|
+
line: 1,
|
|
143
|
+
message: 'Clear commit message',
|
|
144
|
+
author: {
|
|
145
|
+
name: 'Alice Lead',
|
|
146
|
+
email: 'alice@example.com',
|
|
147
|
+
},
|
|
148
|
+
updated: '2024-01-15 11:00:00.000000000',
|
|
149
|
+
unresolved: false,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const setupMockHandlers = () => {
|
|
155
|
+
server.use(
|
|
156
|
+
// Get change details
|
|
157
|
+
http.get('*/a/changes/:changeId', () => {
|
|
158
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
159
|
+
}),
|
|
160
|
+
// Get diff (returns base64-encoded content)
|
|
161
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
162
|
+
return HttpResponse.text(btoa(mockDiff))
|
|
163
|
+
}),
|
|
164
|
+
// Get comments
|
|
165
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
166
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
|
|
167
|
+
}),
|
|
168
|
+
// Get file diff for context (optional, may fail gracefully)
|
|
169
|
+
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
|
|
170
|
+
return HttpResponse.text(mockDiff)
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const createMockConfigLayer = () => Layer.succeed(ConfigService, createMockConfigService())
|
|
176
|
+
|
|
177
|
+
test('should display comprehensive change information in pretty format', async () => {
|
|
178
|
+
setupMockHandlers()
|
|
179
|
+
|
|
180
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
181
|
+
const program = showCommand('12345', {}).pipe(
|
|
182
|
+
Effect.provide(GerritApiServiceLive),
|
|
183
|
+
Effect.provide(mockConfigLayer),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await Effect.runPromise(program)
|
|
187
|
+
|
|
188
|
+
const output = capturedLogs.join('\n')
|
|
189
|
+
|
|
190
|
+
// Check that all sections are present
|
|
191
|
+
expect(output).toContain('📋 Change 12345: Fix authentication bug')
|
|
192
|
+
expect(output).toContain('📝 Details:')
|
|
193
|
+
expect(output).toContain('Project: test-project')
|
|
194
|
+
expect(output).toContain('Branch: main')
|
|
195
|
+
expect(output).toContain('Status: NEW')
|
|
196
|
+
expect(output).toContain('Owner: John Doe')
|
|
197
|
+
expect(output).toContain('Change-Id: I123abc456def')
|
|
198
|
+
expect(output).toContain('🔍 Diff:')
|
|
199
|
+
expect(output).toContain('💬 Inline Comments:')
|
|
200
|
+
|
|
201
|
+
// Check diff content is included
|
|
202
|
+
expect(output).toContain('src/auth.js')
|
|
203
|
+
expect(output).toContain('authenticate(user)')
|
|
204
|
+
|
|
205
|
+
// Check comments are included
|
|
206
|
+
expect(output).toContain('Good improvement!')
|
|
207
|
+
expect(output).toContain('Consider adding JSDoc')
|
|
208
|
+
expect(output).toContain('Clear commit message')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('should output XML format when --xml flag is used', async () => {
|
|
212
|
+
setupMockHandlers()
|
|
213
|
+
|
|
214
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
215
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
216
|
+
Effect.provide(GerritApiServiceLive),
|
|
217
|
+
Effect.provide(mockConfigLayer),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
await Effect.runPromise(program)
|
|
221
|
+
|
|
222
|
+
const output = capturedStdout.join('')
|
|
223
|
+
|
|
224
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
225
|
+
expect(output).toContain('<show_result>')
|
|
226
|
+
expect(output).toContain('<status>success</status>')
|
|
227
|
+
expect(output).toContain('<change>')
|
|
228
|
+
expect(output).toContain('<id>I123abc456def</id>')
|
|
229
|
+
expect(output).toContain('<number>12345</number>')
|
|
230
|
+
expect(output).toContain('<subject><![CDATA[Fix authentication bug]]></subject>')
|
|
231
|
+
expect(output).toContain('<status>NEW</status>')
|
|
232
|
+
expect(output).toContain('<project>test-project</project>')
|
|
233
|
+
expect(output).toContain('<branch>main</branch>')
|
|
234
|
+
expect(output).toContain('<owner>')
|
|
235
|
+
expect(output).toContain('<name><![CDATA[John Doe]]></name>')
|
|
236
|
+
expect(output).toContain('<email>john@example.com</email>')
|
|
237
|
+
expect(output).toContain('<diff><![CDATA[')
|
|
238
|
+
expect(output).toContain('<comments>')
|
|
239
|
+
expect(output).toContain('<count>3</count>')
|
|
240
|
+
expect(output).toContain('</show_result>')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('should handle API errors gracefully in pretty format', async () => {
|
|
244
|
+
server.use(
|
|
245
|
+
http.get('*/a/changes/:changeId', () => {
|
|
246
|
+
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
251
|
+
const program = showCommand('12345', {}).pipe(
|
|
252
|
+
Effect.provide(GerritApiServiceLive),
|
|
253
|
+
Effect.provide(mockConfigLayer),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
await Effect.runPromise(program)
|
|
257
|
+
|
|
258
|
+
const output = capturedErrors.join('\n')
|
|
259
|
+
expect(output).toContain('✗ Error:')
|
|
260
|
+
// The error message will be from the network layer
|
|
261
|
+
expect(output.length).toBeGreaterThan(0)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('should handle API errors gracefully in XML format', async () => {
|
|
265
|
+
server.use(
|
|
266
|
+
http.get('*/a/changes/:changeId', () => {
|
|
267
|
+
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
|
|
268
|
+
}),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
272
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
273
|
+
Effect.provide(GerritApiServiceLive),
|
|
274
|
+
Effect.provide(mockConfigLayer),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
await Effect.runPromise(program)
|
|
278
|
+
|
|
279
|
+
const output = capturedStdout.join('')
|
|
280
|
+
|
|
281
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
282
|
+
expect(output).toContain('<show_result>')
|
|
283
|
+
expect(output).toContain('<status>error</status>')
|
|
284
|
+
expect(output).toContain('<error><![CDATA[')
|
|
285
|
+
expect(output).toContain('</show_result>')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('should properly escape XML special characters', async () => {
|
|
289
|
+
const changeWithSpecialChars = generateMockChange({
|
|
290
|
+
_number: 12345,
|
|
291
|
+
change_id: 'I123abc456def',
|
|
292
|
+
subject: 'Fix "quotes" & <tags> in auth',
|
|
293
|
+
project: 'test-project',
|
|
294
|
+
branch: 'feature/fix&improve',
|
|
295
|
+
owner: {
|
|
296
|
+
_account_id: 1002,
|
|
297
|
+
name: 'User <with> & "special" chars',
|
|
298
|
+
email: 'user@example.com',
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
server.use(
|
|
303
|
+
http.get('*/a/changes/:changeId', () => {
|
|
304
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithSpecialChars)}`)
|
|
305
|
+
}),
|
|
306
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
307
|
+
return HttpResponse.text('diff content')
|
|
308
|
+
}),
|
|
309
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
310
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
315
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
316
|
+
Effect.provide(GerritApiServiceLive),
|
|
317
|
+
Effect.provide(mockConfigLayer),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
await Effect.runPromise(program)
|
|
321
|
+
|
|
322
|
+
const output = capturedStdout.join('')
|
|
323
|
+
|
|
324
|
+
expect(output).toContain('<subject><![CDATA[Fix "quotes" & <tags> in auth]]></subject>')
|
|
325
|
+
expect(output).toContain('<branch>feature/fix&improve</branch>')
|
|
326
|
+
expect(output).toContain('<name><![CDATA[User <with> & "special" chars]]></name>')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('should handle mixed file and commit message comments', async () => {
|
|
330
|
+
setupMockHandlers()
|
|
331
|
+
|
|
332
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
333
|
+
const program = showCommand('12345', {}).pipe(
|
|
334
|
+
Effect.provide(GerritApiServiceLive),
|
|
335
|
+
Effect.provide(mockConfigLayer),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
await Effect.runPromise(program)
|
|
339
|
+
|
|
340
|
+
const output = capturedLogs.join('\n')
|
|
341
|
+
|
|
342
|
+
// Should show comments from both files and commit message
|
|
343
|
+
expect(output).toContain('Good improvement!')
|
|
344
|
+
expect(output).toContain('Consider adding JSDoc')
|
|
345
|
+
expect(output).toContain('Clear commit message')
|
|
346
|
+
|
|
347
|
+
// Commit message path should be renamed
|
|
348
|
+
expect(output).toContain('Commit Message')
|
|
349
|
+
expect(output).not.toContain('/COMMIT_MSG')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('should handle changes with missing optional fields', async () => {
|
|
353
|
+
const minimalChange = generateMockChange({
|
|
354
|
+
_number: 12345,
|
|
355
|
+
change_id: 'I123abc456def',
|
|
356
|
+
subject: 'Minimal change',
|
|
357
|
+
status: 'NEW',
|
|
358
|
+
project: 'test-project',
|
|
359
|
+
branch: 'main',
|
|
360
|
+
owner: {
|
|
361
|
+
_account_id: 1003,
|
|
362
|
+
email: 'user@example.com',
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
server.use(
|
|
367
|
+
http.get('*/a/changes/:changeId', () => {
|
|
368
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(minimalChange)}`)
|
|
369
|
+
}),
|
|
370
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
371
|
+
return HttpResponse.text('minimal diff')
|
|
372
|
+
}),
|
|
373
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
374
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
379
|
+
const program = showCommand('12345', {}).pipe(
|
|
380
|
+
Effect.provide(GerritApiServiceLive),
|
|
381
|
+
Effect.provide(mockConfigLayer),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
await Effect.runPromise(program)
|
|
385
|
+
|
|
386
|
+
const output = capturedLogs.join('\n')
|
|
387
|
+
|
|
388
|
+
expect(output).toContain('📋 Change 12345: Minimal change')
|
|
389
|
+
expect(output).toContain('Owner: user@example.com') // Should fallback to email
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('should display review activity messages', async () => {
|
|
393
|
+
const mockChange = generateMockChange({
|
|
394
|
+
_number: 12345,
|
|
395
|
+
subject: 'Fix authentication bug',
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
const mockMessages: MessageInfo[] = [
|
|
399
|
+
{
|
|
400
|
+
id: 'msg1',
|
|
401
|
+
message: 'Patch Set 2: Code-Review+2',
|
|
402
|
+
author: { _account_id: 1001, name: 'Jane Reviewer' },
|
|
403
|
+
date: '2024-01-15 11:30:00.000000000',
|
|
404
|
+
_revision_number: 2,
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: 'msg2',
|
|
408
|
+
message: 'Patch Set 2: Verified+1\\n\\nBuild Successful',
|
|
409
|
+
author: { _account_id: 1002, name: 'Jenkins Bot' },
|
|
410
|
+
date: '2024-01-15 11:31:00.000000000',
|
|
411
|
+
_revision_number: 2,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: 'msg3',
|
|
415
|
+
message: 'Uploaded patch set 1.',
|
|
416
|
+
author: { _account_id: 1000, name: 'Author' },
|
|
417
|
+
date: '2024-01-15 11:29:00.000000000',
|
|
418
|
+
tag: 'autogenerated:gerrit:newPatchSet',
|
|
419
|
+
_revision_number: 1,
|
|
420
|
+
},
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
server.use(
|
|
424
|
+
http.get('*/a/changes/:changeId', ({ request }) => {
|
|
425
|
+
const url = new URL(request.url)
|
|
426
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
427
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: mockMessages })}`)
|
|
428
|
+
}
|
|
429
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
430
|
+
}),
|
|
431
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
432
|
+
return HttpResponse.text('diff content')
|
|
433
|
+
}),
|
|
434
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
435
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
436
|
+
}),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
440
|
+
const program = showCommand('12345', {}).pipe(
|
|
441
|
+
Effect.provide(GerritApiServiceLive),
|
|
442
|
+
Effect.provide(mockConfigLayer),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
await Effect.runPromise(program)
|
|
446
|
+
|
|
447
|
+
const output = capturedLogs.join('\n')
|
|
448
|
+
|
|
449
|
+
// Should display review activity section
|
|
450
|
+
expect(output).toContain('📝 Review Activity:')
|
|
451
|
+
expect(output).toContain('Jane Reviewer')
|
|
452
|
+
expect(output).toContain('Code-Review+2')
|
|
453
|
+
expect(output).toContain('Jenkins Bot')
|
|
454
|
+
expect(output).toContain('Build Successful')
|
|
455
|
+
|
|
456
|
+
// Should filter out autogenerated messages
|
|
457
|
+
expect(output).not.toContain('Uploaded patch set')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('should output JSON format when --json flag is used', async () => {
|
|
461
|
+
setupMockHandlers()
|
|
462
|
+
|
|
463
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
464
|
+
const program = showCommand('12345', { json: true }).pipe(
|
|
465
|
+
Effect.provide(GerritApiServiceLive),
|
|
466
|
+
Effect.provide(mockConfigLayer),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
await Effect.runPromise(program)
|
|
470
|
+
|
|
471
|
+
const output = capturedStdout.join('')
|
|
472
|
+
|
|
473
|
+
// Parse JSON to verify it's valid
|
|
474
|
+
const parsed = JSON.parse(output)
|
|
475
|
+
|
|
476
|
+
expect(parsed.status).toBe('success')
|
|
477
|
+
expect(parsed.change.id).toBe('I123abc456def')
|
|
478
|
+
expect(parsed.change.number).toBe(12345)
|
|
479
|
+
expect(parsed.change.subject).toBe('Fix authentication bug')
|
|
480
|
+
expect(parsed.change.status).toBe('NEW')
|
|
481
|
+
expect(parsed.change.project).toBe('test-project')
|
|
482
|
+
expect(parsed.change.branch).toBe('main')
|
|
483
|
+
expect(parsed.change.owner.name).toBe('John Doe')
|
|
484
|
+
expect(parsed.change.owner.email).toBe('john@example.com')
|
|
485
|
+
|
|
486
|
+
// Check diff is present
|
|
487
|
+
expect(parsed.diff).toContain('src/auth.js')
|
|
488
|
+
expect(parsed.diff).toContain('authenticate(user)')
|
|
489
|
+
|
|
490
|
+
// Check comments array
|
|
491
|
+
expect(Array.isArray(parsed.comments)).toBe(true)
|
|
492
|
+
expect(parsed.comments.length).toBe(3)
|
|
493
|
+
expect(parsed.comments[0].message).toContain('Clear commit message')
|
|
494
|
+
expect(parsed.comments[1].message).toBe('Good improvement!')
|
|
495
|
+
expect(parsed.comments[2].message).toBe('Consider adding JSDoc')
|
|
496
|
+
|
|
497
|
+
// Check messages array (should be empty for this test)
|
|
498
|
+
expect(Array.isArray(parsed.messages)).toBe(true)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
test('should handle API errors gracefully in JSON format', async () => {
|
|
502
|
+
server.use(
|
|
503
|
+
http.get('*/a/changes/:changeId', () => {
|
|
504
|
+
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
|
|
505
|
+
}),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
509
|
+
const program = showCommand('12345', { json: true }).pipe(
|
|
510
|
+
Effect.provide(GerritApiServiceLive),
|
|
511
|
+
Effect.provide(mockConfigLayer),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
await Effect.runPromise(program)
|
|
515
|
+
|
|
516
|
+
const output = capturedStdout.join('')
|
|
517
|
+
|
|
518
|
+
// Parse JSON to verify it's valid
|
|
519
|
+
const parsed = JSON.parse(output)
|
|
520
|
+
|
|
521
|
+
expect(parsed.status).toBe('error')
|
|
522
|
+
expect(parsed.error).toBeDefined()
|
|
523
|
+
expect(typeof parsed.error).toBe('string')
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
test('should sort comments by date in ascending order in XML output', async () => {
|
|
527
|
+
setupMockHandlers()
|
|
528
|
+
|
|
529
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
530
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
531
|
+
Effect.provide(GerritApiServiceLive),
|
|
532
|
+
Effect.provide(mockConfigLayer),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
await Effect.runPromise(program)
|
|
536
|
+
|
|
537
|
+
const output = capturedStdout.join('')
|
|
538
|
+
|
|
539
|
+
// Extract comment sections to verify order
|
|
540
|
+
const commentMatches = output.matchAll(
|
|
541
|
+
/<comment>[\s\S]*?<updated>(.*?)<\/updated>[\s\S]*?<message><!\[CDATA\[(.*?)\]\]><\/message>[\s\S]*?<\/comment>/g,
|
|
542
|
+
)
|
|
543
|
+
const comments = Array.from(commentMatches).map((match) => ({
|
|
544
|
+
updated: match[1],
|
|
545
|
+
message: match[2],
|
|
546
|
+
}))
|
|
547
|
+
|
|
548
|
+
// Should have 3 comments
|
|
549
|
+
expect(comments.length).toBe(3)
|
|
550
|
+
|
|
551
|
+
// Comments should be in ascending date order (oldest first)
|
|
552
|
+
expect(comments[0].updated).toBe('2024-01-15 11:00:00.000000000')
|
|
553
|
+
expect(comments[0].message).toBe('Clear commit message')
|
|
554
|
+
|
|
555
|
+
expect(comments[1].updated).toBe('2024-01-15 11:30:00.000000000')
|
|
556
|
+
expect(comments[1].message).toBe('Good improvement!')
|
|
557
|
+
|
|
558
|
+
expect(comments[2].updated).toBe('2024-01-15 11:45:00.000000000')
|
|
559
|
+
expect(comments[2].message).toBe('Consider adding JSDoc')
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
test('should include messages in JSON output', async () => {
|
|
563
|
+
const mockChange = generateMockChange({
|
|
564
|
+
_number: 12345,
|
|
565
|
+
subject: 'Fix authentication bug',
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
const mockMessages: MessageInfo[] = [
|
|
569
|
+
{
|
|
570
|
+
id: 'msg1',
|
|
571
|
+
message: 'Patch Set 2: Verified-1\\n\\nBuild Failed https://jenkins.example.com/job/123',
|
|
572
|
+
author: { _account_id: 1001, name: 'Jenkins Bot' },
|
|
573
|
+
date: '2024-01-15 11:30:00.000000000',
|
|
574
|
+
_revision_number: 2,
|
|
575
|
+
},
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
server.use(
|
|
579
|
+
http.get('*/a/changes/:changeId', ({ request }) => {
|
|
580
|
+
const url = new URL(request.url)
|
|
581
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
582
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: mockMessages })}`)
|
|
583
|
+
}
|
|
584
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
585
|
+
}),
|
|
586
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
587
|
+
return HttpResponse.text('diff content')
|
|
588
|
+
}),
|
|
589
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
590
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
591
|
+
}),
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
595
|
+
const program = showCommand('12345', { json: true }).pipe(
|
|
596
|
+
Effect.provide(GerritApiServiceLive),
|
|
597
|
+
Effect.provide(mockConfigLayer),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
await Effect.runPromise(program)
|
|
601
|
+
|
|
602
|
+
const output = capturedStdout.join('')
|
|
603
|
+
const parsed = JSON.parse(output)
|
|
604
|
+
|
|
605
|
+
expect(parsed.messages).toBeDefined()
|
|
606
|
+
expect(Array.isArray(parsed.messages)).toBe(true)
|
|
607
|
+
expect(parsed.messages.length).toBe(1)
|
|
608
|
+
expect(parsed.messages[0].message).toContain('Build Failed')
|
|
609
|
+
expect(parsed.messages[0].message).toContain('https://jenkins.example.com')
|
|
610
|
+
expect(parsed.messages[0].author.name).toBe('Jenkins Bot')
|
|
611
|
+
expect(parsed.messages[0].revision).toBe(2)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
test('should handle large JSON output without truncation', async () => {
|
|
615
|
+
// Create a large diff to simulate output > 64KB
|
|
616
|
+
const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
|
|
617
|
+
|
|
618
|
+
const mockChange = generateMockChange({
|
|
619
|
+
_number: 12345,
|
|
620
|
+
subject: 'Large change with extensive diff',
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
// Create many comments to increase JSON size
|
|
624
|
+
const manyComments: Record<string, any[]> = {
|
|
625
|
+
'src/file.js': Array.from({ length: 100 }, (_, i) => ({
|
|
626
|
+
id: `comment${i}`,
|
|
627
|
+
path: 'src/file.js',
|
|
628
|
+
line: i + 1,
|
|
629
|
+
message: `Comment ${i}: ${'a'.repeat(500)}`, // Make comments substantial
|
|
630
|
+
author: {
|
|
631
|
+
name: 'Reviewer',
|
|
632
|
+
email: 'reviewer@example.com',
|
|
633
|
+
},
|
|
634
|
+
updated: '2024-01-15 11:30:00.000000000',
|
|
635
|
+
unresolved: false,
|
|
636
|
+
})),
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
server.use(
|
|
640
|
+
http.get('*/a/changes/:changeId', () => {
|
|
641
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
642
|
+
}),
|
|
643
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
644
|
+
return HttpResponse.text(btoa(largeDiff))
|
|
645
|
+
}),
|
|
646
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
647
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(manyComments)}`)
|
|
648
|
+
}),
|
|
649
|
+
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
|
|
650
|
+
return HttpResponse.text('context')
|
|
651
|
+
}),
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
655
|
+
const program = showCommand('12345', { json: true }).pipe(
|
|
656
|
+
Effect.provide(GerritApiServiceLive),
|
|
657
|
+
Effect.provide(mockConfigLayer),
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
await Effect.runPromise(program)
|
|
661
|
+
|
|
662
|
+
const output = capturedStdout.join('')
|
|
663
|
+
|
|
664
|
+
// Verify output is larger than 64KB (the previous truncation point)
|
|
665
|
+
expect(output.length).toBeGreaterThan(65536)
|
|
666
|
+
|
|
667
|
+
// Verify JSON is valid and complete
|
|
668
|
+
const parsed = JSON.parse(output)
|
|
669
|
+
expect(parsed.status).toBe('success')
|
|
670
|
+
expect(parsed.diff).toContain('x'.repeat(100000))
|
|
671
|
+
expect(parsed.comments.length).toBe(100)
|
|
672
|
+
|
|
673
|
+
// Verify last comment is present (proves no truncation)
|
|
674
|
+
const lastComment = parsed.comments[parsed.comments.length - 1]
|
|
675
|
+
expect(lastComment.message).toContain('Comment 99')
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
test('should handle stdout drain event when buffer is full', async () => {
|
|
679
|
+
setupMockHandlers()
|
|
680
|
+
|
|
681
|
+
// Store original stdout.write
|
|
682
|
+
const originalStdoutWrite = process.stdout.write
|
|
683
|
+
|
|
684
|
+
let drainCallback: (() => void) | null = null
|
|
685
|
+
let errorCallback: ((err: Error) => void) | null = null
|
|
686
|
+
let writeCallbackFn: ((err?: Error) => void) | null = null
|
|
687
|
+
|
|
688
|
+
// Mock stdout.write to simulate full buffer
|
|
689
|
+
const mockWrite = mock((chunk: any, callback?: any) => {
|
|
690
|
+
capturedStdout.push(String(chunk))
|
|
691
|
+
writeCallbackFn = callback
|
|
692
|
+
// Return false to simulate full buffer
|
|
693
|
+
return false
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
// Mock stdout.once to capture drain and error listeners
|
|
697
|
+
const mockOnce = mock((event: string, callback: any) => {
|
|
698
|
+
if (event === 'drain') {
|
|
699
|
+
drainCallback = callback
|
|
700
|
+
// Simulate drain event after a short delay
|
|
701
|
+
setTimeout(() => {
|
|
702
|
+
if (drainCallback) {
|
|
703
|
+
drainCallback()
|
|
704
|
+
if (writeCallbackFn) {
|
|
705
|
+
writeCallbackFn()
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}, 10)
|
|
709
|
+
} else if (event === 'error') {
|
|
710
|
+
errorCallback = callback
|
|
711
|
+
}
|
|
712
|
+
return process.stdout
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
// Apply mocks
|
|
716
|
+
// @ts-ignore
|
|
717
|
+
process.stdout.write = mockWrite
|
|
718
|
+
// @ts-ignore
|
|
719
|
+
process.stdout.once = mockOnce
|
|
720
|
+
|
|
721
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
722
|
+
const program = showCommand('12345', { json: true }).pipe(
|
|
723
|
+
Effect.provide(GerritApiServiceLive),
|
|
724
|
+
Effect.provide(mockConfigLayer),
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
await Effect.runPromise(program)
|
|
728
|
+
|
|
729
|
+
// Restore original stdout.write
|
|
730
|
+
// @ts-ignore
|
|
731
|
+
process.stdout.write = originalStdoutWrite
|
|
732
|
+
|
|
733
|
+
// Verify that write returned false (buffer full)
|
|
734
|
+
expect(mockWrite).toHaveBeenCalled()
|
|
735
|
+
|
|
736
|
+
// Verify that drain listener was registered
|
|
737
|
+
expect(mockOnce).toHaveBeenCalledWith('drain', expect.any(Function))
|
|
738
|
+
|
|
739
|
+
// Verify that error listener was registered for robustness
|
|
740
|
+
expect(mockOnce).toHaveBeenCalledWith('error', expect.any(Function))
|
|
741
|
+
|
|
742
|
+
// Verify output is still valid JSON despite drain handling
|
|
743
|
+
const output = capturedStdout.join('')
|
|
744
|
+
const parsed = JSON.parse(output)
|
|
745
|
+
expect(parsed.status).toBe('success')
|
|
746
|
+
expect(parsed.change.id).toBe('I123abc456def')
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
test('should handle large XML output without truncation', async () => {
|
|
750
|
+
// Create a large diff to simulate output > 64KB
|
|
751
|
+
const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
|
|
752
|
+
|
|
753
|
+
const mockChange = generateMockChange({
|
|
754
|
+
_number: 12345,
|
|
755
|
+
subject: 'Large change with extensive diff',
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
// Create many comments to increase XML size
|
|
759
|
+
const manyComments: Record<string, any[]> = {
|
|
760
|
+
'src/file.js': Array.from({ length: 100 }, (_, i) => ({
|
|
761
|
+
id: `comment${i}`,
|
|
762
|
+
path: 'src/file.js',
|
|
763
|
+
line: i + 1,
|
|
764
|
+
message: `Comment ${i}: ${'a'.repeat(500)}`,
|
|
765
|
+
author: {
|
|
766
|
+
name: 'Reviewer',
|
|
767
|
+
email: 'reviewer@example.com',
|
|
768
|
+
},
|
|
769
|
+
updated: '2024-01-15 11:30:00.000000000',
|
|
770
|
+
unresolved: false,
|
|
771
|
+
})),
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
server.use(
|
|
775
|
+
http.get('*/a/changes/:changeId', () => {
|
|
776
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
777
|
+
}),
|
|
778
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
779
|
+
return HttpResponse.text(btoa(largeDiff))
|
|
780
|
+
}),
|
|
781
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
782
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(manyComments)}`)
|
|
783
|
+
}),
|
|
784
|
+
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
|
|
785
|
+
return HttpResponse.text('context')
|
|
786
|
+
}),
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
790
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
791
|
+
Effect.provide(GerritApiServiceLive),
|
|
792
|
+
Effect.provide(mockConfigLayer),
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
await Effect.runPromise(program)
|
|
796
|
+
|
|
797
|
+
const output = capturedStdout.join('')
|
|
798
|
+
|
|
799
|
+
// Verify output is larger than 64KB
|
|
800
|
+
expect(output.length).toBeGreaterThan(65536)
|
|
801
|
+
|
|
802
|
+
// Verify XML is valid and complete
|
|
803
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
804
|
+
expect(output).toContain('<show_result>')
|
|
805
|
+
expect(output).toContain('<status>success</status>')
|
|
806
|
+
expect(output).toContain('x'.repeat(100000))
|
|
807
|
+
expect(output).toContain('<count>100</count>')
|
|
808
|
+
expect(output).toContain('</show_result>')
|
|
809
|
+
|
|
810
|
+
// Verify last comment is present (proves no truncation)
|
|
811
|
+
expect(output).toContain('Comment 99')
|
|
812
|
+
})
|
|
813
|
+
})
|