@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,334 @@
|
|
|
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 { groupsCommand } from '@/cli/commands/groups'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
9
|
+
|
|
10
|
+
const mockGroups = {
|
|
11
|
+
administrators: {
|
|
12
|
+
id: 'administrators',
|
|
13
|
+
name: 'Administrators',
|
|
14
|
+
description: 'Site administrators with full access',
|
|
15
|
+
owner: 'Administrators',
|
|
16
|
+
owner_id: 'administrators',
|
|
17
|
+
group_id: 1,
|
|
18
|
+
options: { visible_to_all: true },
|
|
19
|
+
created_on: '2024-01-01 10:00:00.000000000',
|
|
20
|
+
},
|
|
21
|
+
'project-reviewers': {
|
|
22
|
+
id: 'project-reviewers',
|
|
23
|
+
name: 'Project Reviewers',
|
|
24
|
+
description: 'Code reviewers for the project',
|
|
25
|
+
owner: 'Project Owners',
|
|
26
|
+
owner_id: 'project-owners',
|
|
27
|
+
group_id: 2,
|
|
28
|
+
options: { visible_to_all: false },
|
|
29
|
+
},
|
|
30
|
+
developers: {
|
|
31
|
+
id: 'developers',
|
|
32
|
+
name: 'Developers',
|
|
33
|
+
description: 'Development team members',
|
|
34
|
+
owner: 'Administrators',
|
|
35
|
+
owner_id: 'administrators',
|
|
36
|
+
group_id: 3,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const server = setupServer(
|
|
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
|
+
|
|
54
|
+
describe('groups command', () => {
|
|
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
|
+
})
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
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 list all groups', async () => {
|
|
78
|
+
server.use(
|
|
79
|
+
http.get('*/a/groups/', () => {
|
|
80
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
85
|
+
const program = groupsCommand({}).pipe(
|
|
86
|
+
Effect.provide(GerritApiServiceLive),
|
|
87
|
+
Effect.provide(mockConfigLayer),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
await Effect.runPromise(program)
|
|
91
|
+
|
|
92
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
93
|
+
expect(output).toContain('Available groups (3)')
|
|
94
|
+
expect(output).toContain('Administrators')
|
|
95
|
+
expect(output).toContain('Site administrators with full access')
|
|
96
|
+
expect(output).toContain('Project Reviewers')
|
|
97
|
+
expect(output).toContain('Developers')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should list groups with pattern filter', async () => {
|
|
101
|
+
server.use(
|
|
102
|
+
http.get('*/a/groups/', ({ request }) => {
|
|
103
|
+
const url = new URL(request.url)
|
|
104
|
+
const pattern = url.searchParams.get('r')
|
|
105
|
+
expect(pattern).toBe('project-.*')
|
|
106
|
+
|
|
107
|
+
const filtered = {
|
|
108
|
+
'project-reviewers': mockGroups['project-reviewers'],
|
|
109
|
+
}
|
|
110
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(filtered)}`)
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
115
|
+
const program = groupsCommand({ pattern: 'project-.*' }).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('Project Reviewers')
|
|
124
|
+
expect(output).not.toContain('Administrators')
|
|
125
|
+
expect(output).not.toContain('Developers')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should list owned groups only', async () => {
|
|
129
|
+
server.use(
|
|
130
|
+
http.get('*/a/groups/', ({ request }) => {
|
|
131
|
+
const url = new URL(request.url)
|
|
132
|
+
expect(url.searchParams.has('owned')).toBe(true)
|
|
133
|
+
|
|
134
|
+
const filtered = {
|
|
135
|
+
administrators: mockGroups['administrators'],
|
|
136
|
+
}
|
|
137
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(filtered)}`)
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
142
|
+
const program = groupsCommand({ owned: true }).pipe(
|
|
143
|
+
Effect.provide(GerritApiServiceLive),
|
|
144
|
+
Effect.provide(mockConfigLayer),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
await Effect.runPromise(program)
|
|
148
|
+
|
|
149
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
150
|
+
expect(output).toContain('Administrators')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should list groups for specific project', async () => {
|
|
154
|
+
server.use(
|
|
155
|
+
http.get('*/a/groups/', ({ request }) => {
|
|
156
|
+
const url = new URL(request.url)
|
|
157
|
+
const project = url.searchParams.get('p')
|
|
158
|
+
expect(project).toBe('my-project')
|
|
159
|
+
|
|
160
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
|
|
161
|
+
}),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
165
|
+
const program = groupsCommand({ project: 'my-project' }).pipe(
|
|
166
|
+
Effect.provide(GerritApiServiceLive),
|
|
167
|
+
Effect.provide(mockConfigLayer),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
await Effect.runPromise(program)
|
|
171
|
+
|
|
172
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
173
|
+
expect(output).toContain('Available groups')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should output XML format when --xml flag is used', async () => {
|
|
177
|
+
server.use(
|
|
178
|
+
http.get('*/a/groups/', () => {
|
|
179
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
184
|
+
const program = groupsCommand({ xml: true }).pipe(
|
|
185
|
+
Effect.provide(GerritApiServiceLive),
|
|
186
|
+
Effect.provide(mockConfigLayer),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
await Effect.runPromise(program)
|
|
190
|
+
|
|
191
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
192
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
193
|
+
expect(output).toContain('<groups_result>')
|
|
194
|
+
expect(output).toContain('<status>success</status>')
|
|
195
|
+
expect(output).toContain('<count>3</count>')
|
|
196
|
+
expect(output).toContain('<groups>')
|
|
197
|
+
expect(output).toContain('<group>')
|
|
198
|
+
expect(output).toContain('<id><![CDATA[administrators]]></id>')
|
|
199
|
+
expect(output).toContain('<name><![CDATA[Administrators]]></name>')
|
|
200
|
+
expect(output).toContain(
|
|
201
|
+
'<description><![CDATA[Site administrators with full access]]></description>',
|
|
202
|
+
)
|
|
203
|
+
expect(output).toContain('<owner><![CDATA[Administrators]]></owner>')
|
|
204
|
+
expect(output).toContain('<visible_to_all>true</visible_to_all>')
|
|
205
|
+
expect(output).toContain('</group>')
|
|
206
|
+
expect(output).toContain('</groups>')
|
|
207
|
+
expect(output).toContain('</groups_result>')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should handle empty results', async () => {
|
|
211
|
+
server.use(
|
|
212
|
+
http.get('*/a/groups/', () => {
|
|
213
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({})}`)
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
218
|
+
const program = groupsCommand({}).pipe(
|
|
219
|
+
Effect.provide(GerritApiServiceLive),
|
|
220
|
+
Effect.provide(mockConfigLayer),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
await Effect.runPromise(program)
|
|
224
|
+
|
|
225
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
226
|
+
expect(output).toContain('No groups found')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should handle empty results with XML format', async () => {
|
|
230
|
+
server.use(
|
|
231
|
+
http.get('*/a/groups/', () => {
|
|
232
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({})}`)
|
|
233
|
+
}),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
237
|
+
const program = groupsCommand({ xml: true }).pipe(
|
|
238
|
+
Effect.provide(GerritApiServiceLive),
|
|
239
|
+
Effect.provide(mockConfigLayer),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
await Effect.runPromise(program)
|
|
243
|
+
|
|
244
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
245
|
+
expect(output).toContain('<groups_result>')
|
|
246
|
+
expect(output).toContain('<status>success</status>')
|
|
247
|
+
expect(output).toContain('<count>0</count>')
|
|
248
|
+
expect(output).toContain('<groups />')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should handle network errors gracefully', async () => {
|
|
252
|
+
server.use(
|
|
253
|
+
http.get('*/a/groups/', () => {
|
|
254
|
+
return HttpResponse.error()
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
259
|
+
const program = groupsCommand({}).pipe(
|
|
260
|
+
Effect.provide(GerritApiServiceLive),
|
|
261
|
+
Effect.provide(mockConfigLayer),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should handle API errors (403)', async () => {
|
|
268
|
+
server.use(
|
|
269
|
+
http.get('*/a/groups/', () => {
|
|
270
|
+
return HttpResponse.text('Forbidden', { status: 403 })
|
|
271
|
+
}),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
275
|
+
const program = groupsCommand({}).pipe(
|
|
276
|
+
Effect.provide(GerritApiServiceLive),
|
|
277
|
+
Effect.provide(mockConfigLayer),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should handle groups without optional fields', async () => {
|
|
284
|
+
const minimalGroups = {
|
|
285
|
+
'minimal-group': {
|
|
286
|
+
id: 'minimal-group',
|
|
287
|
+
name: 'Minimal Group',
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
server.use(
|
|
292
|
+
http.get('*/a/groups/', () => {
|
|
293
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(minimalGroups)}`)
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
298
|
+
const program = groupsCommand({ xml: true }).pipe(
|
|
299
|
+
Effect.provide(GerritApiServiceLive),
|
|
300
|
+
Effect.provide(mockConfigLayer),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
await Effect.runPromise(program)
|
|
304
|
+
|
|
305
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
306
|
+
expect(output).toContain('<id><![CDATA[minimal-group]]></id>')
|
|
307
|
+
expect(output).toContain('<name><![CDATA[Minimal Group]]></name>')
|
|
308
|
+
expect(output).not.toContain('<description>')
|
|
309
|
+
expect(output).not.toContain('<owner>')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('should respect limit parameter', async () => {
|
|
313
|
+
server.use(
|
|
314
|
+
http.get('*/a/groups/', ({ request }) => {
|
|
315
|
+
const url = new URL(request.url)
|
|
316
|
+
const limit = url.searchParams.get('n')
|
|
317
|
+
expect(limit).toBe('10')
|
|
318
|
+
|
|
319
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
|
|
320
|
+
}),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
324
|
+
const program = groupsCommand({ limit: '10' }).pipe(
|
|
325
|
+
Effect.provide(GerritApiServiceLive),
|
|
326
|
+
Effect.provide(mockConfigLayer),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
await Effect.runPromise(program)
|
|
330
|
+
|
|
331
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
332
|
+
expect(output).toContain('Available groups')
|
|
333
|
+
})
|
|
334
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Mock } from 'bun:test'
|
|
2
|
+
import { mock } from 'bun:test'
|
|
3
|
+
import type { SetupServer } from 'msw/node'
|
|
4
|
+
import { setupServer } from 'msw/node'
|
|
5
|
+
import { http, HttpResponse } from 'msw'
|
|
6
|
+
import { Layer } from 'effect'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './config-mock'
|
|
9
|
+
|
|
10
|
+
export const server: SetupServer = setupServer(
|
|
11
|
+
// Default handler for auth check
|
|
12
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
13
|
+
const auth = request.headers.get('Authorization')
|
|
14
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
15
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
16
|
+
}
|
|
17
|
+
return HttpResponse.json({
|
|
18
|
+
_account_id: 1000,
|
|
19
|
+
name: 'Test User',
|
|
20
|
+
email: 'test@example.com',
|
|
21
|
+
})
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Store captured output
|
|
26
|
+
export let capturedStdout: string[] = []
|
|
27
|
+
export let capturedErrors: string[] = []
|
|
28
|
+
|
|
29
|
+
// Mock process.stdout.write to capture JSON output
|
|
30
|
+
export const mockStdoutWrite: Mock<(chunk: unknown) => boolean> = mock(
|
|
31
|
+
(chunk: unknown): boolean => {
|
|
32
|
+
capturedStdout.push(String(chunk))
|
|
33
|
+
return true
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Mock console.error to capture errors
|
|
38
|
+
export const mockConsoleError: Mock<(...args: unknown[]) => void> = mock(
|
|
39
|
+
(...args: unknown[]): void => {
|
|
40
|
+
capturedErrors.push(args.join(' '))
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// Mock process.exit to prevent test termination
|
|
45
|
+
export const mockProcessExit: Mock<(code?: number) => never> = mock((_code?: number): never => {
|
|
46
|
+
throw new Error('Process exited')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Store original methods
|
|
50
|
+
export const originalStdoutWrite: typeof process.stdout.write = process.stdout.write
|
|
51
|
+
export const originalConsoleError: typeof console.error = console.error
|
|
52
|
+
export const originalProcessExit: typeof process.exit = process.exit
|
|
53
|
+
|
|
54
|
+
export const setupBuildStatusTests = (): void => {
|
|
55
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
56
|
+
// @ts-ignore - Mocking stdout
|
|
57
|
+
process.stdout.write = mockStdoutWrite
|
|
58
|
+
// @ts-ignore - Mocking console
|
|
59
|
+
console.error = mockConsoleError
|
|
60
|
+
// @ts-ignore - Mocking process.exit
|
|
61
|
+
process.exit = mockProcessExit
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const teardownBuildStatusTests = (): void => {
|
|
65
|
+
server.close()
|
|
66
|
+
// @ts-ignore - Restoring stdout
|
|
67
|
+
process.stdout.write = originalStdoutWrite
|
|
68
|
+
console.error = originalConsoleError
|
|
69
|
+
// @ts-ignore - Restoring process.exit
|
|
70
|
+
process.exit = originalProcessExit
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const resetBuildStatusMocks = (): void => {
|
|
74
|
+
server.resetHandlers()
|
|
75
|
+
mockStdoutWrite.mockClear()
|
|
76
|
+
mockConsoleError.mockClear()
|
|
77
|
+
mockProcessExit.mockClear()
|
|
78
|
+
capturedStdout = []
|
|
79
|
+
capturedErrors = []
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const createMockConfigLayer = (): Layer.Layer<ConfigService> =>
|
|
83
|
+
Layer.succeed(ConfigService, createMockConfigService())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import type { ConfigServiceImpl } from '@/services/config'
|
|
3
|
+
import type { GerritCredentials } from '@/schemas/gerrit'
|
|
4
|
+
import type { AiConfig, AppConfig } from '@/schemas/config'
|
|
5
|
+
|
|
6
|
+
export const createMockConfigService = (
|
|
7
|
+
credentials: GerritCredentials = {
|
|
8
|
+
host: 'https://test.gerrit.com',
|
|
9
|
+
username: 'testuser',
|
|
10
|
+
password: 'testpass',
|
|
11
|
+
},
|
|
12
|
+
aiConfig: AiConfig = { autoDetect: true },
|
|
13
|
+
): ConfigServiceImpl => ({
|
|
14
|
+
getCredentials: Effect.succeed(credentials),
|
|
15
|
+
saveCredentials: () => Effect.succeed(undefined as void),
|
|
16
|
+
deleteCredentials: Effect.succeed(undefined as void),
|
|
17
|
+
getAiConfig: Effect.succeed(aiConfig),
|
|
18
|
+
saveAiConfig: () => Effect.succeed(undefined as void),
|
|
19
|
+
getFullConfig: Effect.succeed({
|
|
20
|
+
host: credentials.host,
|
|
21
|
+
username: credentials.username,
|
|
22
|
+
password: credentials.password,
|
|
23
|
+
aiTool: aiConfig.tool,
|
|
24
|
+
aiAutoDetect: aiConfig.autoDetect ?? true,
|
|
25
|
+
} as AppConfig),
|
|
26
|
+
saveFullConfig: () => Effect.succeed(undefined as void),
|
|
27
|
+
})
|