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
669 lines · 18.6 KBRaw
1import type { Args, ResultOf } 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("git runs at the built-in's moments and no others", async ($, on) => {
24 const world = Fixtures.inRepository(on)
25
26 let read = 0
27
28 function spawnedSince(): string[] {
29 const words = world.runs
30 .slice(read)
31 .map(run => Fixtures.gitWordOf(run.argv))
32 .filter(word => word !== Fixtures.POLL_WORD)
33
34 read = world.runs.length
35
36 return words
37 }
38
39 const edit = () =>
40 $.tool.call({
41 tool: 'Edit',
42 file_path: '/work/app.ts',
43 old_string: '1',
44 new_string: '2',
45 })
46
47 on('tool.call', () => ({ result: 'done' }))
48 on('turn.complete', ($, e) => ({ text: e.answer }))
49 on('command.run', { command: 'clear' }, () => ({}))
50
51 await $.session.start(Fixtures.SESSION)
52 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
53 await world.clock.advance(Fixtures.SETTLE_MS)
54
55 expect(spawnedSince(), 'the start and the footer: nothing').toEqual([])
56
57 await edit()
58 await world.clock.advance(Fixtures.SETTLE_MS)
59
60 expect(
61 spawnedSince(),
62 'an edit with no room for a pane: nothing, the width is read first',
63 ).toEqual([])
64
65 await $.command.run(Fixtures.DIFF)
66 await world.clock.advance(Fixtures.SETTLE_MS)
67
68 expect(spawnedSince(), '/diff: found, then one fetch for the pane').toEqual(
69 [
70 'rev-parse --show-toplevel',
71 'status',
72 'diff --shortstat',
73 'diff --numstat',
74 'ls-files',
75 'diff -- app.ts',
76 ],
77 )
78
79 await edit()
80 await world.clock.advance(Fixtures.SETTLE_MS)
81
82 expect(spawnedSince(), 'an edit, the pane open: one fetch').toEqual([
83 'diff --shortstat',
84 'diff --numstat',
85 'ls-files',
86 'diff -- app.ts',
87 ])
88
89 await $.tool.call({ tool: 'Bash', command: 'make' })
90 await world.clock.advance(Fixtures.SETTLE_MS)
91
92 expect(spawnedSince(), 'a shell command, the pane open: one fetch').toEqual(
93 ['diff --shortstat', 'diff --numstat', 'ls-files', 'diff -- app.ts'],
94 )
95
96 await $.turn.complete({
97 answer: 'done',
98 durationMs: 1,
99 isAborted: false,
100 turnId: 't1',
101 reason: 'answer',
102 })
103
104 await world.clock.advance(Fixtures.SETTLE_MS)
105
106 expect(spawnedSince(), "a turn's end: nothing").toEqual([])
107
108 await $.command.run(Fixtures.DIFF)
109 await edit()
110 await $.tool.call({ tool: 'Bash', command: 'make' })
111 await world.clock.advance(Fixtures.SETTLE_MS)
112
113 expect(spawnedSince(), 'the pane closed by hand: nothing').toEqual([])
114
115 await $.command.run(Fixtures.CLEAR)
116 await $.command.run(Fixtures.DIFF)
117 await world.clock.advance(Fixtures.SETTLE_MS)
118
119 expect(spawnedSince()[0], '/clear forgot the repository').toBe(
120 'rev-parse --show-toplevel',
121 )
122 })
123
124 test('/diff whose probe never answers probes once more', async ($, on) => {
125 const probes: (readonly string[])[] = []
126 const clock = Fixtures.startsSession(on)
127
128 on('process.run', async ($, e) => {
129 probes.push(e.argv)
130
131 if (probes.length === 1) {
132 await clock.sleep(Limits.GIT_TIMEOUT_MS)
133
134 return { deny: Fixtures.GIT_HUNG }
135 }
136
137 return { value: Fixtures.NOT_A_REPOSITORY }
138 })
139
140 await $.session.start(Fixtures.SESSION)
141
142 const ran = $.command.run(Fixtures.DIFF)
143
144 await clock.settle()
145
146 expect(probes, "this /diff's probe, unanswered yet").toHaveLength(1)
147
148 await clock.advance(Limits.GIT_TIMEOUT_MS)
149
150 expect(await ran).toEqual({
151 text: expect.stringContaining("isn't in a git repository"),
152 })
153
154 expect(probes, 'then one more, which answered').toHaveLength(2)
155 })
156
157 test('two /diff typed together probe the repository once', async ($, on) => {
158 const world = Fixtures.inRepository(on)
159
160 await $.session.start(Fixtures.SESSION)
161
162 const [first, second] = await Promise.all([
163 $.command.run(Fixtures.DIFF),
164 $.command.run(Fixtures.DIFF),
165 ])
166
167 const probes = world.runs.filter(run =>
168 run.argv.includes('--show-toplevel'),
169 )
170
171 expect(first.text).toMatch(/^Diff panel (shown|hidden)$/)
172 expect(second.text).toMatch(/^Diff panel (shown|hidden)$/)
173 expect(probes, 'the second joined the probe in flight').toHaveLength(1)
174 })
175
176 test('outside a repository /diff says so, opens nothing', async ($, on) => {
177 const opened: string[] = []
178
179 Fixtures.startsSession(on)
180 on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY }))
181
182 on('ui.open', ($, e, next) => {
183 opened.push(e.id)
184
185 return next(e)
186 })
187
188 await $.session.start(Fixtures.SESSION)
189
190 const { text } = await $.command.run(Fixtures.DIFF)
191
192 expect(text).toContain("isn't in a git repository")
193 expect(opened).toEqual([])
194 })
195
196 test('a git that never answers is not "no repository"', async ($, on) => {
197 Fixtures.startsSession(on)
198 on('process.run', () => ({ deny: Fixtures.GIT_HUNG }))
199
200 await $.session.start(Fixtures.SESSION)
201
202 const { text } = await $.command.run(Fixtures.DIFF)
203
204 expect(text).toContain("git didn't answer")
205 })
206
207 test('when the built-in holds /diff, the mod stands down', async ($, on) => {
208 const logged: string[] = []
209 const runs: Args<'process.run'>[] = []
210
211 mock.clock(on)
212 on('session.start', ($, e) => ({ cwd: e.cwd }))
213 on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS }))
214 on('command.run', () => ({ text: 'the built-in /diff ran' }))
215 on('tool.call', () => ({ result: 'edited' }))
216
217 on('process.run', ($, e) => {
218 runs.push(e)
219
220 return { value: Fixtures.gitIn(e.argv) }
221 })
222
223 on('ui.log', ($, e) => {
224 logged.push(e.text)
225
226 return { value: undefined }
227 })
228
229 await $.session.start(Fixtures.SESSION)
230
231 expect(await $.command.run(Fixtures.DIFF)).toEqual({
232 text: 'the built-in /diff ran',
233 })
234
235 await $.tool.call({
236 tool: 'Edit',
237 file_path: '/work/app.ts',
238 old_string: '1',
239 new_string: '2',
240 })
241
242 expect(logged).toEqual([])
243 expect(runs, 'idle: no git for /diff or the edit').toEqual([])
244 })
245
246 test('a refusal the built-in did not cause is said aloud', async ($, on) => {
247 const logged: string[] = []
248
249 mock.clock(on)
250 on('session.start', ($, e) => ({ cwd: e.cwd }))
251
252 on('command.register', () => ({
253 deny: '32 commands are registered already',
254 }))
255
256 on('ui.log', ($, e) => {
257 logged.push(e.text)
258
259 return { value: undefined }
260 })
261
262 await $.session.start(Fixtures.SESSION)
263
264 expect(logged).toEqual([
265 'could not register /diff: diff: $.command.register: 32 commands are ' +
266 'registered already; the diff panel is unavailable this session',
267 ])
268 })
269
270 test('/diff opens the pane, unfocused, and says so', async ($, on) => {
271 const world = Fixtures.inRepository(on)
272
273 await $.session.start(Fixtures.SESSION)
274
275 expect(world.runs, 'the start ran nothing').toEqual([])
276
277 expect(await $.command.run(Fixtures.DIFF)).toEqual({
278 text: 'Diff panel shown',
279 })
280
281 expect(world.runs[0]?.argv, '/diff found the repository first').toContain(
282 '--show-toplevel',
283 )
284
285 expect(world.opened).toEqual([
286 { id: 'diff', title: 'Diff', holdToasts: true },
287 ])
288
289 await world.clock.advance(Fixtures.SETTLE_MS)
290
291 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
292
293 expect(drawn).toContain('1 file changed')
294 expect(drawn).toContain('app.ts')
295 })
296
297 test('off fullscreen, /diff opens the dialog, focused', async ($, on) => {
298 const world = Fixtures.inRepository(on)
299
300 await $.session.start(Fixtures.SESSION)
301
302 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
303
304 expect(world.opened[0]).toEqual({
305 id: 'diff',
306 title: 'Diff',
307 holdToasts: true,
308 closeOnEscape: true,
309 rows: expect.any(Number),
310 focus: true,
311 })
312
313 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({
314 text: 'Diff dialog dismissed',
315 })
316 })
317
318 test('/diff again closes the pane and says so', async ($, on) => {
319 const world = Fixtures.inRepository(on)
320
321 await $.session.start(Fixtures.SESSION)
322 await $.command.run(Fixtures.DIFF)
323
324 expect(await $.command.run(Fixtures.DIFF)).toEqual({
325 text: 'Diff panel hidden',
326 })
327
328 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
329 })
330
331 test('a wide terminal opens the pane at the first edit', async ($, on) => {
332 const world = Fixtures.inRepository(on)
333
334 on('tool.call', () => ({ result: 'edited' }))
335
336 await $.session.start(Fixtures.SESSION)
337 await $.ui.render(Fixtures.HINT)
338
339 await $.tool.call({
340 tool: 'Edit',
341 file_path: '/work/app.ts',
342 old_string: '1',
343 new_string: '2',
344 })
345
346 await world.clock.advance(Fixtures.SETTLE_MS)
347
348 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
349 })
350
351 test('on the main screen the first edit opens nothing, and /diff still opens the dialog', async ($, on) => {
352 const world = Fixtures.inRepository(on)
353
354 on('tool.call', () => ({ result: 'edited' }))
355
356 await $.session.start(Fixtures.SESSION)
357 await $.ui.render(Fixtures.MAIN_SCREEN_HINT)
358
359 await $.tool.call({
360 tool: 'Edit',
361 file_path: '/work/app.ts',
362 old_string: '1',
363 new_string: '2',
364 })
365
366 await world.clock.advance(Fixtures.SETTLE_MS)
367
368 expect(world.opened, 'a pane there would be an unasked dialog').toEqual([])
369 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
370
371 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
372
373 expect(
374 world.opened[0],
375 "the person's /diff opens the dialog, not dismisses one",
376 ).toMatchObject({ id: 'diff', focus: true })
377 })
378
379 test('a surface that does not say whether it docks a pane opens nothing at the first edit', async ($, on) => {
380 const world = Fixtures.inRepository(on)
381
382 on('tool.call', () => ({ result: 'edited' }))
383
384 await $.session.start(Fixtures.SESSION)
385 await $.ui.render(Fixtures.UNSAID_HINT)
386
387 await $.tool.call({
388 tool: 'Edit',
389 file_path: '/work/app.ts',
390 old_string: '1',
391 new_string: '2',
392 })
393
394 await world.clock.advance(Fixtures.SETTLE_MS)
395
396 expect(world.opened, 'unknown is not a dock').toEqual([])
397
398 await $.ui.render(Fixtures.HINT)
399
400 await $.tool.call({
401 tool: 'Edit',
402 file_path: '/work/app.ts',
403 old_string: '2',
404 new_string: '3',
405 })
406
407 await world.clock.advance(Fixtures.SETTLE_MS)
408
409 expect(
410 world.opened.map(pane => pane.id),
411 'once a drawing says the layout docks, the next edit opens it',
412 ).toEqual(['diff'])
413 })
414
415 test('an edit that failed or was refused opens nothing', async ($, on) => {
416 const world = Fixtures.inRepository(on)
417
418 const answers: ResultOf['tool.call'][] = [
419 { isError: true, result: 'no such text' },
420 { deny: 'not allowed' },
421 ]
422
423 const edit = () =>
424 $.tool.call({
425 tool: 'Edit',
426 file_path: '/work/app.ts',
427 old_string: '1',
428 new_string: '2',
429 })
430
431 on('tool.call', () => answers.shift() ?? { result: 'edited' })
432
433 await $.session.start(Fixtures.SESSION)
434 await $.ui.render(Fixtures.HINT)
435 await edit()
436 await edit()
437 await world.clock.advance(Fixtures.SETTLE_MS)
438
439 expect(world.opened, 'neither edit landed').toEqual([])
440 expect(world.runs, 'so nothing asked after the repository').toEqual([])
441
442 await edit()
443 await world.clock.advance(Fixtures.SETTLE_MS)
444
445 expect(
446 world.opened.map(pane => pane.id),
447 'the third did',
448 ).toEqual(['diff'])
449 })
450
451 test('/clear closes the pane it finds open', async ($, on) => {
452 const world = Fixtures.inRepository(on)
453
454 on('command.run', { command: 'clear' }, () => ({}))
455
456 await $.session.start(Fixtures.SESSION)
457 await $.command.run(Fixtures.DIFF)
458 await $.command.run(Fixtures.CLEAR)
459
460 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
461 })
462
463 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
464 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
465 const called: Args<'tool.call'>[] = []
466
467 on('tool.call', ($, e) => {
468 called.push(e)
469
470 return { result: 'called' }
471 })
472
473 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
474
475 await $.session.start(Fixtures.WORKTREE_SESSION)
476 await $.command.run(Fixtures.DIFF)
477 await world.clock.advance(Fixtures.SETTLE_MS)
478 await $.ui.render(Fixtures.PANE)
479 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
480 await world.clock.settle()
481
482 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
483
484 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
485
486 expect(armed).toContain('asked ✓')
487 expect(called).toEqual([])
488
489 expect(world.statuses.at(-2)).toBe(
490 '.env rides your next prompt (press asked ✓ to drop it)',
491 )
492
493 expect(submitted).toMatchObject({
494 context: [
495 'The user attached the diff of .env from the diff pane to this ' +
496 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
497 ],
498 })
499 })
500
501 test('an ask with no room in the context is dropped', async ($, on) => {
502 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
503 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
504 const reached: (readonly string[] | undefined)[] = []
505
506 on('prompt.submit', ($, e) => {
507 reached.push(e.context)
508
509 return { text: e.text }
510 })
511
512 await $.session.start(Fixtures.WORKTREE_SESSION)
513 await $.command.run(Fixtures.DIFF)
514 await world.clock.advance(Fixtures.SETTLE_MS)
515 await $.ui.render(Fixtures.PANE)
516 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
517 await world.clock.settle()
518
519 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
520 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
521
522 expect(reached).toEqual([[full], undefined])
523
524 expect(world.statuses.at(-1)).toBe(
525 ".env's diff did not fit in the prompt and was dropped",
526 )
527 })
528
529 test('a worktree opens on the base it kept', async ($, on) => {
530 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
531 'base:/main/wt': 'uncommitted',
532 })
533
534 await $.session.start(Fixtures.WORKTREE_SESSION)
535 await $.command.run(Fixtures.DIFF)
536 await world.clock.advance(Fixtures.SETTLE_MS)
537
538 expect(
539 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
540 "the built-in's base line under the header",
541 ).toContain('uncommitted (vs HEAD)')
542 })
543
544 test('no repository is an answer kept until /clear', async ($, on) => {
545 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
546 Fixtures.oneSecret()
547
548 const script: Record<string, string> = notYet
549 const world = Fixtures.inRepository(on, script)
550
551 const probesOf = () =>
552 world.runs.filter(run => run.argv.includes('--show-toplevel')).length
553
554 on('command.run', { command: 'clear' }, () => ({}))
555
556 await $.session.start(Fixtures.WORKTREE_SESSION)
557
558 const before = await $.command.run(Fixtures.DIFF)
559
560 script['rev-parse --path-format=absolute'] = worktree
561
562 const again = await $.command.run(Fixtures.DIFF)
563 const probesKept = probesOf()
564
565 await $.command.run(Fixtures.CLEAR)
566
567 const after = await $.command.run(Fixtures.DIFF)
568
569 await world.clock.advance(Fixtures.SETTLE_MS)
570 await $.ui.render(Fixtures.PANE)
571
572 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
573
574 const third = $.command.run(Fixtures.DIFF)
575
576 await world.clock.advance(Fixtures.SETTLE_MS)
577 await third
578
579 expect(before.text).toContain("isn't in a git repository")
580
581 expect(again.text, "kept, as the built-in's answer is").toContain(
582 "isn't in a git repository",
583 )
584
585 expect(probesKept, 'one probe answered both').toBe(1)
586 expect(after.text, '/clear forgot it').toBe('Diff panel shown')
587 expect(world.opened[0]?.id).toBe('diff')
588 expect(probesOf(), 'then one more, pinned from there on').toBe(2)
589
590 expect(
591 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
592 ).toBe(false)
593
594 expect(
595 world.runs
596 .filter(run => run.argv.includes('--numstat'))
597 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
598 ).toBe(true)
599 })
600
601 test('a repository that appears later opens on first edit', async ($, on) => {
602 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
603 Fixtures.oneSecret()
604
605 const script: Record<string, string> = notYet
606 const world = Fixtures.inRepository(on, script)
607
608 on('tool.call', () => ({ result: 'edited' }))
609
610 await $.session.start(Fixtures.WORKTREE_SESSION)
611 await $.ui.render(Fixtures.HINT)
612
613 script['rev-parse --path-format=absolute'] = worktree
614
615 await $.tool.call({
616 tool: 'Edit',
617 file_path: '/main/wt/.env',
618 old_string: 'old',
619 new_string: 'new',
620 })
621
622 await world.clock.advance(Fixtures.SETTLE_MS)
623
624 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
625 })
626
627 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
628 const runs: Args<'process.run'>[] = []
629
630 Fixtures.startsSession(on)
631
632 on('process.run', ($, e) => {
633 runs.push(e)
634
635 return e.argv.includes('--show-toplevel')
636 ? { deny: Fixtures.FAILED_START }
637 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
638 })
639
640 await $.session.start(Fixtures.WORKTREE_SESSION)
641
642 const { text } = await $.command.run(Fixtures.DIFF)
643 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
644
645 expect(text).toContain("isn't in a git repository")
646 expect(probes, "this /diff's alone; no retry").toHaveLength(1)
647 })
648
649 test('/diff on a narrow terminal: the resize line', async ($, on) => {
650 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
651
652 await $.session.start(Fixtures.WORKTREE_SESSION)
653
654 const narrow = await $.command.run(
655 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
656 )
657
658 const openedNarrow = [...world.opened]
659
660 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
661
662 expect(narrow.text).toContain(
663 'Resize your terminal to at least 110 columns to show the diff panel',
664 )
665
666 expect(openedNarrow).toEqual([])
667 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
668 })
669})