@aaronshaf/ger 2.0.2 → 2.0.3

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.
@@ -0,0 +1,42 @@
1
+ # ADR 0023: Surface Reviewers and CCs in `ger show`
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ `ger` supports reviewer management (`add-reviewer`, `remove-reviewer`) but did not expose a reliable way to view current reviewers for a change in one command.
10
+
11
+ This created a workflow gap:
12
+
13
+ - Users could mutate reviewer state but not inspect it from the CLI.
14
+ - `show` already served as the canonical "single change detail" command and was the best place to surface reviewer assignments.
15
+
16
+ ## Decision
17
+
18
+ Add reviewer visibility to `ger show` by:
19
+
20
+ 1. Keeping `getChange` lightweight and using a `listChanges` fallback (with detailed account/label options) when reviewer state is not present in the base change response.
21
+ 2. Extending `ChangeInfo` schema with Gerrit reviewer state maps (`REVIEWER`, `CC`, `REMOVED`).
22
+ 3. Rendering reviewers and CCs in all `show` output formats (pretty, JSON, XML).
23
+
24
+ ## Rationale
25
+
26
+ - **Single source of truth**: `show` remains the canonical command for full change context.
27
+ - **No new command surface**: avoids adding a narrowly scoped `list-reviewers` command.
28
+ - **Automation-friendly**: JSON/XML consumers can parse reviewer state without scraping text output.
29
+ - **Backward compatible**: reviewer fields are optional and do not break servers or older data shapes.
30
+
31
+ ## Consequences
32
+
33
+ ### Positive
34
+
35
+ - Users can verify reviewer assignment directly after add/remove operations.
36
+ - Better parity between mutation commands and read visibility.
37
+ - More complete machine-readable change payloads.
38
+
39
+ ### Negative
40
+
41
+ - Extra `listChanges` request when reviewer data is absent from `getChange`.
42
+ - Additional schema/output maintenance for reviewer state rendering.
@@ -28,3 +28,4 @@ Records of significant architectural decisions with context and rationale. Each
28
28
  | [0020](0020-code-coverage-enforcement.md) | 80% coverage threshold in pre-commit |
29
29
  | [0021](0021-typescript-isolated-declarations.md) | Explicit return types on exports |
30
30
  | [0022](0022-biome-oxlint-tooling.md) | Biome formatter + oxlint linter |
31
+ | [0023](0023-show-reviewer-list.md) | Surface reviewers and CCs in `ger show` |
@@ -23,11 +23,13 @@ ger show # Auto-detect from HEAD
23
23
 
24
24
  **Output includes:**
25
25
  - Change metadata (number, project, branch, status)
26
- - Owner and reviewer information
26
+ - Owner, reviewers, and CC information
27
27
  - Submit requirements
28
28
  - Full diff
29
29
  - All comments with context
30
30
 
31
+ Reviewer listing for a specific change is provided by `show` (there is no separate `list-reviewers` command).
32
+
31
33
  ### diff
32
34
 
33
35
  Get change diff in various formats.
@@ -14,25 +14,36 @@ const ChangeInfo = Schema.Struct({
14
14
  _number: Schema.Number, // Numeric change ID
15
15
  project: Schema.String, // Project name
16
16
  branch: Schema.String, // Target branch
17
+ change_id: Schema.String, // Gerrit Change-Id
17
18
  topic: Schema.optional(Schema.String),
18
19
  subject: Schema.String, // First line of commit message
19
20
  status: Schema.Literal('NEW', 'MERGED', 'ABANDONED', 'DRAFT'),
20
- created: Schema.String, // ISO timestamp
21
- updated: Schema.String, // ISO timestamp
22
- submitted: Schema.optional(Schema.String),
23
- submitter: Schema.optional(AccountInfo),
24
- owner: AccountInfo,
21
+ created: Schema.optional(Schema.String),
22
+ updated: Schema.optional(Schema.String),
23
+ owner: Schema.optional(AccountInfo),
25
24
  current_revision: Schema.optional(Schema.String),
26
25
  revisions: Schema.optional(Schema.Record(Schema.String, RevisionInfo)),
27
26
  labels: Schema.optional(Schema.Record(Schema.String, LabelInfo)),
28
- reviewers: Schema.optional(ReviewerMap),
29
- messages: Schema.optional(Schema.Array(ChangeMessage)),
30
- mergeable: Schema.optional(Schema.Boolean),
27
+ reviewers: Schema.optional(ReviewerStateMap),
28
+ submittable: Schema.optional(Schema.Boolean),
29
+ work_in_progress: Schema.optional(Schema.Boolean),
31
30
  insertions: Schema.optional(Schema.Number),
32
31
  deletions: Schema.optional(Schema.Number),
33
32
  })
