@aaronshaf/ger 0.2.4 → 0.2.5
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/CLAUDE.md +3 -3
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/cli/commands/build-status.ts +116 -0
- package/src/cli/index.ts +58 -0
- package/tests/abandon.test.ts +178 -111
- package/tests/build-status.test.ts +691 -0
- package/tests/mine.test.ts +130 -163
- package/tests/mocks/fetch-mock.ts +0 -142
- package/tests/setup.ts +0 -13
package/CLAUDE.md
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
### Testing & Coverage
|
|
26
26
|
- **ENFORCE** minimum 80% code coverage
|
|
27
27
|
- **RUN** all tests in pre-commit and pre-push hooks
|
|
28
|
-
- **USE**
|
|
28
|
+
- **USE** MSW (Mock Service Worker) for all HTTP request mocking
|
|
29
29
|
- **REQUIRE** meaningful integration tests for all commands that simulate full workflows
|
|
30
30
|
- **IMPLEMENT** both unit tests and integration tests for every command modification/addition
|
|
31
|
-
- **ENSURE** integration tests use realistic
|
|
31
|
+
- **ENSURE** integration tests use realistic MSW handlers that match Gerrit API responses
|
|
32
32
|
- **EXCLUDE** generated code and tmp/ from coverage
|
|
33
33
|
|
|
34
34
|
### Security
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
### Testing Requirements for Commands
|
|
57
57
|
- **UNIT TESTS**: Test individual functions, schemas, and utilities
|
|
58
58
|
- **INTEGRATION TESTS**: Test complete command flows with mocked HTTP requests
|
|
59
|
-
- **HTTP MOCKING**: Use
|
|
59
|
+
- **HTTP MOCKING**: Use MSW handlers with http.get/http.post patterns for mocking
|
|
60
60
|
- **SCHEMA VALIDATION**: Ensure mocks return data that validates against Effect Schemas
|
|
61
61
|
- **COMMAND COVERAGE**: Every command must have integration tests covering:
|
|
62
62
|
- Happy path execution
|
package/README.md
CHANGED
|
@@ -55,6 +55,10 @@ ger diff 12345
|
|
|
55
55
|
# Extract URLs from messages (e.g., Jenkins build links)
|
|
56
56
|
ger extract-url "build-summary-report" | tail -1
|
|
57
57
|
|
|
58
|
+
# Check CI build status (parses build messages)
|
|
59
|
+
ger build-status 12345 # Returns: pending, running, success, failure, or not_found
|
|
60
|
+
ger build-status # Auto-detects from HEAD commit
|
|
61
|
+
|
|
58
62
|
# AI-powered code review (requires claude, llm, or opencode CLI)
|
|
59
63
|
ger review 12345
|
|
60
64
|
ger review 12345 --dry-run # Preview without posting
|
|
@@ -249,6 +253,51 @@ ger extract-url "github.com" --include-comments
|
|
|
249
253
|
ger extract-url "job/[^/]+/job/[^/]+/\d+/$" --regex
|
|
250
254
|
```
|
|
251
255
|
|
|
256
|
+
### Build Status
|
|
257
|
+
|
|
258
|
+
Check the CI build status of a change by parsing Gerrit messages for build events and verification results:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Check build status for a specific change
|
|
262
|
+
ger build-status 12345
|
|
263
|
+
|
|
264
|
+
# Auto-detect change from HEAD commit
|
|
265
|
+
ger build-status
|
|
266
|
+
|
|
267
|
+
# Use in scripts with jq
|
|
268
|
+
ger build-status | jq -r '.state'
|
|
269
|
+
|
|
270
|
+
# Wait for build completion in CI scripts
|
|
271
|
+
while [ "$(ger build-status | jq -r '.state')" = "running" ]; do
|
|
272
|
+
echo "Waiting for build..."
|
|
273
|
+
sleep 30
|
|
274
|
+
done
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Output format (JSON):
|
|
278
|
+
```json
|
|
279
|
+
{"state": "success"}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Build states:
|
|
283
|
+
- **`pending`**: No "Build Started" message found yet
|
|
284
|
+
- **`running`**: "Build Started" found, but no verification result yet
|
|
285
|
+
- **`success`**: Verified +1 after most recent "Build Started"
|
|
286
|
+
- **`failure`**: Verified -1 after most recent "Build Started"
|
|
287
|
+
- **`not_found`**: Change does not exist
|
|
288
|
+
|
|
289
|
+
#### How it works:
|
|
290
|
+
1. Fetches all messages for the change
|
|
291
|
+
2. Finds the most recent "Build Started" message
|
|
292
|
+
3. Checks for "Verified +1" or "Verified -1" messages after the build started
|
|
293
|
+
4. Returns the appropriate state
|
|
294
|
+
|
|
295
|
+
#### Use cases:
|
|
296
|
+
- **CI/CD integration**: Wait for builds before proceeding with deployment
|
|
297
|
+
- **Automation**: Trigger actions based on build results
|
|
298
|
+
- **Scripting**: Check build status in shell scripts
|
|
299
|
+
- **Monitoring**: Poll build status for long-running builds
|
|
300
|
+
|
|
252
301
|
### Diff
|
|
253
302
|
```bash
|
|
254
303
|
# Full diff
|
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import type { MessageInfo } from '@/schemas/gerrit'
|
|
4
|
+
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
5
|
+
|
|
6
|
+
// Export types for external use
|
|
7
|
+
export type BuildState = 'pending' | 'running' | 'success' | 'failure' | 'not_found'
|
|
8
|
+
|
|
9
|
+
// Effect Schema for BuildStatus (follows project patterns)
|
|
10
|
+
export const BuildStatus: Schema.Schema<{
|
|
11
|
+
readonly state: 'pending' | 'running' | 'success' | 'failure' | 'not_found'
|
|
12
|
+
}> = Schema.Struct({
|
|
13
|
+
state: Schema.Literal('pending', 'running', 'success', 'failure', 'not_found'),
|
|
14
|
+
})
|
|
15
|
+
export type BuildStatus = Schema.Schema.Type<typeof BuildStatus>
|
|
16
|
+
|
|
17
|
+
// Message patterns for precise matching
|
|
18
|
+
const BUILD_STARTED_PATTERN = /Build\s+Started/i
|
|
19
|
+
const VERIFIED_PLUS_PATTERN = /Verified\s*[+]\s*1/
|
|
20
|
+
const VERIFIED_MINUS_PATTERN = /Verified\s*[-]\s*1/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse messages to determine build status based on "Build Started" and verification messages
|
|
24
|
+
*/
|
|
25
|
+
const parseBuildStatus = (messages: readonly MessageInfo[]): BuildStatus => {
|
|
26
|
+
// Empty messages means change exists but has no activity yet - return pending
|
|
27
|
+
if (messages.length === 0) {
|
|
28
|
+
return { state: 'pending' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find the most recent "Build Started" message
|
|
32
|
+
let lastBuildDate: string | null = null
|
|
33
|
+
for (const msg of messages) {
|
|
34
|
+
if (BUILD_STARTED_PATTERN.test(msg.message)) {
|
|
35
|
+
lastBuildDate = msg.date
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If no build has started, state is "pending"
|
|
40
|
+
if (!lastBuildDate) {
|
|
41
|
+
return { state: 'pending' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for verification messages after the build started
|
|
45
|
+
for (const msg of messages) {
|
|
46
|
+
const date = msg.date
|
|
47
|
+
// Gerrit timestamps are ISO 8601 strings (lexicographically sortable)
|
|
48
|
+
if (date <= lastBuildDate) continue
|
|
49
|
+
|
|
50
|
+
if (VERIFIED_PLUS_PATTERN.test(msg.message)) {
|
|
51
|
+
return { state: 'success' }
|
|
52
|
+
} else if (VERIFIED_MINUS_PATTERN.test(msg.message)) {
|
|
53
|
+
return { state: 'failure' }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build started but no verification yet, state is "running"
|
|
58
|
+
return { state: 'running' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get messages for a change
|
|
63
|
+
*/
|
|
64
|
+
const getMessagesForChange = (
|
|
65
|
+
changeId: string,
|
|
66
|
+
): Effect.Effect<readonly MessageInfo[], ApiError, GerritApiService> =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const gerritApi = yield* GerritApiService
|
|
69
|
+
const messages = yield* gerritApi.getMessages(changeId)
|
|
70
|
+
return messages
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build status command - determines build status from Gerrit messages
|
|
75
|
+
*/
|
|
76
|
+
export const buildStatusCommand = (
|
|
77
|
+
changeId: string | undefined,
|
|
78
|
+
): Effect.Effect<void, ApiError | Error | GitError | NoChangeIdError, GerritApiService> =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
// Auto-detect Change-ID from HEAD commit if not provided
|
|
81
|
+
const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
|
|
82
|
+
|
|
83
|
+
// Fetch messages
|
|
84
|
+
const messages = yield* getMessagesForChange(resolvedChangeId)
|
|
85
|
+
|
|
86
|
+
// Parse build status
|
|
87
|
+
const status = parseBuildStatus(messages)
|
|
88
|
+
|
|
89
|
+
// Output JSON to stdout
|
|
90
|
+
const jsonOutput = JSON.stringify(status) + '\n'
|
|
91
|
+
yield* Effect.sync(() => {
|
|
92
|
+
process.stdout.write(jsonOutput)
|
|
93
|
+
})
|
|
94
|
+
}).pipe(
|
|
95
|
+
// Error handling - return not_found for API errors (e.g., change not found)
|
|
96
|
+
Effect.catchAll((error) => {
|
|
97
|
+
if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
|
|
98
|
+
// Change not found - output not_found state and exit successfully
|
|
99
|
+
const status: BuildStatus = { state: 'not_found' }
|
|
100
|
+
return Effect.sync(() => {
|
|
101
|
+
process.stdout.write(JSON.stringify(status) + '\n')
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// For other errors, write to stderr and exit with error
|
|
106
|
+
const errorMessage =
|
|
107
|
+
error instanceof GitError || error instanceof NoChangeIdError || error instanceof Error
|
|
108
|
+
? error.message
|
|
109
|
+
: String(error)
|
|
110
|
+
|
|
111
|
+
return Effect.sync(() => {
|
|
112
|
+
console.error(`Error: ${errorMessage}`)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
})
|
|
115
|
+
}),
|
|
116
|
+
)
|
package/src/cli/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { ConfigServiceLive } from '@/services/config'
|
|
|
31
31
|
import { ReviewStrategyServiceLive } from '@/services/review-strategy'
|
|
32
32
|
import { GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
33
33
|
import { abandonCommand } from './commands/abandon'
|
|
34
|
+
import { buildStatusCommand } from './commands/build-status'
|
|
34
35
|
import { commentCommand } from './commands/comment'
|
|
35
36
|
import { commentsCommand } from './commands/comments'
|
|
36
37
|
import { diffCommand } from './commands/diff'
|
|
@@ -419,6 +420,63 @@ Note: When no change-id is provided, it will be automatically extracted from the
|
|
|
419
420
|
}
|
|
420
421
|
})
|
|
421
422
|
|
|
423
|
+
// build-status command
|
|
424
|
+
program
|
|
425
|
+
.command('build-status [change-id]')
|
|
426
|
+
.description(
|
|
427
|
+
'Check build status from Gerrit messages (auto-detects from HEAD commit if not specified)',
|
|
428
|
+
)
|
|
429
|
+
.addHelpText(
|
|
430
|
+
'after',
|
|
431
|
+
`
|
|
432
|
+
This command parses Gerrit change messages to determine build status.
|
|
433
|
+
It looks for "Build Started" messages and subsequent verification labels.
|
|
434
|
+
|
|
435
|
+
Output is JSON with a "state" field that can be:
|
|
436
|
+
- pending: No build has started yet
|
|
437
|
+
- running: Build started but no verification yet
|
|
438
|
+
- success: Build completed with Verified+1
|
|
439
|
+
- failure: Build completed with Verified-1
|
|
440
|
+
- not_found: Change does not exist
|
|
441
|
+
|
|
442
|
+
Examples:
|
|
443
|
+
# Check build status for specific change (using change number)
|
|
444
|
+
$ ger build-status 392385
|
|
445
|
+
{"state":"success"}
|
|
446
|
+
|
|
447
|
+
# Check build status for specific change (using Change-ID)
|
|
448
|
+
$ ger build-status If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
449
|
+
{"state":"running"}
|
|
450
|
+
|
|
451
|
+
# Auto-detect from HEAD commit
|
|
452
|
+
$ ger build-status
|
|
453
|
+
{"state":"pending"}
|
|
454
|
+
|
|
455
|
+
# Use in scripts (exit code 0 on success, 1 on error)
|
|
456
|
+
$ if ger build-status | jq -e '.state == "success"' > /dev/null; then
|
|
457
|
+
echo "Build passed!"
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
Note: When no change-id is provided, it will be automatically extracted from the
|
|
461
|
+
Change-ID footer in your HEAD commit.`,
|
|
462
|
+
)
|
|
463
|
+
.action(async (changeId) => {
|
|
464
|
+
try {
|
|
465
|
+
const effect = buildStatusCommand(changeId).pipe(
|
|
466
|
+
Effect.provide(GerritApiServiceLive),
|
|
467
|
+
Effect.provide(ConfigServiceLive),
|
|
468
|
+
)
|
|
469
|
+
await Effect.runPromise(effect)
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// Errors are handled within the command itself
|
|
472
|
+
// This catch is just for any unexpected errors
|
|
473
|
+
if (error instanceof Error && error.message !== 'Process exited') {
|
|
474
|
+
console.error('✗ Unexpected error:', error.message)
|
|
475
|
+
process.exit(1)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
|
|
422
480
|
// extract-url command
|
|
423
481
|
program
|
|
424
482
|
.command('extract-url <pattern> [change-id]')
|
package/tests/abandon.test.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } 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 { abandonCommand } from '@/cli/commands/abandon'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
2
9
|
import type { ChangeInfo } from '@/schemas/gerrit'
|
|
3
10
|
|
|
4
11
|
const mockChange: ChangeInfo = {
|
|
@@ -28,136 +35,196 @@ const mockChange: ChangeInfo = {
|
|
|
28
35
|
submittable: false,
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
// Create MSW server
|
|
39
|
+
const server = setupServer(
|
|
40
|
+
// Default handler for auth check
|
|
41
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
42
|
+
const auth = request.headers.get('Authorization')
|
|
43
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
44
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
45
|
+
}
|
|
46
|
+
return HttpResponse.json({
|
|
47
|
+
_account_id: 1000,
|
|
48
|
+
name: 'Test User',
|
|
49
|
+
email: 'test@example.com',
|
|
50
|
+
})
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
|
|
31
54
|
describe('abandon command', () => {
|
|
32
|
-
let
|
|
55
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
56
|
+
let mockConsoleError: ReturnType<typeof mock>
|
|
57
|
+
|
|
58
|
+
beforeAll(() => {
|
|
59
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
server.close()
|
|
64
|
+
})
|
|
33
65
|
|
|
34
66
|
beforeEach(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
mockConsoleLog = mock(() => {})
|
|
68
|
+
mockConsoleError = mock(() => {})
|
|
69
|
+
console.log = mockConsoleLog
|
|
70
|
+
console.error = mockConsoleError
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
server.resetHandlers()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should abandon a change with a message', async () => {
|
|
78
|
+
server.use(
|
|
79
|
+
http.get('*/a/changes/12345', () => {
|
|
80
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
81
|
+
}),
|
|
82
|
+
http.post('*/a/changes/12345/abandon', async ({ request }) => {
|
|
83
|
+
const body = (await request.json()) as { message?: string }
|
|
84
|
+
expect(body.message).toBe('No longer needed')
|
|
85
|
+
return HttpResponse.text(")]}'\n{}")
|
|
41
86
|
}),
|
|
42
87
|
)
|
|
43
|
-
|
|
88
|
+
|
|
89
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
90
|
+
const program = abandonCommand('12345', {
|
|
91
|
+
message: 'No longer needed',
|
|
92
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
93
|
+
|
|
94
|
+
await Effect.runPromise(program)
|
|
95
|
+
|
|
96
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
97
|
+
expect(output).toContain('Abandoned change 12345')
|
|
98
|
+
expect(output).toContain('Test change to abandon')
|
|
99
|
+
expect(output).toContain('Message: No longer needed')
|
|
44
100
|
})
|
|
45
101
|
|
|
46
|
-
it('should
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"subject": "Test change to abandon",
|
|
58
|
-
"status": "NEW",
|
|
59
|
-
"_number": 12345
|
|
60
|
-
}`,
|
|
61
|
-
})
|
|
62
|
-
.mockResolvedValueOnce({
|
|
63
|
-
ok: true,
|
|
64
|
-
text: async () => ')]}\n{}',
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// Note: This is a unit test demonstrating the API calls
|
|
68
|
-
// Actual integration would require running the full command
|
|
69
|
-
// which we avoid to prevent hitting production
|
|
70
|
-
|
|
71
|
-
// Verify the mock setup
|
|
72
|
-
const response = await mockFetch('https://test.gerrit.com/a/changes/12345')
|
|
73
|
-
const text = await response.text()
|
|
74
|
-
expect(text).toContain('Test change to abandon')
|
|
75
|
-
|
|
76
|
-
// Verify abandon endpoint would be called
|
|
77
|
-
const abandonResponse = await mockFetch('https://test.gerrit.com/a/changes/12345/abandon', {
|
|
78
|
-
method: 'POST',
|
|
79
|
-
body: JSON.stringify({ message: 'No longer needed' }),
|
|
80
|
-
})
|
|
81
|
-
expect(abandonResponse.ok).toBe(true)
|
|
102
|
+
it('should abandon a change without a message', async () => {
|
|
103
|
+
server.use(
|
|
104
|
+
http.get('*/a/changes/12345', () => {
|
|
105
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
106
|
+
}),
|
|
107
|
+
http.post('*/a/changes/12345/abandon', async ({ request }) => {
|
|
108
|
+
const body = (await request.json()) as { message?: string }
|
|
109
|
+
expect(body.message).toBeUndefined()
|
|
110
|
+
return HttpResponse.text(")]}'\n{}")
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
82
113
|
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
115
|
+
const program = abandonCommand('12345', {}).pipe(
|
|
116
|
+
Effect.provide(GerritApiServiceLive),
|
|
117
|
+
Effect.provide(mockConfigLayer),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
await Effect.runPromise(program)
|
|
121
|
+
|
|
122
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
123
|
+
expect(output).toContain('Abandoned change 12345')
|
|
124
|
+
expect(output).toContain('Test change to abandon')
|
|
125
|
+
expect(output).not.toContain('Message:')
|
|
85
126
|
})
|
|
86
127
|
|
|
87
|
-
it('should
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
it('should output XML format when --xml flag is used', async () => {
|
|
129
|
+
server.use(
|
|
130
|
+
http.get('*/a/changes/12345', () => {
|
|
131
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
132
|
+
}),
|
|
133
|
+
http.post('*/a/changes/12345/abandon', async ({ request }) => {
|
|
134
|
+
const body = (await request.json()) as { message?: string }
|
|
135
|
+
expect(body.message).toBe('Abandoning for testing')
|
|
136
|
+
return HttpResponse.text(")]}'\n{}")
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
92
139
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
140
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
141
|
+
const program = abandonCommand('12345', {
|
|
142
|
+
xml: true,
|
|
143
|
+
message: 'Abandoning for testing',
|
|
144
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
145
|
+
|
|
146
|
+
await Effect.runPromise(program)
|
|
147
|
+
|
|
148
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
149
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
150
|
+
expect(output).toContain('<abandon_result>')
|
|
151
|
+
expect(output).toContain('<status>success</status>')
|
|
152
|
+
expect(output).toContain('<change_number>12345</change_number>')
|
|
153
|
+
expect(output).toContain('<subject><![CDATA[Test change to abandon]]></subject>')
|
|
154
|
+
expect(output).toContain('<message><![CDATA[Abandoning for testing]]></message>')
|
|
155
|
+
expect(output).toContain('</abandon_result>')
|
|
156
|
+
})
|
|
97
157
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
it('should output XML format without message when no message provided', async () => {
|
|
159
|
+
server.use(
|
|
160
|
+
http.get('*/a/changes/12345', () => {
|
|
161
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
162
|
+
}),
|
|
163
|
+
http.post('*/a/changes/12345/abandon', async () => {
|
|
164
|
+
return HttpResponse.text(")]}'\n{}")
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
169
|
+
const program = abandonCommand('12345', { xml: true }).pipe(
|
|
170
|
+
Effect.provide(GerritApiServiceLive),
|
|
171
|
+
Effect.provide(mockConfigLayer),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await Effect.runPromise(program)
|
|
175
|
+
|
|
176
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
177
|
+
expect(output).toContain('<abandon_result>')
|
|
178
|
+
expect(output).toContain('<status>success</status>')
|
|
179
|
+
expect(output).not.toContain('<message>')
|
|
103
180
|
})
|
|
104
181
|
|
|
105
|
-
it('should handle
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
182
|
+
it('should handle not found errors gracefully', async () => {
|
|
183
|
+
server.use(
|
|
184
|
+
http.get('*/a/changes/99999', () => {
|
|
185
|
+
return HttpResponse.text('Change not found', { status: 404 })
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
111
188
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
189
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
190
|
+
const program = abandonCommand('99999', {
|
|
191
|
+
message: 'Test message',
|
|
192
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
115
193
|
|
|
116
|
-
|
|
117
|
-
expect(
|
|
194
|
+
// Should fail when change is not found
|
|
195
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
118
196
|
})
|
|
119
197
|
|
|
120
|
-
it('should
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
198
|
+
it('should show error when change ID is not provided', async () => {
|
|
199
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
200
|
+
const program = abandonCommand(undefined, {}).pipe(
|
|
201
|
+
Effect.provide(GerritApiServiceLive),
|
|
202
|
+
Effect.provide(mockConfigLayer),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
await Effect.runPromise(program)
|
|
206
|
+
|
|
207
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
208
|
+
expect(errorOutput).toContain('Change ID is required')
|
|
209
|
+
expect(errorOutput).toContain('Usage: ger abandon <change-id>')
|
|
132
210
|
})
|
|
133
211
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
'https://test.gerrit.com/a/changes/?q=owner:self+status:open',
|
|
144
|
-
)
|
|
145
|
-
const text = await response.text()
|
|
146
|
-
expect(text).toContain('Test change to abandon')
|
|
147
|
-
})
|
|
212
|
+
it('should handle abandon API failure', async () => {
|
|
213
|
+
server.use(
|
|
214
|
+
http.get('*/a/changes/12345', () => {
|
|
215
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
216
|
+
}),
|
|
217
|
+
http.post('*/a/changes/12345/abandon', () => {
|
|
218
|
+
return HttpResponse.text('Forbidden', { status: 403 })
|
|
219
|
+
}),
|
|
220
|
+
)
|
|
148
221
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'https://test.gerrit.com/a/changes/?q=owner:self+status:open',
|
|
157
|
-
)
|
|
158
|
-
const text = await response.text()
|
|
159
|
-
const parsed = JSON.parse(text.replace(")]}'\n", ''))
|
|
160
|
-
expect(parsed).toEqual([])
|
|
161
|
-
})
|
|
222
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
223
|
+
const program = abandonCommand('12345', {
|
|
224
|
+
message: 'Test',
|
|
225
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
226
|
+
|
|
227
|
+
// Should throw/fail
|
|
228
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
162
229
|
})
|
|
163
230
|
})
|