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
460 lines · 12.8 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('the start asks nothing of git and registers /diff', async ($, on) => {
12 const world = Fixtures.inRepository(on)
13
14 await $.session.start(Fixtures.SESSION)
15
16 expect(world.runs, "no git, as the built-in's start runs none").toEqual([])
17
18 expect(await $.command.run(Fixtures.DIFF), '/diff was registered').toEqual({
19 text: 'Diff panel shown',
20 })
21 })
22
23 test('/diff whose probe never answers probes once more', async ($, on) => {
24 const probes: (readonly string[])[] = []
25 const clock = Fixtures.startsSession(on)
26
27 on('process.run', async ($, e) => {
28 probes.push(e.argv)
29
30 if (probes.length === 1) {
31 await clock.sleep(Limits.GIT_TIMEOUT_MS)
32
33 return { deny: Fixtures.GIT_HUNG }
34 }
35
36 return { value: Fixtures.NOT_A_REPOSITORY }
37 })
38
39 await $.session.start(Fixtures.SESSION)
40
41 const ran = $.command.run(Fixtures.DIFF)
42
43 await clock.settle()
44
45 expect(probes, "this /diff's probe, unanswered yet").toHaveLength(1)
46
47 await clock.advance(Limits.GIT_TIMEOUT_MS)
48
49 expect(await ran).toEqual({
50 text: expect.stringContaining("isn't in a git repository"),
51 })
52
53 expect(probes, 'then one more, which answered').toHaveLength(2)
54 })
55
56 test('two /diff typed together probe the repository once', async ($, on) => {
57 const world = Fixtures.inRepository(on)
58
59 await $.session.start(Fixtures.SESSION)
60
61 const [first, second] = await Promise.all([
62 $.command.run(Fixtures.DIFF),
63 $.command.run(Fixtures.DIFF),
64 ])
65
66 const probes = world.runs.filter(run =>
67 run.argv.includes('--show-toplevel'),
68 )
69
70 expect(first.text).toMatch(/^Diff panel (shown|hidden)$/)
71 expect(second.text).toMatch(/^Diff panel (shown|hidden)$/)
72 expect(probes, 'the second joined the probe in flight').toHaveLength(1)
73 })
74
75 test('outside a repository /diff says so, opens nothing', async ($, on) => {
76 const opened: string[] = []
77
78 Fixtures.startsSession(on)
79 on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY }))
80
81 on('ui.open', ($, e, next) => {
82 opened.push(e.id)
83
84 return next(e)
85 })
86
87 await $.session.start(Fixtures.SESSION)
88
89 const { text } = await $.command.run(Fixtures.DIFF)
90
91 expect(text).toContain("isn't in a git repository")
92 expect(opened).toEqual([])
93 })
94
95 test('a git that never answers is not "no repository"', async ($, on) => {
96 Fixtures.startsSession(on)
97 on('process.run', () => ({ deny: Fixtures.GIT_HUNG }))
98
99 await $.session.start(Fixtures.SESSION)
100
101 const { text } = await $.command.run(Fixtures.DIFF)
102
103 expect(text).toContain("git didn't answer")
104 })
105
106 test('when the built-in holds /diff, the mod stands down', async ($, on) => {
107 const logged: string[] = []
108 const runs: Args<'process.run'>[] = []
109
110 mock.clock(on)
111 on('session.start', ($, e) => ({ cwd: e.cwd }))
112 on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS }))
113 on('command.run', () => ({ text: 'the built-in /diff ran' }))
114 on('tool.call', () => ({ result: 'edited' }))
115
116 on('process.run', ($, e) => {
117 runs.push(e)
118
119 return { value: Fixtures.gitIn(e.argv) }
120 })
121
122 on('ui.log', ($, e) => {
123 logged.push(e.text)
124
125 return { value: undefined }
126 })
127
128 await $.session.start(Fixtures.SESSION)
129
130 expect(await $.command.run(Fixtures.DIFF)).toEqual({
131 text: 'the built-in /diff ran',
132 })
133
134 await $.tool.call({
135 tool: 'Edit',
136 file_path: '/work/app.ts',
137 old_string: '1',
138 new_string: '2',
139 })
140
141 expect(logged).toEqual([])
142 expect(runs, 'idle: no git for /diff or the edit').toEqual([])
143 })
144
145 test('a refusal the built-in did not cause is said aloud', async ($, on) => {
146 const logged: string[] = []
147
148 mock.clock(on)
149 on('session.start', ($, e) => ({ cwd: e.cwd }))
150
151 on('command.register', () => ({
152 deny: '32 commands are registered already',
153 }))
154
155 on('ui.log', ($, e) => {
156 logged.push(e.text)
157
158 return { value: undefined }
159 })
160
161 await $.session.start(Fixtures.SESSION)
162
163 expect(logged).toEqual([
164 'could not register /diff: diff: $.command.register: 32 commands are ' +
165 'registered already; the diff panel is unavailable this session',
166 ])
167 })
168
169 test('/diff opens the pane, unfocused, and says so', async ($, on) => {
170 const world = Fixtures.inRepository(on)
171
172 await $.session.start(Fixtures.SESSION)
173
174 expect(world.runs, 'the start ran nothing').toEqual([])
175
176 expect(await $.command.run(Fixtures.DIFF)).toEqual({
177 text: 'Diff panel shown',
178 })
179
180 expect(world.runs[0]?.argv, '/diff found the repository first').toContain(
181 '--show-toplevel',
182 )
183
184 expect(world.opened).toEqual([
185 { id: 'diff', title: 'Diff', holdToasts: true },
186 ])
187
188 await world.clock.advance(Fixtures.SETTLE_MS)
189
190 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
191
192 expect(drawn).toContain('1 file changed')
193 expect(drawn).toContain('app.ts')
194 })
195
196 test('off fullscreen, /diff opens the dialog, focused', async ($, on) => {
197 const world = Fixtures.inRepository(on)
198
199 await $.session.start(Fixtures.SESSION)
200
201 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
202
203 expect(world.opened[0]).toEqual({
204 id: 'diff',
205 title: 'Diff',
206 holdToasts: true,
207 closeOnEscape: true,
208 rows: expect.any(Number),
209 focus: true,
210 })
211
212 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({
213 text: 'Diff dialog dismissed',
214 })
215 })
216
217 test('/diff again closes the pane and says so', async ($, on) => {
218 const world = Fixtures.inRepository(on)
219
220 await $.session.start(Fixtures.SESSION)
221 await $.command.run(Fixtures.DIFF)
222
223 expect(await $.command.run(Fixtures.DIFF)).toEqual({
224 text: 'Diff panel hidden',
225 })
226
227 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
228 })
229
230 test('a wide terminal opens the pane at the first edit', async ($, on) => {
231 const world = Fixtures.inRepository(on)
232
233 on('tool.call', () => ({ result: 'edited' }))
234
235 await $.session.start(Fixtures.SESSION)
236 await $.ui.render(Fixtures.HINT)
237
238 await $.tool.call({
239 tool: 'Edit',
240 file_path: '/work/app.ts',
241 old_string: '1',
242 new_string: '2',
243 })
244
245 await world.clock.advance(Fixtures.SETTLE_MS)
246
247 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
248 })
249
250 test('/clear closes the pane it finds open', async ($, on) => {
251 const world = Fixtures.inRepository(on)
252
253 on('command.run', { command: 'clear' }, () => ({}))
254
255 await $.session.start(Fixtures.SESSION)
256 await $.command.run(Fixtures.DIFF)
257 await $.command.run(Fixtures.CLEAR)
258
259 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
260 })
261
262 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
263 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
264 const called: Args<'tool.call'>[] = []
265
266 on('tool.call', ($, e) => {
267 called.push(e)
268
269 return { result: 'called' }
270 })
271
272 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
273
274 await $.session.start(Fixtures.WORKTREE_SESSION)
275 await $.command.run(Fixtures.DIFF)
276 await world.clock.advance(Fixtures.SETTLE_MS)
277 await $.ui.render(Fixtures.PANE)
278 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
279 await world.clock.settle()
280
281 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
282
283 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
284
285 expect(armed).toContain('asked ✓')
286 expect(called).toEqual([])
287
288 expect(world.statuses.at(-2)).toBe(
289 '.env rides your next prompt (press asked ✓ to drop it)',
290 )
291
292 expect(submitted).toMatchObject({
293 context: [
294 'The user attached the diff of .env from the diff pane to this ' +
295 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
296 ],
297 })
298 })
299
300 test('an ask with no room in the context is dropped', async ($, on) => {
301 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
302 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
303 const reached: (readonly string[] | undefined)[] = []
304
305 on('prompt.submit', ($, e) => {
306 reached.push(e.context)
307
308 return { text: e.text }
309 })
310
311 await $.session.start(Fixtures.WORKTREE_SESSION)
312 await $.command.run(Fixtures.DIFF)
313 await world.clock.advance(Fixtures.SETTLE_MS)
314 await $.ui.render(Fixtures.PANE)
315 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
316 await world.clock.settle()
317
318 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
319 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
320
321 expect(reached).toEqual([[full], undefined])
322
323 expect(world.statuses.at(-1)).toBe(
324 ".env's diff did not fit in the prompt and was dropped",
325 )
326 })
327
328 test('a worktree opens on the base it kept', async ($, on) => {
329 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
330 'base:/main/wt': 'uncommitted',
331 })
332
333 await $.session.start(Fixtures.WORKTREE_SESSION)
334 await $.command.run(Fixtures.DIFF)
335 await world.clock.advance(Fixtures.SETTLE_MS)
336
337 expect(
338 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
339 "the built-in's base line under the header",
340 ).toContain('uncommitted (vs HEAD)')
341 })
342
343 test('a repository made later is pinned once, on /diff', async ($, on) => {
344 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
345 Fixtures.oneSecret()
346
347 const script: Record<string, string> = notYet
348 const world = Fixtures.inRepository(on, script)
349
350 await $.session.start(Fixtures.WORKTREE_SESSION)
351
352 const before = await $.command.run(Fixtures.DIFF)
353
354 script['rev-parse --path-format=absolute'] = worktree
355
356 const after = await $.command.run(Fixtures.DIFF)
357
358 await world.clock.advance(Fixtures.SETTLE_MS)
359 await $.ui.render(Fixtures.PANE)
360
361 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
362
363 const third = $.command.run(Fixtures.DIFF)
364
365 await world.clock.advance(Fixtures.SETTLE_MS)
366 await third
367
368 const probes = world.runs.filter(run =>
369 run.argv.includes('--show-toplevel'),
370 )
371
372 expect(before.text).toContain("isn't in a git repository")
373 expect(after.text).toBe('Diff panel shown')
374 expect(world.opened[0]?.id).toBe('diff')
375
376 expect(
377 probes,
378 'the first two /diff probed; the third was pinned',
379 ).toHaveLength(2)
380
381 expect(
382 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
383 ).toBe(false)
384
385 expect(
386 world.runs
387 .filter(run => run.argv.includes('--numstat'))
388 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
389 ).toBe(true)
390 })
391
392 test('a repository that appears later opens on first edit', async ($, on) => {
393 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
394 Fixtures.oneSecret()
395
396 const script: Record<string, string> = notYet
397 const world = Fixtures.inRepository(on, script)
398
399 on('tool.call', () => ({ result: 'edited' }))
400
401 await $.session.start(Fixtures.WORKTREE_SESSION)
402 await $.ui.render(Fixtures.HINT)
403
404 script['rev-parse --path-format=absolute'] = worktree
405
406 await $.tool.call({
407 tool: 'Edit',
408 file_path: '/main/wt/.env',
409 old_string: 'old',
410 new_string: 'new',
411 })
412
413 await world.clock.advance(Fixtures.SETTLE_MS)
414
415 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
416 })
417
418 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
419 const runs: Args<'process.run'>[] = []
420
421 Fixtures.startsSession(on)
422
423 on('process.run', ($, e) => {
424 runs.push(e)
425
426 return e.argv.includes('--show-toplevel')
427 ? { deny: Fixtures.FAILED_START }
428 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
429 })
430
431 await $.session.start(Fixtures.WORKTREE_SESSION)
432
433 const { text } = await $.command.run(Fixtures.DIFF)
434 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
435
436 expect(text).toContain("isn't in a git repository")
437 expect(probes, "this /diff's alone; no retry").toHaveLength(1)
438 })
439
440 test('/diff on a narrow terminal: the resize line', async ($, on) => {
441 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
442
443 await $.session.start(Fixtures.WORKTREE_SESSION)
444
445 const narrow = await $.command.run(
446 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
447 )
448
449 const openedNarrow = [...world.opened]
450
451 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
452
453 expect(narrow.text).toContain(
454 'Resize your terminal to at least 110 columns to show the diff panel',
455 )
456
457 expect(openedNarrow).toEqual([])
458 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
459 })
460})