34
33
  ```
35
34
 
35
+ ### ReviewerStateMap
36
+
37
+ Reviewer assignments grouped by Gerrit reviewer state.
38
+
39
+ ```typescript
40
+ const ReviewerStateMap = Schema.Struct({
41
+ REVIEWER: Schema.optional(Schema.Array(AccountInfo)),
42
+ CC: Schema.optional(Schema.Array(AccountInfo)),
43
+ REMOVED: Schema.optional(Schema.Array(AccountInfo)),
44
+ })
45
+ ```
46
+
36
47
  ### AccountInfo
37
48
 
38
49
  User account information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS",
5
5
  "keywords": [
6
6
  "gerrit",
@@ -7,7 +7,6 @@ import { formatDiffPretty } from '@/utils/diff-formatters'
7
7
  import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
8
8
  import { formatDate } from '@/utils/formatters'
9
9
  import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
10
- import { writeFileSync } from 'node:fs'
11
10
 
12
11
  export const SHOW_HELP_TEXT = `
13
12
  Examples:
@@ -34,6 +33,13 @@ interface ShowOptions {
34
33
  json?: boolean
35
34
  }
36
35
 
36
+ interface ReviewerIdentity {
37
+ accountId?: number
38
+ name?: string
39
+ email?: string
40
+ username?: string
41
+ }
42
+
37
43
  interface ChangeDetails {
38
44
  id: string
39
45
  number: number
@@ -49,6 +55,24 @@ interface ChangeDetails {
49
55
  updated?: string
50
56
  commitMessage: string
51
57
  topic?: string
58
+ reviewers: ReviewerIdentity[]
59
+ ccs: ReviewerIdentity[]
60
+ }
61
+
62
+ const formatReviewerLabel = (reviewer: ReviewerIdentity): string => {
63
+ const preferredIdentity = reviewer.name || reviewer.email || reviewer.username
64
+ if (!preferredIdentity) {
65
+ if (reviewer.accountId !== undefined) {
66
+ return `Account ${reviewer.accountId}`
67
+ }
68
+ return 'Unknown Reviewer'
69
+ }
70
+
71
+ if (reviewer.email && reviewer.name && reviewer.name !== reviewer.email) {
72
+ return `${reviewer.name} <${reviewer.email}>`
73
+ }
74
+
75
+ return preferredIdentity
52
76
  }
53
77
 
54
78
  const getChangeDetails = (
@@ -58,6 +82,21 @@ const getChangeDetails = (
58
82
  const gerritApi = yield* GerritApiService
59
83
  const change = yield* gerritApi.getChange(changeId)
60
84
 
85
+ let reviewerMap = change.reviewers
86
+ const shouldFetchReviewerFallback =
87
+ reviewerMap === undefined ||
88
+ (reviewerMap.REVIEWER === undefined && reviewerMap.CC === undefined)
89
+
90
+ if (shouldFetchReviewerFallback) {
91
+ const detailedChanges = yield* gerritApi
92
+ .listChanges(`change:${change._number}`)
93
+ .pipe(Effect.catchAll(() => Effect.succeed([])))
94
+ const detailedChange =
95
+ detailedChanges.find((candidate) => candidate._number === change._number) ||
96
+ detailedChanges[0]
97
+ reviewerMap = detailedChange?.reviewers
98
+ }
99
+
61
100
  return {
62
101
  id: change.change_id,
63
102
  number: change._number,
@@ -73,6 +112,18 @@ const getChangeDetails = (
73
112
  updated: change.updated,
74
113
  commitMessage: change.subject, // For now, using subject as commit message
75
114
  topic: change.topic,
115
+ reviewers: (reviewerMap?.REVIEWER ?? []).map((reviewer) => ({
116
+ accountId: reviewer._account_id,
117
+ name: reviewer.name,
118
+ email: reviewer.email,
119
+ username: reviewer.username,
120
+ })),
121
+ ccs: (reviewerMap?.CC ?? []).map((cc) => ({
122
+ accountId: cc._account_id,
123
+ name: cc.name,
124
+ email: cc.email,
125
+ username: cc.username,
126
+ })),
76
127
  }
77
128
  })
78
129
 
@@ -154,6 +205,14 @@ const formatShowPretty = (
154
205
  console.log(
155
206
  ` Updated: ${changeDetails.updated ? formatDate(changeDetails.updated) : 'Unknown'}`,
156
207
  )
208
+ if (changeDetails.reviewers.length > 0) {
209
+ console.log(
210
+ ` Reviewers: ${changeDetails.reviewers.map((reviewer) => formatReviewerLabel(reviewer)).join(', ')}`,
211
+ )
212
+ }
213
+ if (changeDetails.ccs.length > 0) {
214
+ console.log(` CCs: ${changeDetails.ccs.map((cc) => formatReviewerLabel(cc)).join(', ')}`)
215
+ }
157
216
  console.log(` Change-Id: ${changeDetails.id}`)
158
217
  console.log()
159
218
 
@@ -227,6 +286,22 @@ const formatShowJson = async (
227
286
  branch: changeDetails.branch,
228
287
  topic: changeDetails.topic,
229
288
  owner: removeUndefined(changeDetails.owner),
289
+ reviewers: changeDetails.reviewers.map((reviewer) =>
290
+ removeUndefined({
291
+ account_id: reviewer.accountId,
292
+ name: reviewer.name,
293
+ email: reviewer.email,
294
+ username: reviewer.username,
295
+ }),
296
+ ),
297
+ ccs: changeDetails.ccs.map((cc) =>
298
+ removeUndefined({
299
+ account_id: cc.accountId,
300
+ name: cc.name,
301
+ email: cc.email,
302
+ username: cc.username,
303
+ }),
304
+ ),
230
305
  created: changeDetails.created,
231
306
  updated: changeDetails.updated,
232
307
  }),
@@ -318,6 +393,44 @@ const formatShowXml = async (
318
393
  xmlParts.push(` <email>${escapeXML(changeDetails.owner.email)}</email>`)
319
394
  }
320
395
  xmlParts.push(` </owner>`)
396
+ xmlParts.push(` <reviewers>`)
397
+ xmlParts.push(` <count>${changeDetails.reviewers.length}</count>`)
398
+ for (const reviewer of changeDetails.reviewers) {
399
+ xmlParts.push(` <reviewer>`)
400
+ if (reviewer.accountId !== undefined) {
401
+ xmlParts.push(` <account_id>${reviewer.accountId}</account_id>`)
402
+ }
403
+ if (reviewer.name) {
404
+ xmlParts.push(` <name><![CDATA[${sanitizeCDATA(reviewer.name)}]]></name>`)
405
+ }
406
+ if (reviewer.email) {
407
+ xmlParts.push(` <email>${escapeXML(reviewer.email)}</email>`)
408
+ }
409
+ if (reviewer.username) {
410
+ xmlParts.push(` <username>${escapeXML(reviewer.username)}</username>`)
411
+ }
412
+ xmlParts.push(` </reviewer>`)
413
+ }
414
+ xmlParts.push(` </reviewers>`)
415
+ xmlParts.push(` <ccs>`)
416
+ xmlParts.push(` <count>${changeDetails.ccs.length}</count>`)
417
+ for (const cc of changeDetails.ccs) {
418
+ xmlParts.push(` <cc>`)
419
+ if (cc.accountId !== undefined) {
420
+ xmlParts.push(` <account_id>${cc.accountId}</account_id>`)
421
+ }
422
+ if (cc.name) {
423
+ xmlParts.push(` <name><![CDATA[${sanitizeCDATA(cc.name)}]]></name>`)
424
+ }
425
+ if (cc.email) {
426
+ xmlParts.push(` <email>${escapeXML(cc.email)}</email>`)
427
+ }
428
+ if (cc.username) {
429
+ xmlParts.push(` <username>${escapeXML(cc.username)}</username>`)
430
+ }
431
+ xmlParts.push(` </cc>`)
432
+ }
433
+ xmlParts.push(` </ccs>`)
321
434
  xmlParts.push(` <created>${escapeXML(changeDetails.created || '')}</created>`)
322
435
  xmlParts.push(` <updated>${escapeXML(changeDetails.updated || '')}</updated>`)
323
436
  xmlParts.push(` </change>`)
@@ -1,6 +1,5 @@
1
1
  import { Schema } from '@effect/schema'
2
2
 
3
- // Authentication schemas
4
3
  export const GerritCredentials: Schema.Schema<{
5
4
  readonly host: string
6
5
  readonly username: string
@@ -21,7 +20,6 @@ export const GerritCredentials: Schema.Schema<{
21
20
  })
22
21
  export type GerritCredentials = Schema.Schema.Type<typeof GerritCredentials>
23
22
 
24
- // Forward declare RevisionInfo type for use in ChangeInfo
25
23
  export interface RevisionInfoType {
26
24
  readonly kind?: string
27
25
  readonly _number: number
@@ -65,7 +63,22 @@ export interface RevisionInfoType {
65
63
  >
66
64
  }
67
65
 
68
- // Change schemas
66
+ type ChangeReviewerAccount = {
67
+ readonly _account_id?: number
68
+ readonly name?: string
69
+ readonly email?: string
70
+ readonly username?: string
71
+ }
72
+ const ChangeReviewerAccountInfo: Schema.Schema<ChangeReviewerAccount> = Schema.Struct({
73
+ _account_id: Schema.optional(Schema.Number),
74
+ name: Schema.optional(Schema.String),
75
+ email: Schema.optional(Schema.String),
76
+ username: Schema.optional(Schema.String),
77
+ })
78
+ type ChangeReviewerMap = Partial<
79
+ Record<'REVIEWER' | 'CC' | 'REMOVED', ReadonlyArray<ChangeReviewerAccount>>
80
+ >
81
+
69
82
  export const ChangeInfo: Schema.Schema<{
70
83
  readonly id: string
71
84
  readonly project: string
@@ -119,6 +132,7 @@ export const ChangeInfo: Schema.Schema<{
119
132
  readonly current_revision?: string
120
133
  readonly revisions?: Record<string, RevisionInfoType>
121
134
  readonly topic?: string
135
+ readonly reviewers?: ChangeReviewerMap
122
136
  }> = Schema.Struct({
123
137
  id: Schema.String,
124
138
  project: Schema.String,
@@ -184,10 +198,16 @@ export const ChangeInfo: Schema.Schema<{
184
198
  current_revision: Schema.optional(Schema.String),
185
199
  revisions: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Any })),
186
200
  topic: Schema.optional(Schema.String),
201
+ reviewers: Schema.optional(
202
+ Schema.Struct({
203
+ REVIEWER: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
204
+ CC: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
205
+ REMOVED: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
206
+ }),
207
+ ),
187
208
  })
188
209
  export type ChangeInfo = Schema.Schema.Type<typeof ChangeInfo>
189
210
 
190
- // Comment schemas
191
211
  export const CommentInput: Schema.Schema<{
192
212
  readonly message: string
193
213
  readonly unresolved?: boolean
@@ -200,7 +220,6 @@ export const CommentInput: Schema.Schema<{
200
220
  })
201
221
  export type CommentInput = Schema.Schema.Type<typeof CommentInput>
202
222
 
203
- // Comment info returned from API
204
223
  export const CommentInfo: Schema.Schema<{
205
224
  readonly id: string
206
225
  readonly path?: string
@@ -246,7 +265,6 @@ export const CommentInfo: Schema.Schema<{
246
265
  })
247
266
  export type CommentInfo = Schema.Schema.Type<typeof CommentInfo>
248
267
 
249
- // Message info for review messages
250
268
  export const MessageInfo: Schema.Schema<{
251
269
  readonly id: string
252
270
  readonly message: string
@@ -319,7 +337,6 @@ export const ReviewInput: Schema.Schema<{
319
337
  })
320
338
  export type ReviewInput = Schema.Schema.Type<typeof ReviewInput>
321
339
 
322
- // Project schema
323
340
  export const ProjectInfo: Schema.Schema<{
324
341
  readonly id: string
325
342
  readonly name: string
@@ -333,7 +350,6 @@ export const ProjectInfo: Schema.Schema<{
333
350
  })
334
351
  export type ProjectInfo = Schema.Schema.Type<typeof ProjectInfo>
335
352
 
336
- // File and diff schemas
337
353
  export const FileInfo: Schema.Schema<{
338
354
  readonly status?: 'A' | 'D' | 'R' | 'C' | 'M'
339
355
  readonly lines_inserted?: number
@@ -95,6 +95,27 @@ describe('show command', () => {
95
95
  name: 'John Doe',
96
96
  email: 'john@example.com',
97
97
  },
98
+ reviewers: {
99
+ REVIEWER: [
100
+ {
101
+ _account_id: 2001,
102
+ name: 'Jane Reviewer',
103
+ email: 'jane.reviewer@example.com',
104
+ username: 'jreviewer',
105
+ },
106
+ {
107
+ email: 'second.reviewer@example.com',
108
+ username: 'sreviewer',
109
+ },
110
+ ],
111
+ CC: [
112
+ {
113
+ _account_id: 2003,
114
+ name: 'Team Observer',
115
+ email: 'observer@example.com',
116
+ },
117
+ ],
118
+ },
98
119
  })
99
120
 
100
121
  const mockDiff = `--- a/src/auth.js
@@ -194,6 +215,9 @@ describe('show command', () => {
194
215
  expect(output).toContain('Branch: main')
195
216
  expect(output).toContain('Status: NEW')
196
217
  expect(output).toContain('Owner: John Doe')
218
+ expect(output).toContain('Reviewers: Jane Reviewer <jane.reviewer@example.com>')
219
+ expect(output).toContain('second.reviewer@example.com')
220
+ expect(output).toContain('CCs: Team Observer <observer@example.com>')
197
221
  expect(output).toContain('Change-Id: I123abc456def')
198
222
  expect(output).toContain('🔍 Diff:')
199
223
  expect(output).toContain('💬 Inline Comments:')
@@ -234,6 +258,13 @@ describe('show command', () => {
234
258
  expect(output).toContain('<owner>')
235
259
  expect(output).toContain('<name><![CDATA[John Doe]]></name>')
236
260
  expect(output).toContain('<email>john@example.com</email>')
261
+ expect(output).toContain('<reviewers>')
262
+ expect(output).toContain('<count>2</count>')
263
+ expect(output).toContain('<name><![CDATA[Jane Reviewer]]></name>')
264
+ expect(output).toContain('<ccs>')
265
+ expect(output).toContain('<count>1</count>')
266
+ expect(output).toContain('<name><![CDATA[Team Observer]]></name>')
267
+ expect(output).not.toContain('<account_id>undefined</account_id>')
237
268
  expect(output).toContain('<diff><![CDATA[')
238
269
  expect(output).toContain('<comments>')
239
270
  expect(output).toContain('<count>3</count>')
@@ -482,6 +513,14 @@ describe('show command', () => {
482
513
  expect(parsed.change.branch).toBe('main')
483
514
  expect(parsed.change.owner.name).toBe('John Doe')
484
515
  expect(parsed.change.owner.email).toBe('john@example.com')
516
+ expect(Array.isArray(parsed.change.reviewers)).toBe(true)
517
+ expect(parsed.change.reviewers.length).toBe(2)
518
+ expect(parsed.change.reviewers[0].name).toBe('Jane Reviewer')
519
+ expect(parsed.change.reviewers[1].email).toBe('second.reviewer@example.com')
520
+ expect(parsed.change.reviewers[1].account_id).toBeUndefined()
521
+ expect(Array.isArray(parsed.change.ccs)).toBe(true)
522
+ expect(parsed.change.ccs.length).toBe(1)
523
+ expect(parsed.change.ccs[0].name).toBe('Team Observer')
485
524
 
486
525
  // Check diff is present
487
526
  expect(parsed.diff).toContain('src/auth.js')
@@ -611,6 +650,93 @@ describe('show command', () => {
611
650
  expect(parsed.messages[0].revision).toBe(2)
612
651
  })
613
652
 
653
+ test('should fetch reviewers from listChanges when getChange lacks reviewer data', async () => {
654
+ let listChangesOptions: string[] = []
655
+ let listChangesQuery = ''
656
+
657
+ const changeWithoutReviewers = {
658
+ ...mockChange,
659
+ reviewers: undefined,
660
+ }
661
+
662
+ server.use(
663
+ http.get('*/a/changes/:changeId', ({ request }) => {
664
+ const url = new URL(request.url)
665
+ if (url.searchParams.get('o') === 'MESSAGES') {
666
+ return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: [] })}`)
667
+ }
668
+ return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithoutReviewers)}`)
669
+ }),
670
+ http.get('*/a/changes/', ({ request }) => {
671
+ const url = new URL(request.url)
672
+ listChangesOptions = url.searchParams.getAll('o')
673
+ listChangesQuery = url.searchParams.get('q') || ''
674
+ return HttpResponse.text(`)]}'\n${JSON.stringify([mockChange])}`)
675
+ }),
676
+ http.get('*/a/changes/:changeId/revisions/current/patch', () => {
677
+ return HttpResponse.text(btoa(mockDiff))
678
+ }),
679
+ http.get('*/a/changes/:changeId/revisions/current/comments', () => {
680
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
681
+ }),
682
+ )
683
+
684
+ const mockConfigLayer = createMockConfigLayer()
685
+ const program = showCommand('12345', {}).pipe(
686
+ Effect.provide(GerritApiServiceLive),
687
+ Effect.provide(mockConfigLayer),
688
+ )
689
+
690
+ await Effect.runPromise(program)
691
+
692
+ expect(listChangesQuery).toBe('change:12345')
693
+ expect(listChangesOptions).toContain('LABELS')
694
+ expect(listChangesOptions).toContain('DETAILED_LABELS')
695
+ expect(listChangesOptions).toContain('DETAILED_ACCOUNTS')
696
+ })
697
+
698
+ test('should not fetch listChanges when reviewer data is explicitly present but empty', async () => {
699
+ let listChangesCalled = false
700
+
701
+ const changeWithEmptyReviewerLists = {
702
+ ...mockChange,
703
+ reviewers: {
704
+ REVIEWER: [],
705
+ CC: [],
706
+ },
707
+ }
708
+
709
+ server.use(
710
+ http.get('*/a/changes/:changeId', ({ request }) => {
711
+ const url = new URL(request.url)
712
+ if (url.searchParams.get('o') === 'MESSAGES') {
713
+ return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: [] })}`)
714
+ }
715
+ return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithEmptyReviewerLists)}`)
716
+ }),
717
+ http.get('*/a/changes/', () => {
718
+ listChangesCalled = true
719
+ return HttpResponse.text(`)]}'\n[]`)
720
+ }),
721
+ http.get('*/a/changes/:changeId/revisions/current/patch', () => {
722
+ return HttpResponse.text(btoa(mockDiff))
723
+ }),
724
+ http.get('*/a/changes/:changeId/revisions/current/comments', () => {
725
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
726
+ }),
727
+ )
728
+
729
+ const mockConfigLayer = createMockConfigLayer()
730
+ const program = showCommand('12345', {}).pipe(
731
+ Effect.provide(GerritApiServiceLive),
732
+ Effect.provide(mockConfigLayer),
733
+ )
734
+
735
+ await Effect.runPromise(program)
736
+
737
+ expect(listChangesCalled).toBe(false)
738
+ })
739
+
614
740
  test('should handle large JSON output without truncation', async () => {
615
741
  // Create a large diff to simulate output > 64KB
616
742
  const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
@@ -132,6 +132,34 @@ describe('submit command', () => {
132
132
  expect(output).toContain('Status: MERGED')
133
133
  })
134
134
 
135
+ it('should fetch change without detailed reviewer options', async () => {
136
+ let requestedOptions: string[] = []
137
+
138
+ server.use(
139
+ http.get('*/a/changes/12345', ({ request }) => {
140
+ const url = new URL(request.url)
141
+ requestedOptions = url.searchParams.getAll('o')
142
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockSubmittableChange)}`)
143
+ }),
144
+ http.post('*/a/changes/12345/submit', () => {
145
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockSubmitResponse)}`)
146
+ }),
147
+ )
148
+
149
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
150
+ const program = submitCommand('12345', {}).pipe(
151
+ Effect.provide(GerritApiServiceLive),
152
+ Effect.provide(mockConfigLayer),
153
+ )
154
+
155
+ await Effect.runPromise(program)
156
+
157
+ expect(requestedOptions).toContain('CURRENT_REVISION')
158
+ expect(requestedOptions).toContain('CURRENT_COMMIT')
159
+ expect(requestedOptions).not.toContain('DETAILED_LABELS')
160
+ expect(requestedOptions).not.toContain('DETAILED_ACCOUNTS')
161
+ })
162
+
135
163
  it('should output XML format when --xml flag is used', async () => {
136
164
  server.use(
137
165
  http.get('*/a/changes/12345', () => {