@aaronshaf/ger 1.2.10 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +83 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +105 -0
- package/DEVELOPMENT.md +361 -0
- package/EXAMPLES.md +457 -0
- package/README.md +831 -16
- package/bin/ger +3 -18
- package/biome.json +36 -0
- package/bun.lock +678 -0
- package/bunfig.toml +8 -0
- package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
- package/docs/adr/0002-use-bun-runtime.md +64 -0
- package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
- package/docs/adr/0004-use-commander-for-cli.md +76 -0
- package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
- package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
- package/docs/adr/0007-git-hooks-for-quality.md +94 -0
- package/docs/adr/0008-no-as-typecasting.md +83 -0
- package/docs/adr/0009-file-size-limits.md +82 -0
- package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
- package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
- package/docs/adr/0012-build-status-message-parsing.md +94 -0
- package/docs/adr/0013-git-subprocess-integration.md +98 -0
- package/docs/adr/0014-group-management-support.md +95 -0
- package/docs/adr/0015-batch-comment-processing.md +111 -0
- package/docs/adr/0016-flexible-change-identifiers.md +94 -0
- package/docs/adr/0017-git-worktree-support.md +102 -0
- package/docs/adr/0018-auto-install-commit-hook.md +103 -0
- package/docs/adr/0019-sdk-package-exports.md +95 -0
- package/docs/adr/0020-code-coverage-enforcement.md +105 -0
- package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
- package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
- package/docs/adr/README.md +30 -0
- package/docs/prd/README.md +12 -0
- package/docs/prd/architecture.md +325 -0
- package/docs/prd/commands.md +425 -0
- package/docs/prd/data-model.md +349 -0
- package/docs/prd/overview.md +124 -0
- package/index.ts +219 -0
- package/oxlint.json +24 -0
- package/package.json +82 -15
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/skills/gerrit-workflow/SKILL.md +247 -0
- package/skills/gerrit-workflow/examples.md +572 -0
- package/skills/gerrit-workflow/reference.md +728 -0
- package/src/api/gerrit.ts +696 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/add-reviewer.ts +156 -0
- package/src/cli/commands/build-status.ts +282 -0
- package/src/cli/commands/checkout.ts +422 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/groups-members.ts +104 -0
- package/src/cli/commands/groups-show.ts +169 -0
- package/src/cli/commands/groups.ts +137 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/projects.ts +68 -0
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/rebase.ts +52 -0
- package/src/cli/commands/remove-reviewer.ts +123 -0
- package/src/cli/commands/restore.ts +50 -0
- package/src/cli/commands/review.ts +486 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/setup.ts +286 -0
- package/src/cli/commands/show.ts +491 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/submit.ts +108 -0
- package/src/cli/commands/vote.ts +119 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +53 -0
- package/src/cli/register-commands.ts +659 -0
- package/src/cli/register-group-commands.ts +88 -0
- package/src/cli/register-reviewer-commands.ts +97 -0
- package/src/prompts/default-review.md +86 -0
- package/src/prompts/system-inline-review.md +135 -0
- package/src/prompts/system-overall-review.md +206 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +84 -0
- package/src/schemas/gerrit.ts +681 -0
- package/src/services/commit-hook.ts +314 -0
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +250 -0
- package/src/services/git-worktree.ts +342 -0
- package/src/services/review-strategy.ts +292 -0
- package/src/test-utils/mock-generator.ts +138 -0
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +110 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +271 -0
- package/src/utils/url-parser.ts +118 -0
- package/tests/abandon.test.ts +230 -0
- package/tests/add-reviewer.test.ts +579 -0
- package/tests/build-status-watch.test.ts +344 -0
- package/tests/build-status.test.ts +789 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/checkout/integration.test.ts +653 -0
- package/tests/checkout/parse-input.test.ts +55 -0
- package/tests/checkout/validation.test.ts +178 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +708 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/extract-url.test.ts +517 -0
- package/tests/groups-members.test.ts +256 -0
- package/tests/groups-show.test.ts +323 -0
- package/tests/groups.test.ts +334 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/init.test.ts +70 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +285 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/projects.test.ts +259 -0
- package/tests/rebase.test.ts +271 -0
- package/tests/remove-reviewer.test.ts +357 -0
- package/tests/restore.test.ts +237 -0
- package/tests/review.test.ts +135 -0
- package/tests/search.test.ts +712 -0
- package/tests/setup.test.ts +63 -0
- package/tests/show-auto-detect.test.ts +324 -0
- package/tests/show.test.ts +813 -0
- package/tests/status.test.ts +145 -0
- package/tests/submit.test.ts +316 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/git-branch-detection.test.ts +82 -0
- package/tests/unit/git-worktree.test.ts +55 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
- package/tests/unit/services/review-strategy.test.ts +349 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tests/vote.test.ts +317 -0
- package/tests/workspace.test.ts +295 -0
- package/tsconfig.json +36 -5
- package/src/commands/branch.ts +0 -180
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- package/src/utils.ts +0 -130
|
@@ -0,0 +1,237 @@
|
|
|
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 { restoreCommand } from '@/cli/commands/restore'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
9
|
+
import type { ChangeInfo } from '@/schemas/gerrit'
|
|
10
|
+
|
|
11
|
+
const mockChange: ChangeInfo = {
|
|
12
|
+
id: 'test-project~master~I123',
|
|
13
|
+
_number: 12345,
|
|
14
|
+
change_id: 'I123',
|
|
15
|
+
project: 'test-project',
|
|
16
|
+
branch: 'master',
|
|
17
|
+
subject: 'Test change to restore',
|
|
18
|
+
status: 'ABANDONED',
|
|
19
|
+
created: '2024-01-01 10:00:00.000000000',
|
|
20
|
+
updated: '2024-01-01 12:00:00.000000000',
|
|
21
|
+
owner: {
|
|
22
|
+
_account_id: 1000,
|
|
23
|
+
name: 'Test User',
|
|
24
|
+
email: 'test@example.com',
|
|
25
|
+
},
|
|
26
|
+
labels: {
|
|
27
|
+
'Code-Review': {
|
|
28
|
+
value: 0,
|
|
29
|
+
},
|
|
30
|
+
Verified: {
|
|
31
|
+
value: 0,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
work_in_progress: false,
|
|
35
|
+
submittable: false,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mockRestoredChange: ChangeInfo = {
|
|
39
|
+
...mockChange,
|
|
40
|
+
status: 'NEW',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create MSW server
|
|
44
|
+
const server = setupServer(
|
|
45
|
+
// Default handler for auth check
|
|
46
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
47
|
+
const auth = request.headers.get('Authorization')
|
|
48
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
49
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
50
|
+
}
|
|
51
|
+
return HttpResponse.json({
|
|
52
|
+
_account_id: 1000,
|
|
53
|
+
name: 'Test User',
|
|
54
|
+
email: 'test@example.com',
|
|
55
|
+
})
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
describe('restore command', () => {
|
|
60
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
61
|
+
let mockConsoleError: ReturnType<typeof mock>
|
|
62
|
+
|
|
63
|
+
beforeAll(() => {
|
|
64
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
server.close()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
mockConsoleLog = mock(() => {})
|
|
73
|
+
mockConsoleError = mock(() => {})
|
|
74
|
+
console.log = mockConsoleLog
|
|
75
|
+
console.error = mockConsoleError
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
server.resetHandlers()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should restore a change with a message', async () => {
|
|
83
|
+
server.use(
|
|
84
|
+
http.post('*/a/changes/12345/restore', async ({ request }) => {
|
|
85
|
+
const body = (await request.json()) as { message?: string }
|
|
86
|
+
expect(body.message).toBe('Restoring this change')
|
|
87
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockRestoredChange)}`)
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
92
|
+
const program = restoreCommand('12345', {
|
|
93
|
+
message: 'Restoring this change',
|
|
94
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
95
|
+
|
|
96
|
+
await Effect.runPromise(program)
|
|
97
|
+
|
|
98
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
99
|
+
expect(output).toContain('Restored change 12345')
|
|
100
|
+
expect(output).toContain('Test change to restore')
|
|
101
|
+
expect(output).toContain('Message: Restoring this change')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should restore a change without a message', async () => {
|
|
105
|
+
server.use(
|
|
106
|
+
http.post('*/a/changes/12345/restore', async ({ request }) => {
|
|
107
|
+
const body = (await request.json()) as { message?: string }
|
|
108
|
+
expect(body.message).toBeUndefined()
|
|
109
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockRestoredChange)}`)
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
114
|
+
const program = restoreCommand('12345', {}).pipe(
|
|
115
|
+
Effect.provide(GerritApiServiceLive),
|
|
116
|
+
Effect.provide(mockConfigLayer),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
await Effect.runPromise(program)
|
|
120
|
+
|
|
121
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
122
|
+
expect(output).toContain('Restored change 12345')
|
|
123
|
+
expect(output).toContain('Test change to restore')
|
|
124
|
+
expect(output).not.toContain('Message:')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should output XML format when --xml flag is used', async () => {
|
|
128
|
+
server.use(
|
|
129
|
+
http.post('*/a/changes/12345/restore', async ({ request }) => {
|
|
130
|
+
const body = (await request.json()) as { message?: string }
|
|
131
|
+
expect(body.message).toBe('Restoring for testing')
|
|
132
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockRestoredChange)}`)
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
137
|
+
const program = restoreCommand('12345', {
|
|
138
|
+
xml: true,
|
|
139
|
+
message: 'Restoring for testing',
|
|
140
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
141
|
+
|
|
142
|
+
await Effect.runPromise(program)
|
|
143
|
+
|
|
144
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
145
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
146
|
+
expect(output).toContain('<restore_result>')
|
|
147
|
+
expect(output).toContain('<status>success</status>')
|
|
148
|
+
expect(output).toContain('<change_number>12345</change_number>')
|
|
149
|
+
expect(output).toContain('<subject><![CDATA[Test change to restore]]></subject>')
|
|
150
|
+
expect(output).toContain('<message><![CDATA[Restoring for testing]]></message>')
|
|
151
|
+
expect(output).toContain('</restore_result>')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should output XML format without message when no message provided', async () => {
|
|
155
|
+
server.use(
|
|
156
|
+
http.post('*/a/changes/12345/restore', async () => {
|
|
157
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockRestoredChange)}`)
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
162
|
+
const program = restoreCommand('12345', { xml: true }).pipe(
|
|
163
|
+
Effect.provide(GerritApiServiceLive),
|
|
164
|
+
Effect.provide(mockConfigLayer),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
await Effect.runPromise(program)
|
|
168
|
+
|
|
169
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
170
|
+
expect(output).toContain('<restore_result>')
|
|
171
|
+
expect(output).toContain('<status>success</status>')
|
|
172
|
+
expect(output).not.toContain('<message>')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should handle not found errors gracefully', async () => {
|
|
176
|
+
server.use(
|
|
177
|
+
http.post('*/a/changes/99999/restore', () => {
|
|
178
|
+
return HttpResponse.text('Change not found', { status: 404 })
|
|
179
|
+
}),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
183
|
+
const program = restoreCommand('99999', {
|
|
184
|
+
message: 'Test message',
|
|
185
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
186
|
+
|
|
187
|
+
// Should fail when change is not found
|
|
188
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should show error when change ID is not provided', async () => {
|
|
192
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
193
|
+
const program = restoreCommand(undefined, {}).pipe(
|
|
194
|
+
Effect.provide(GerritApiServiceLive),
|
|
195
|
+
Effect.provide(mockConfigLayer),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
await Effect.runPromise(program)
|
|
199
|
+
|
|
200
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
201
|
+
expect(errorOutput).toContain('Change ID is required')
|
|
202
|
+
expect(errorOutput).toContain('Usage: ger restore <change-id>')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should handle restore API failure', async () => {
|
|
206
|
+
server.use(
|
|
207
|
+
http.post('*/a/changes/12345/restore', () => {
|
|
208
|
+
return HttpResponse.text('Forbidden', { status: 403 })
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
213
|
+
const program = restoreCommand('12345', {
|
|
214
|
+
message: 'Test',
|
|
215
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
216
|
+
|
|
217
|
+
// Should throw/fail
|
|
218
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should handle changes that are already active', async () => {
|
|
222
|
+
server.use(
|
|
223
|
+
http.post('*/a/changes/12345/restore', () => {
|
|
224
|
+
return HttpResponse.text('Change is already active', { status: 409 })
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
229
|
+
const program = restoreCommand('12345', {}).pipe(
|
|
230
|
+
Effect.provide(GerritApiServiceLive),
|
|
231
|
+
Effect.provide(mockConfigLayer),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Should throw/fail
|
|
235
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test'
|
|
2
|
+
import { Effect, Layer } from 'effect'
|
|
3
|
+
import { GitWorktreeService, WorktreeCreationError } from '@/services/git-worktree'
|
|
4
|
+
|
|
5
|
+
describe('Review Command - Focused Tests', () => {
|
|
6
|
+
let consoleSpy: any
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
consoleSpy = {
|
|
10
|
+
log: spyOn(console, 'log').mockImplementation(() => {}),
|
|
11
|
+
error: spyOn(console, 'error').mockImplementation(() => {}),
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
consoleSpy.log.mockRestore()
|
|
17
|
+
consoleSpy.error.mockRestore()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('should integrate GitWorktreeService with review workflow', async () => {
|
|
21
|
+
// Mock Git Worktree Service
|
|
22
|
+
const mockGitService = {
|
|
23
|
+
validatePreconditions: () => Effect.succeed(undefined),
|
|
24
|
+
createWorktree: (changeId: string) =>
|
|
25
|
+
Effect.succeed({
|
|
26
|
+
path: `/tmp/test-worktree-${changeId}`,
|
|
27
|
+
changeId,
|
|
28
|
+
originalCwd: '/test/current',
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
pid: process.pid,
|
|
31
|
+
}),
|
|
32
|
+
fetchAndCheckoutPatchset: () => Effect.succeed(undefined),
|
|
33
|
+
cleanup: () => Effect.succeed(undefined),
|
|
34
|
+
getChangedFiles: () => Effect.succeed(['src/main.ts', 'tests/main.test.ts']),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Test the complete GitWorktreeService workflow
|
|
38
|
+
const result = await Effect.runPromise(
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const service = yield* GitWorktreeService
|
|
41
|
+
|
|
42
|
+
// Test precondition validation
|
|
43
|
+
yield* service.validatePreconditions()
|
|
44
|
+
|
|
45
|
+
// Test worktree creation
|
|
46
|
+
const worktree = yield* service.createWorktree('12345')
|
|
47
|
+
expect(worktree.changeId).toBe('12345')
|
|
48
|
+
expect(worktree.path).toContain('12345')
|
|
49
|
+
expect(worktree.originalCwd).toBe('/test/current')
|
|
50
|
+
|
|
51
|
+
// Test patchset fetch
|
|
52
|
+
yield* service.fetchAndCheckoutPatchset(worktree)
|
|
53
|
+
|
|
54
|
+
// Test getting changed files
|
|
55
|
+
const files = yield* service.getChangedFiles()
|
|
56
|
+
expect(files).toEqual(['src/main.ts', 'tests/main.test.ts'])
|
|
57
|
+
|
|
58
|
+
// Test cleanup
|
|
59
|
+
yield* service.cleanup(worktree)
|
|
60
|
+
|
|
61
|
+
return { success: true, worktree, files }
|
|
62
|
+
}).pipe(Effect.provide(Layer.succeed(GitWorktreeService, mockGitService))),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Verify the workflow completed successfully
|
|
66
|
+
expect(result.success).toBe(true)
|
|
67
|
+
expect(result.worktree.path).toContain('12345')
|
|
68
|
+
expect(result.files).toHaveLength(2)
|
|
69
|
+
expect(result.files).toEqual(['src/main.ts', 'tests/main.test.ts'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('should handle concurrent worktree scenarios with unique paths', async () => {
|
|
73
|
+
const mockGitService = {
|
|
74
|
+
validatePreconditions: () => Effect.succeed(undefined),
|
|
75
|
+
createWorktree: (changeId: string) =>
|
|
76
|
+
Effect.succeed({
|
|
77
|
+
path: `/tmp/test-worktree-${changeId}-${Date.now()}-${process.pid}`,
|
|
78
|
+
changeId,
|
|
79
|
+
originalCwd: process.cwd(),
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
pid: process.pid,
|
|
82
|
+
}),
|
|
83
|
+
fetchAndCheckoutPatchset: () => Effect.succeed(undefined),
|
|
84
|
+
cleanup: () => Effect.succeed(undefined),
|
|
85
|
+
getChangedFiles: () => Effect.succeed(['test.ts']),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Simulate concurrent worktree creation
|
|
89
|
+
const [result1, result2] = await Promise.all([
|
|
90
|
+
Effect.runPromise(
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
const service = yield* GitWorktreeService
|
|
93
|
+
return yield* service.createWorktree('change-1')
|
|
94
|
+
}).pipe(Effect.provide(Layer.succeed(GitWorktreeService, mockGitService))),
|
|
95
|
+
),
|
|
96
|
+
Effect.runPromise(
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const service = yield* GitWorktreeService
|
|
99
|
+
return yield* service.createWorktree('change-2')
|
|
100
|
+
}).pipe(Effect.provide(Layer.succeed(GitWorktreeService, mockGitService))),
|
|
101
|
+
),
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
// Verify both worktrees have unique paths
|
|
105
|
+
expect(result1.path).not.toBe(result2.path)
|
|
106
|
+
expect(result1.changeId).toBe('change-1')
|
|
107
|
+
expect(result2.changeId).toBe('change-2')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('should handle error scenarios in worktree operations', async () => {
|
|
111
|
+
const failingGitService = {
|
|
112
|
+
validatePreconditions: () =>
|
|
113
|
+
Effect.fail(new WorktreeCreationError({ message: 'Git repository validation failed' })),
|
|
114
|
+
createWorktree: () =>
|
|
115
|
+
Effect.fail(new WorktreeCreationError({ message: 'Worktree creation failed' })),
|
|
116
|
+
fetchAndCheckoutPatchset: () =>
|
|
117
|
+
Effect.fail(new WorktreeCreationError({ message: 'Patchset fetch failed' })),
|
|
118
|
+
cleanup: () => Effect.succeed(undefined), // Cleanup should never fail
|
|
119
|
+
getChangedFiles: () => Effect.fail(new WorktreeCreationError({ message: 'Git diff failed' })),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Test validation failure
|
|
123
|
+
const validationResult = await Effect.runPromiseExit(
|
|
124
|
+
Effect.gen(function* () {
|
|
125
|
+
const service = yield* GitWorktreeService
|
|
126
|
+
yield* service.validatePreconditions()
|
|
127
|
+
}).pipe(Effect.provide(Layer.succeed(GitWorktreeService, failingGitService))),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(validationResult._tag).toBe('Failure')
|
|
131
|
+
if (validationResult._tag === 'Failure') {
|
|
132
|
+
expect(String(validationResult.cause)).toContain('Git repository validation failed')
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
})
|