spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / mods / diff / tests / register.test.ts
387 lines · 11.0 KBRaw
1import type { Args } from 'claude-code'
2import { describe, expect, mock, test, tier } from 'claude-code/testing'
3
4import Limits from '../hooks/limits'
5import Names from '../hooks/names'
6import Fixtures from './fixtures'
7
8tier('builtin')
9
10describe('register', () => {
11 test('/diff at boot joins the boot probe, then asks again', async ($, on) => {
12 const probes: (readonly string[])[] = []
13 const clock = Fixtures.startsSession(on)
14
15 on('process.run', async ($, e) => {
16 probes.push(e.argv)
17
18 if (probes.length === 1) {
19 await clock.sleep(Limits.GIT_TIMEOUT_MS)
20
21 return { deny: Fixtures.GIT_HUNG }
22 }
23
24 return { value: Fixtures.NOT_A_REPOSITORY }
25 })
26
27 const booting = $.session.start(Fixtures.SESSION)
28
29 await clock.settle()
30
31 const ran = $.command.run(Fixtures.DIFF)
32
33 await clock.settle()
34
35 expect(probes, 'the boot probe, which /diff joined').toHaveLength(1)
36
37 await clock.advance(Limits.GIT_TIMEOUT_MS)
38 await booting
39
40 expect(await ran).toEqual({
41 text: expect.stringContaining("isn't in a git repository"),
42 })
43
44 expect(probes, 'then one more of its own').toHaveLength(2)
45 })
46
47 test('outside a repository /diff says so, opens nothing', async ($, on) => {
48 const opened: string[] = []
49
50 Fixtures.startsSession(on)
51 on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY }))
52
53 on('ui.open', ($, e, next) => {
54 opened.push(e.id)
55
56 return next(e)
57 })
58
59 await $.session.start(Fixtures.SESSION)
60
61 const { text } = await $.command.run(Fixtures.DIFF)
62
63 expect(text).toContain("isn't in a git repository")
64 expect(opened).toEqual([])
65 })
66
67 test('a git that never answers is not "no repository"', async ($, on) => {
68 Fixtures.startsSession(on)
69 on('process.run', () => ({ deny: Fixtures.GIT_HUNG }))
70
71 await $.session.start(Fixtures.SESSION)
72
73 const { text } = await $.command.run(Fixtures.DIFF)
74
75 expect(text).toContain("git didn't answer")
76 })
77
78 test('when the built-in holds /diff, the mod stands down', async ($, on) => {
79 const logged: string[] = []
80
81 mock.clock(on)
82 on('session.start', ($, e) => ({ cwd: e.cwd }))
83 on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS }))
84 on('command.run', () => ({ text: 'the built-in /diff ran' }))
85
86 on('ui.log', ($, e) => {
87 logged.push(e.text)
88
89 return { value: undefined }
90 })
91
92 await $.session.start(Fixtures.SESSION)
93
94 expect(await $.command.run(Fixtures.DIFF)).toEqual({
95 text: 'the built-in /diff ran',
96 })
97
98 expect(logged).toEqual([])
99 })
100
101 test('a refusal the built-in did not cause is said aloud', async ($, on) => {
102 const logged: string[] = []
103
104 mock.clock(on)
105 on('session.start', ($, e) => ({ cwd: e.cwd }))
106
107 on('command.register', () => ({
108 deny: '32 commands are registered already',
109 }))
110
111 on('ui.log', ($, e) => {
112 logged.push(e.text)
113
114 return { value: undefined }
115 })
116
117 await $.session.start(Fixtures.SESSION)
118
119 expect(logged).toEqual([
120 'could not register /diff: diff: $.command.register: 32 commands are ' +
121 'registered already; the diff panel is unavailable this session',
122 ])
123 })
124
125 test('/diff opens the pane over the session changes', async ($, on) => {
126 const world = Fixtures.inRepository(on)
127
128 await $.session.start(Fixtures.SESSION)
129
130 expect(await $.command.run(Fixtures.DIFF)).toEqual({})
131 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
132
133 await world.clock.advance(Fixtures.SETTLE_MS)
134
135 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
136
137 expect(drawn).toContain('1 file changed')
138 expect(drawn).toContain('app.ts')
139 })
140
141 test('the close button closes the pane; /diff reopens it', async ($, on) => {
142 const world = Fixtures.inRepository(on)
143
144 await $.session.start(Fixtures.SESSION)
145 await $.command.run(Fixtures.DIFF)
146 await world.clock.advance(Fixtures.SETTLE_MS)
147 await $.ui.render(Fixtures.PANE)
148
149 expect(await $.ui.press({ plugin: 'diff', key: 'close' })).toEqual({
150 element: 'close',
151 })
152
153 await world.clock.settle()
154
155 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
156 expect(await $.command.run(Fixtures.DIFF)).toEqual({})
157 expect(world.opened.map(pane => pane.id)).toEqual(['diff', 'diff'])
158 })
159
160 test('a wide terminal opens the pane at the first edit', async ($, on) => {
161 const world = Fixtures.inRepository(on)
162
163 on('tool.call', () => ({ result: 'edited' }))
164
165 await $.session.start(Fixtures.SESSION)
166 await $.ui.render(Fixtures.HINT)
167
168 await $.tool.call({
169 tool: 'Edit',
170 file_path: '/work/app.ts',
171 old_string: '1',
172 new_string: '2',
173 })
174
175 await world.clock.advance(Fixtures.SETTLE_MS)
176
177 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
178 })
179
180 test('/clear closes the pane it finds open', async ($, on) => {
181 const world = Fixtures.inRepository(on)
182
183 on('command.run', { command: 'clear' }, () => ({}))
184
185 await $.session.start(Fixtures.SESSION)
186 await $.command.run(Fixtures.DIFF)
187 await $.command.run(Fixtures.CLEAR)
188
189 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
190 })
191
192 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
193 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
194 const called: Args<'tool.call'>[] = []
195
196 on('tool.call', ($, e) => {
197 called.push(e)
198
199 return { result: 'called' }
200 })
201
202 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
203
204 await $.session.start(Fixtures.WORKTREE_SESSION)
205 await $.command.run(Fixtures.DIFF)
206 await world.clock.advance(Fixtures.SETTLE_MS)
207 await $.ui.render(Fixtures.PANE)
208 await $.ui.press({ plugin: 'diff', key: 'ask' })
209 await world.clock.settle()
210
211 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
212
213 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
214
215 expect(armed).toContain('asked ✓')
216 expect(called).toEqual([])
217
218 expect(world.statuses.at(-2)).toBe(
219 '.env rides your next prompt (press asked ✓ to drop it)',
220 )
221
222 expect(submitted).toMatchObject({
223 context: [
224 'The user attached the diff of .env from the diff pane to this ' +
225 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
226 ],
227 })
228 })
229
230 test('an ask with no room in the context is dropped', async ($, on) => {
231 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
232 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
233 const reached: (readonly string[] | undefined)[] = []
234
235 on('prompt.submit', ($, e) => {
236 reached.push(e.context)
237
238 return { text: e.text }
239 })
240
241 await $.session.start(Fixtures.WORKTREE_SESSION)
242 await $.command.run(Fixtures.DIFF)
243 await world.clock.advance(Fixtures.SETTLE_MS)
244 await $.ui.render(Fixtures.PANE)
245 await $.ui.press({ plugin: 'diff', key: 'ask' })
246 await world.clock.settle()
247
248 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
249 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
250
251 expect(reached).toEqual([[full], undefined])
252
253 expect(world.statuses.at(-1)).toBe(
254 ".env's diff did not fit in the prompt and was dropped",
255 )
256 })
257
258 test('a worktree opens on the base it kept', async ($, on) => {
259 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
260 'base:/main/wt': 'uncommitted',
261 })
262
263 await $.session.start(Fixtures.WORKTREE_SESSION)
264 await $.command.run(Fixtures.DIFF)
265 await world.clock.advance(Fixtures.SETTLE_MS)
266
267 const base = Fixtures.elementIn(await $.ui.render(Fixtures.PANE), {
268 type: 'Select',
269 name: 'base',
270 })
271
272 expect(base?.props).toMatchObject({ value: 'uncommitted' })
273 })
274
275 test('a repository made later is pinned once, on /diff', async ($, on) => {
276 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
277 Fixtures.oneSecret()
278
279 const script: Record<string, string> = notYet
280 const world = Fixtures.inRepository(on, script)
281
282 await $.session.start(Fixtures.WORKTREE_SESSION)
283
284 const before = await $.command.run(Fixtures.DIFF)
285
286 script['rev-parse --path-format=absolute'] = worktree
287
288 const after = await $.command.run(Fixtures.DIFF)
289
290 await world.clock.advance(Fixtures.SETTLE_MS)
291 await $.ui.render(Fixtures.PANE)
292
293 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
294
295 const third = $.command.run(Fixtures.DIFF)
296
297 await world.clock.advance(Fixtures.SETTLE_MS)
298 await third
299
300 const probes = world.runs.filter(run =>
301 run.argv.includes('--show-toplevel'),
302 )
303
304 expect(before.text).toContain("isn't in a git repository")
305 expect(after).toEqual({})
306 expect(world.opened[0]?.id).toBe('diff')
307 expect(probes).toHaveLength(3)
308
309 expect(
310 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
311 ).toBe(false)
312
313 expect(
314 world.runs
315 .filter(run => run.argv.includes('--numstat'))
316 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
317 ).toBe(true)
318 })
319
320 test('a repository that appears later opens on first edit', async ($, on) => {
321 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
322 Fixtures.oneSecret()
323
324 const script: Record<string, string> = notYet
325 const world = Fixtures.inRepository(on, script)
326
327 on('tool.call', () => ({ result: 'edited' }))
328
329 await $.session.start(Fixtures.WORKTREE_SESSION)
330 await $.ui.render(Fixtures.HINT)
331
332 script['rev-parse --path-format=absolute'] = worktree
333
334 await $.tool.call({
335 tool: 'Edit',
336 file_path: '/main/wt/.env',
337 old_string: 'old',
338 new_string: 'new',
339 })
340
341 await world.clock.advance(Fixtures.SETTLE_MS)
342
343 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
344 })
345
346 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
347 const runs: Args<'process.run'>[] = []
348
349 Fixtures.startsSession(on)
350
351 on('process.run', ($, e) => {
352 runs.push(e)
353
354 return e.argv.includes('--show-toplevel')
355 ? { deny: Fixtures.FAILED_START }
356 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
357 })
358
359 await $.session.start(Fixtures.WORKTREE_SESSION)
360
361 const { text } = await $.command.run(Fixtures.DIFF)
362 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
363
364 expect(text).toContain("isn't in a git repository")
365 expect(probes, "the boot's, then this /diff's; no retry").toHaveLength(2)
366 })
367
368 test('/diff on a narrow terminal: the resize line', async ($, on) => {
369 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
370
371 await $.session.start(Fixtures.WORKTREE_SESSION)
372 await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS - 1))
373
374 const narrow = await $.command.run(Fixtures.DIFF)
375 const openedNarrow = [...world.opened]
376
377 await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS))
378 await $.command.run(Fixtures.DIFF)
379
380 expect(narrow.text).toContain(
381 'Resize your terminal to at least 110 columns to show the diff panel',
382 )
383
384 expect(openedNarrow).toEqual([])
385 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
386 })
387})