@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,349 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, mock } from 'bun:test'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
|
|
4
|
+
// Create testable versions of the strategies by injecting dependencies
|
|
5
|
+
interface MockDeps {
|
|
6
|
+
execAsync: (cmd: string) => Promise<{ stdout: string; stderr: string }>
|
|
7
|
+
spawn: (command: string, options: any) => any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Test implementation that mirrors the real strategy structure
|
|
11
|
+
const createTestStrategy = (name: string, command: string, flags: string[], deps: MockDeps) => ({
|
|
12
|
+
name,
|
|
13
|
+
isAvailable: () =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
try {
|
|
16
|
+
const result = yield* Effect.tryPromise({
|
|
17
|
+
try: () => deps.execAsync(`which ${command.split(' ')[0]}`),
|
|
18
|
+
catch: () => null,
|
|
19
|
+
}).pipe(Effect.orElseSucceed(() => null))
|
|
20
|
+
|
|
21
|
+
return Boolean(result && result.stdout.trim())
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
executeReview: (prompt: string, options: { cwd?: string } = {}) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
const result = yield* Effect.tryPromise({
|
|
29
|
+
try: async () => {
|
|
30
|
+
const child = deps.spawn(`${command} ${flags.join(' ')}`, {
|
|
31
|
+
shell: true,
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
cwd: options.cwd || process.cwd(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
child.stdin.write(prompt)
|
|
37
|
+
child.stdin.end()
|
|
38
|
+
|
|
39
|
+
let stdout = ''
|
|
40
|
+
let stderr = ''
|
|
41
|
+
|
|
42
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
43
|
+
stdout += data.toString()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
child.stderr.on('data', (data: Buffer) => {
|
|
47
|
+
stderr += data.toString()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
|
51
|
+
child.on('close', (code: number) => {
|
|
52
|
+
if (code !== 0) {
|
|
53
|
+
reject(new Error(`${name} exited with code ${code}: ${stderr}`))
|
|
54
|
+
} else {
|
|
55
|
+
resolve({ stdout, stderr })
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
child.on('error', reject)
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
catch: (error) =>
|
|
63
|
+
new Error(`${name} failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Extract response from <response> tags or use full output
|
|
67
|
+
const responseMatch = result.stdout.match(/<response>([\s\S]*?)<\/response>/i)
|
|
68
|
+
return responseMatch ? responseMatch[1].trim() : result.stdout.trim()
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('Review Strategy', () => {
|
|
73
|
+
let mockExecAsync: any
|
|
74
|
+
let mockSpawn: any
|
|
75
|
+
let mockChildProcess: any
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
mockChildProcess = {
|
|
79
|
+
stdin: {
|
|
80
|
+
write: mock(() => {}),
|
|
81
|
+
end: mock(() => {}),
|
|
82
|
+
},
|
|
83
|
+
stdout: {
|
|
84
|
+
on: mock(() => {}),
|
|
85
|
+
},
|
|
86
|
+
stderr: {
|
|
87
|
+
on: mock(() => {}),
|
|
88
|
+
},
|
|
89
|
+
on: mock(() => {}),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
mockExecAsync = mock()
|
|
93
|
+
mockSpawn = mock(() => mockChildProcess)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const setupSuccessfulExecution = (output = 'AI response') => {
|
|
97
|
+
mockChildProcess.stdout.on.mockImplementation((event: string, callback: Function) => {
|
|
98
|
+
if (event === 'data') {
|
|
99
|
+
process.nextTick(() => callback(Buffer.from(output)))
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
mockChildProcess.stderr.on.mockImplementation((_event: string, _callback: Function) => {
|
|
104
|
+
// No stderr for success
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
mockChildProcess.on.mockImplementation((event: string, callback: Function) => {
|
|
108
|
+
if (event === 'close') {
|
|
109
|
+
process.nextTick(() => callback(0))
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const setupFailedExecution = (exitCode = 1, stderr = 'Command failed') => {
|
|
115
|
+
mockChildProcess.stdout.on.mockImplementation((_event: string, _callback: Function) => {
|
|
116
|
+
// No stdout for failure
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
mockChildProcess.stderr.on.mockImplementation((event: string, callback: Function) => {
|
|
120
|
+
if (event === 'data') {
|
|
121
|
+
process.nextTick(() => callback(Buffer.from(stderr)))
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
mockChildProcess.on.mockImplementation((event: string, callback: Function) => {
|
|
126
|
+
if (event === 'close') {
|
|
127
|
+
process.nextTick(() => callback(exitCode))
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe('Claude CLI Strategy', () => {
|
|
133
|
+
let claudeStrategy: any
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
claudeStrategy = createTestStrategy('Claude CLI', 'claude', ['-p'], {
|
|
137
|
+
execAsync: mockExecAsync,
|
|
138
|
+
spawn: mockSpawn,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should check availability when claude is installed', async () => {
|
|
143
|
+
mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/claude', stderr: '' })
|
|
144
|
+
|
|
145
|
+
const available = await Effect.runPromise(claudeStrategy.isAvailable())
|
|
146
|
+
|
|
147
|
+
expect(available).toBe(true)
|
|
148
|
+
expect(mockExecAsync).toHaveBeenCalledWith('which claude')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should check availability when claude is not installed', async () => {
|
|
152
|
+
mockExecAsync.mockRejectedValueOnce(new Error('Command not found'))
|
|
153
|
+
|
|
154
|
+
const available = await Effect.runPromise(claudeStrategy.isAvailable())
|
|
155
|
+
|
|
156
|
+
expect(available).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should execute review successfully', async () => {
|
|
160
|
+
setupSuccessfulExecution('Claude AI response')
|
|
161
|
+
|
|
162
|
+
const response = await Effect.runPromise(
|
|
163
|
+
claudeStrategy.executeReview('Test prompt', { cwd: '/tmp' }),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
expect(response).toBe('Claude AI response')
|
|
167
|
+
expect(mockSpawn).toHaveBeenCalledWith('claude -p', {
|
|
168
|
+
shell: true,
|
|
169
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
170
|
+
cwd: '/tmp',
|
|
171
|
+
})
|
|
172
|
+
expect(mockChildProcess.stdin.write).toHaveBeenCalledWith('Test prompt')
|
|
173
|
+
expect(mockChildProcess.stdin.end).toHaveBeenCalled()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should extract response from tags', async () => {
|
|
177
|
+
setupSuccessfulExecution('<response>Tagged content</response>')
|
|
178
|
+
|
|
179
|
+
const response = await Effect.runPromise(claudeStrategy.executeReview('Test prompt'))
|
|
180
|
+
|
|
181
|
+
expect(response).toBe('Tagged content')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should handle command failures', async () => {
|
|
185
|
+
setupFailedExecution(1, 'Claude CLI error')
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await Effect.runPromise(claudeStrategy.executeReview('Test prompt'))
|
|
189
|
+
expect(false).toBe(true) // Should not reach here
|
|
190
|
+
} catch (error: any) {
|
|
191
|
+
expect(error.message).toContain('Claude CLI failed')
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('Gemini CLI Strategy', () => {
|
|
197
|
+
let geminiStrategy: any
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
geminiStrategy = createTestStrategy('Gemini CLI', 'gemini', ['-p'], {
|
|
201
|
+
execAsync: mockExecAsync,
|
|
202
|
+
spawn: mockSpawn,
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should check availability', async () => {
|
|
207
|
+
mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/gemini', stderr: '' })
|
|
208
|
+
|
|
209
|
+
const available = await Effect.runPromise(geminiStrategy.isAvailable())
|
|
210
|
+
|
|
211
|
+
expect(available).toBe(true)
|
|
212
|
+
expect(mockExecAsync).toHaveBeenCalledWith('which gemini')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should use -p flag', async () => {
|
|
216
|
+
setupSuccessfulExecution('Gemini response')
|
|
217
|
+
|
|
218
|
+
const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
|
|
219
|
+
|
|
220
|
+
expect(response).toBe('Gemini response')
|
|
221
|
+
expect(mockSpawn).toHaveBeenCalledWith('gemini -p', expect.any(Object))
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should extract response from tags', async () => {
|
|
225
|
+
setupSuccessfulExecution('<response>Gemini tagged content</response>')
|
|
226
|
+
|
|
227
|
+
const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
|
|
228
|
+
|
|
229
|
+
expect(response).toBe('Gemini tagged content')
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('OpenCode CLI Strategy', () => {
|
|
234
|
+
let opencodeStrategy: any
|
|
235
|
+
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
opencodeStrategy = createTestStrategy('OpenCode CLI', 'opencode', ['-p'], {
|
|
238
|
+
execAsync: mockExecAsync,
|
|
239
|
+
spawn: mockSpawn,
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should check availability', async () => {
|
|
244
|
+
mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/opencode', stderr: '' })
|
|
245
|
+
|
|
246
|
+
const available = await Effect.runPromise(opencodeStrategy.isAvailable())
|
|
247
|
+
|
|
248
|
+
expect(available).toBe(true)
|
|
249
|
+
expect(mockExecAsync).toHaveBeenCalledWith('which opencode')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should use -p flag', async () => {
|
|
253
|
+
setupSuccessfulExecution('OpenCode response')
|
|
254
|
+
|
|
255
|
+
const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
|
|
256
|
+
|
|
257
|
+
expect(response).toBe('OpenCode response')
|
|
258
|
+
expect(mockSpawn).toHaveBeenCalledWith('opencode -p', expect.any(Object))
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should extract response from tags', async () => {
|
|
262
|
+
setupSuccessfulExecution('<response>OpenCode tagged content</response>')
|
|
263
|
+
|
|
264
|
+
const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
|
|
265
|
+
|
|
266
|
+
expect(response).toBe('OpenCode tagged content')
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('Integration with actual service patterns', () => {
|
|
271
|
+
it('should demonstrate proper Effect patterns', async () => {
|
|
272
|
+
const mockStrategy = createTestStrategy('Mock CLI', 'mock', [], {
|
|
273
|
+
execAsync: mockExecAsync,
|
|
274
|
+
spawn: mockSpawn,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
setupSuccessfulExecution('Integration test response')
|
|
278
|
+
|
|
279
|
+
// Test using Effect.gen patterns like the real service
|
|
280
|
+
const result = await Effect.runPromise(
|
|
281
|
+
Effect.gen(function* () {
|
|
282
|
+
// Test availability check
|
|
283
|
+
mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/mock', stderr: '' })
|
|
284
|
+
const available = yield* mockStrategy.isAvailable()
|
|
285
|
+
|
|
286
|
+
if (!available) {
|
|
287
|
+
return yield* Effect.fail(new Error('Strategy not available'))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Test execution
|
|
291
|
+
const response = yield* mockStrategy.executeReview('Test prompt', { cwd: '/tmp' })
|
|
292
|
+
return response
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
expect(result).toBe('Integration test response')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should handle error propagation correctly', async () => {
|
|
300
|
+
const mockStrategy = createTestStrategy('Failing CLI', 'failing', [], {
|
|
301
|
+
execAsync: mockExecAsync,
|
|
302
|
+
spawn: mockSpawn,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
setupFailedExecution(1, 'Mock failure')
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await Effect.runPromise(
|
|
309
|
+
Effect.gen(function* () {
|
|
310
|
+
return yield* mockStrategy.executeReview('Test prompt')
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
expect(false).toBe(true) // Should not reach here
|
|
314
|
+
} catch (error: any) {
|
|
315
|
+
expect(error.message).toContain('Failing CLI failed')
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should test multiple strategy selection logic', async () => {
|
|
320
|
+
const strategies = [
|
|
321
|
+
createTestStrategy('Strategy A', 'a', [], { execAsync: mockExecAsync, spawn: mockSpawn }),
|
|
322
|
+
createTestStrategy('Strategy B', 'b', [], { execAsync: mockExecAsync, spawn: mockSpawn }),
|
|
323
|
+
createTestStrategy('Strategy C', 'c', [], { execAsync: mockExecAsync, spawn: mockSpawn }),
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
// Mock availability checks: A fails, B succeeds, C succeeds
|
|
327
|
+
mockExecAsync
|
|
328
|
+
.mockRejectedValueOnce(new Error('Command not found')) // A not available
|
|
329
|
+
.mockResolvedValueOnce({ stdout: '/usr/local/bin/b', stderr: '' }) // B available
|
|
330
|
+
.mockResolvedValueOnce({ stdout: '/usr/local/bin/c', stderr: '' }) // C available
|
|
331
|
+
|
|
332
|
+
const available = await Effect.runPromise(
|
|
333
|
+
Effect.gen(function* () {
|
|
334
|
+
const availableStrategies = []
|
|
335
|
+
for (const strategy of strategies) {
|
|
336
|
+
const isAvailable = yield* strategy.isAvailable()
|
|
337
|
+
if (isAvailable) {
|
|
338
|
+
availableStrategies.push(strategy)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return availableStrategies
|
|
342
|
+
}),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
expect(available.length).toBe(2)
|
|
346
|
+
expect(available.map((s) => s.name)).toEqual(['Strategy B', 'Strategy C'])
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
generateMockAccount,
|
|
4
|
+
generateMockChange,
|
|
5
|
+
generateMockFileDiff,
|
|
6
|
+
generateMockFiles,
|
|
7
|
+
} from '@/test-utils/mock-generator'
|
|
8
|
+
|
|
9
|
+
describe('Mock Generator', () => {
|
|
10
|
+
describe('generateMockChange', () => {
|
|
11
|
+
test('should generate a complete mock change object', () => {
|
|
12
|
+
const change = generateMockChange()
|
|
13
|
+
|
|
14
|
+
expect(change).toMatchObject({
|
|
15
|
+
id: 'myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940',
|
|
16
|
+
project: 'myProject',
|
|
17
|
+
branch: 'master',
|
|
18
|
+
change_id: 'I8473b95934b5732ac55d26311a706c9c2bde9940',
|
|
19
|
+
subject: 'Implementing new feature',
|
|
20
|
+
status: 'NEW',
|
|
21
|
+
created: '2023-12-01 10:00:00.000000000',
|
|
22
|
+
updated: '2023-12-01 15:30:00.000000000',
|
|
23
|
+
insertions: 25,
|
|
24
|
+
deletions: 3,
|
|
25
|
+
_number: 12345,
|
|
26
|
+
owner: {
|
|
27
|
+
_account_id: 1000096,
|
|
28
|
+
name: 'John Developer',
|
|
29
|
+
email: 'john@example.com',
|
|
30
|
+
username: 'jdeveloper',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('should apply overrides to mock change', () => {
|
|
36
|
+
const overrides = {
|
|
37
|
+
subject: 'Custom subject',
|
|
38
|
+
status: 'MERGED' as const,
|
|
39
|
+
insertions: 100,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const change = generateMockChange(overrides)
|
|
43
|
+
|
|
44
|
+
expect(change.subject).toBe('Custom subject')
|
|
45
|
+
expect(change.status).toBe('MERGED')
|
|
46
|
+
expect(change.insertions).toBe(100)
|
|
47
|
+
// Original values should remain for non-overridden fields
|
|
48
|
+
expect(change.project).toBe('myProject')
|
|
49
|
+
expect(change.deletions).toBe(3)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('should handle partial owner overrides', () => {
|
|
53
|
+
const overrides = {
|
|
54
|
+
owner: {
|
|
55
|
+
_account_id: 999,
|
|
56
|
+
name: 'Custom Developer',
|
|
57
|
+
email: 'custom@example.com',
|
|
58
|
+
username: 'customdev',
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const change = generateMockChange(overrides)
|
|
63
|
+
|
|
64
|
+
expect(change.owner).toEqual(overrides.owner)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('generateMockFiles', () => {
|
|
69
|
+
test('should generate mock file info objects', () => {
|
|
70
|
+
const files = generateMockFiles()
|
|
71
|
+
|
|
72
|
+
expect(Object.keys(files)).toContain('src/main.ts')
|
|
73
|
+
expect(Object.keys(files)).toContain('tests/main.test.ts')
|
|
74
|
+
|
|
75
|
+
expect(files['src/main.ts']).toMatchObject({
|
|
76
|
+
status: 'M',
|
|
77
|
+
lines_inserted: 15,
|
|
78
|
+
lines_deleted: 3,
|
|
79
|
+
size_delta: 120,
|
|
80
|
+
size: 1200,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(files['tests/main.test.ts']).toMatchObject({
|
|
84
|
+
status: 'A',
|
|
85
|
+
lines_inserted: 45,
|
|
86
|
+
lines_deleted: 0,
|
|
87
|
+
size_delta: 450,
|
|
88
|
+
size: 450,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('should return consistent file structure', () => {
|
|
93
|
+
const files1 = generateMockFiles()
|
|
94
|
+
const files2 = generateMockFiles()
|
|
95
|
+
|
|
96
|
+
expect(Object.keys(files1)).toEqual(Object.keys(files2))
|
|
97
|
+
expect(files1['src/main.ts']).toEqual(files2['src/main.ts'])
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('generateMockFileDiff', () => {
|
|
102
|
+
test('should generate mock file diff content', () => {
|
|
103
|
+
const diff = generateMockFileDiff()
|
|
104
|
+
|
|
105
|
+
expect(diff).toMatchObject({
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
ab: ['function main() {', ' console.log("Hello, world!")'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
a: [' return 0'],
|
|
112
|
+
b: [' return process.exit(0)'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
ab: ['}'],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
change_type: 'MODIFIED',
|
|
119
|
+
diff_header: ['--- a/src/main.ts', '+++ b/src/main.ts'],
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('should have consistent structure', () => {
|
|
124
|
+
const diff1 = generateMockFileDiff()
|
|
125
|
+
const diff2 = generateMockFileDiff()
|
|
126
|
+
|
|
127
|
+
expect(diff1.change_type).toBe('MODIFIED')
|
|
128
|
+
expect(diff2.change_type).toBe('MODIFIED')
|
|
129
|
+
expect(diff1.content.length).toBe(diff2.content.length)
|
|
130
|
+
expect(diff1.diff_header).toEqual(['--- a/src/main.ts', '+++ b/src/main.ts'])
|
|
131
|
+
expect(diff2.diff_header).toEqual(['--- a/src/main.ts', '+++ b/src/main.ts'])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('generateMockAccount', () => {
|
|
136
|
+
test('should generate mock account object', () => {
|
|
137
|
+
const account = generateMockAccount()
|
|
138
|
+
|
|
139
|
+
expect(account).toMatchObject({
|
|
140
|
+
_account_id: 1000096,
|
|
141
|
+
name: 'Test User',
|
|
142
|
+
email: 'test@example.com',
|
|
143
|
+
username: 'testuser',
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('should return consistent account data', () => {
|
|
148
|
+
const account1 = generateMockAccount()
|
|
149
|
+
const account2 = generateMockAccount()
|
|
150
|
+
|
|
151
|
+
expect(account1).toEqual(account2)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|