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
902 lines · 25.0 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("only the main loop's edit opens the pane", async ($, on) => {
452 const world = Fixtures.inRepository(on)
453
454 const edit = (agentId?: string) =>
455 $.tool.call({
456 tool: 'Write',
457 file_path: '/work/app.ts',
458 content: '2',
459 ...(agentId !== undefined && { agentId }),
460 })
461
462 on('tool.call', () => ({ result: 'written' }))
463
464 await $.session.start(Fixtures.SESSION)
465 await $.ui.render(Fixtures.HINT)
466 await edit('a-explorer')
467 await world.clock.advance(Fixtures.SETTLE_MS)
468
469 expect(world.opened, 'the built-in checkpoints no subagent edit').toEqual(
470 [],
471 )
472
473 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
474
475 await edit()
476 await world.clock.advance(Fixtures.SETTLE_MS)
477
478 expect(
479 world.opened.map(pane => pane.id),
480 "the main loop's",
481 ).toEqual(['diff'])
482 })
483
484 test('checkpointing off in settings: edits open nothing', async ($, on) => {
485 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
486 settings: { fileCheckpointingEnabled: false },
487 })
488
489 on('tool.call', () => ({ result: 'edited' }))
490
491 await $.session.start(Fixtures.SESSION)
492 await $.ui.render(Fixtures.HINT)
493
494 await $.tool.call({
495 tool: 'Edit',
496 file_path: '/work/app.ts',
497 old_string: '1',
498 new_string: '2',
499 })
500
501 await world.clock.advance(Fixtures.SETTLE_MS)
502
503 expect(world.opened, 'the built-in opens on a checkpoint alone').toEqual([])
504
505 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
506
507 expect(await $.command.run(Fixtures.DIFF)).toEqual({
508 text: 'Diff panel shown',
509 })
510 })
511
512 test('checkpointing off by variable: edits open nothing', async ($, on) => {
513 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
514 env: { CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: 'true' },
515 })
516
517 on('tool.call', () => ({ result: 'edited' }))
518
519 await $.session.start(Fixtures.SESSION)
520 await $.ui.render(Fixtures.HINT)
521
522 await $.tool.call({
523 tool: 'Edit',
524 file_path: '/work/app.ts',
525 old_string: '1',
526 new_string: '2',
527 })
528
529 await world.clock.advance(Fixtures.SETTLE_MS)
530
531 expect(world.opened).toEqual([])
532 })
533
534 test('an open left waiting is withdrawn; /diff opens it', async ($, on) => {
535 let isNarrow = true
536
537 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
538 stored: { [Names.STORE_OPEN_KEY]: true },
539 isLeftWaiting: () => isNarrow,
540 })
541
542 const edit = () =>
543 $.tool.call({
544 tool: 'Edit',
545 file_path: '/work/app.ts',
546 old_string: '1',
547 new_string: '2',
548 })
549
550 on('tool.call', () => ({ result: 'edited' }))
551
552 await $.session.start(Fixtures.SESSION)
553 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
554 await edit()
555 await world.clock.advance(Fixtures.SETTLE_MS)
556
557 expect(
558 world.waiting.map(pane => pane.id),
559 'the edit opened',
560 ).toEqual(['diff'])
561
562 expect(
563 world.closed.map(pane => pane.id),
564 'and withdrew the pane the engine left waiting, so no resize seats it',
565 ).toEqual(['diff'])
566
567 expect(
568 world.runs.map(run => Fixtures.gitWordOf(run.argv)).slice(0, 2),
569 'the repository was found and read once before the open, as the ' +
570 'built-in primes; nothing polls for a pane no one sees',
571 ).toEqual(['rev-parse --show-toplevel', 'status'])
572
573 expect(
574 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
575 'no HEAD poll for a withdrawn pane',
576 ).not.toContain(Fixtures.POLL_WORD)
577
578 isNarrow = false
579
580 expect(
581 await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)),
582 '/diff opens it, where a pane believed open would have been hidden',
583 ).toEqual({ text: 'Diff panel shown' })
584
585 await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
586 await edit()
587 await world.clock.advance(Fixtures.SETTLE_MS)
588
589 expect(
590 world.opened.map(pane => pane.id),
591 'closed by hand, no edit opens it again',
592 ).toEqual(['diff'])
593 })
594
595 test('an unplaced open is tried again at the next edit', async ($, on) => {
596 let isNarrow = true
597
598 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
599 stored: { [Names.STORE_OPEN_KEY]: true },
600 isLeftWaiting: () => isNarrow,
601 })
602
603 const edit = () =>
604 $.tool.call({
605 tool: 'Edit',
606 file_path: '/work/app.ts',
607 old_string: '1',
608 new_string: '2',
609 })
610
611 on('tool.call', () => ({ result: 'edited' }))
612
613 await $.session.start(Fixtures.SESSION)
614 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
615 await edit()
616 await world.clock.advance(Fixtures.SETTLE_MS)
617
618 expect(world.opened, 'the first try waited and was withdrawn').toEqual([])
619
620 isNarrow = false
621
622 await edit()
623 await world.clock.advance(Fixtures.SETTLE_MS)
624
625 expect(
626 world.opened.map(pane => pane.id),
627 'the next edit opened it, as the built-in reads the width again then',
628 ).toEqual(['diff'])
629 })
630
631 test('a docked pane opens once its first fetch settled', async ($, on) => {
632 const opened: string[] = []
633 const clock = Fixtures.startsSession(on)
634
635 on('process.run', async ($, e) => {
636 if (e.argv.includes('--shortstat')) {
637 await clock.sleep(Fixtures.SLOW_DIFF_MS)
638 }
639
640 return { value: Fixtures.gitIn(e.argv) }
641 })
642
643 on('ui.open', ($, e) => {
644 opened.push(e.id)
645
646 return { value: undefined }
647 })
648
649 on('ui.close', () => ({ value: undefined }))
650 on('ui.invalidate', () => ({ value: undefined }))
651 on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN)
652 on('session.messages', () => ({ value: [] }))
653 on('settings.read', () => ({ value: {} }))
654 on('tool.call', () => ({ result: 'edited' }))
655 mock.store(on)
656 mock.env(on, {})
657
658 await $.session.start(Fixtures.SESSION)
659 await $.ui.render(Fixtures.HINT)
660
661 await $.tool.call({
662 tool: 'Edit',
663 file_path: '/work/app.ts',
664 old_string: '1',
665 new_string: '2',
666 })
667
668 await clock.advance(Fixtures.SLOW_DIFF_MS - 1)
669
670 expect(opened, 'git has not answered: no pane, no Loading frame').toEqual(
671 [],
672 )
673
674 await clock.advance(Fixtures.SETTLE_MS)
675
676 expect(opened, 'the fetch settled: the pane opens filled').toEqual(['diff'])
677
678 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
679
680 expect(drawn).toContain('1 file changed')
681 expect(drawn).not.toContain('Loading diff')
682 })
683
684 test('/clear closes the pane it finds open', async ($, on) => {
685 const world = Fixtures.inRepository(on)
686
687 on('command.run', { command: 'clear' }, () => ({}))
688
689 await $.session.start(Fixtures.SESSION)
690 await $.command.run(Fixtures.DIFF)
691 await $.command.run(Fixtures.CLEAR)
692
693 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
694 })
695
696 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
697 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
698 const called: Args<'tool.call'>[] = []
699
700 on('tool.call', ($, e) => {
701 called.push(e)
702
703 return { result: 'called' }
704 })
705
706 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
707
708 await $.session.start(Fixtures.WORKTREE_SESSION)
709 await $.command.run(Fixtures.DIFF)
710 await world.clock.advance(Fixtures.SETTLE_MS)
711 await $.ui.render(Fixtures.PANE)
712 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
713 await world.clock.settle()
714
715 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
716
717 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
718
719 expect(armed).toContain('asked ✓')
720 expect(called).toEqual([])
721
722 expect(world.statuses.at(-2)).toBe(
723 '.env rides your next prompt (press asked ✓ to drop it)',
724 )
725
726 expect(submitted).toMatchObject({
727 context: [
728 'The user attached the diff of .env from the diff pane to this ' +
729 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
730 ],
731 })
732 })
733
734 test('an ask with no room in the context is dropped', async ($, on) => {
735 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
736 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
737 const reached: (readonly string[] | undefined)[] = []
738
739 on('prompt.submit', ($, e) => {
740 reached.push(e.context)
741
742 return { text: e.text }
743 })
744
745 await $.session.start(Fixtures.WORKTREE_SESSION)
746 await $.command.run(Fixtures.DIFF)
747 await world.clock.advance(Fixtures.SETTLE_MS)
748 await $.ui.render(Fixtures.PANE)
749 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
750 await world.clock.settle()
751
752 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
753 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
754
755 expect(reached).toEqual([[full], undefined])
756
757 expect(world.statuses.at(-1)).toBe(
758 ".env's diff did not fit in the prompt and was dropped",
759 )
760 })
761
762 test('a worktree opens on the base it kept', async ($, on) => {
763 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
764 stored: { 'base:/main/wt': 'uncommitted' },
765 })
766
767 await $.session.start(Fixtures.WORKTREE_SESSION)
768 await $.command.run(Fixtures.DIFF)
769 await world.clock.advance(Fixtures.SETTLE_MS)
770
771 expect(
772 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
773 "the built-in's base line under the header",
774 ).toContain('uncommitted (vs HEAD)')
775 })
776
777 test('no repository is an answer kept until /clear', async ($, on) => {
778 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
779 Fixtures.oneSecret()
780
781 const script: Record<string, string> = notYet
782 const world = Fixtures.inRepository(on, script)
783
784 const probesOf = () =>
785 world.runs.filter(run => run.argv.includes('--show-toplevel')).length
786
787 on('command.run', { command: 'clear' }, () => ({}))
788
789 await $.session.start(Fixtures.WORKTREE_SESSION)
790
791 const before = await $.command.run(Fixtures.DIFF)
792
793 script['rev-parse --path-format=absolute'] = worktree
794
795 const again = await $.command.run(Fixtures.DIFF)
796 const probesKept = probesOf()
797
798 await $.command.run(Fixtures.CLEAR)
799
800 const after = await $.command.run(Fixtures.DIFF)
801
802 await world.clock.advance(Fixtures.SETTLE_MS)
803 await $.ui.render(Fixtures.PANE)
804
805 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
806
807 const third = $.command.run(Fixtures.DIFF)
808
809 await world.clock.advance(Fixtures.SETTLE_MS)
810 await third
811
812 expect(before.text).toContain("isn't in a git repository")
813
814 expect(again.text, "kept, as the built-in's answer is").toContain(
815 "isn't in a git repository",
816 )
817
818 expect(probesKept, 'one probe answered both').toBe(1)
819 expect(after.text, '/clear forgot it').toBe('Diff panel shown')
820 expect(world.opened[0]?.id).toBe('diff')
821 expect(probesOf(), 'then one more, pinned from there on').toBe(2)
822
823 expect(
824 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
825 ).toBe(false)
826
827 expect(
828 world.runs
829 .filter(run => run.argv.includes('--numstat'))
830 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
831 ).toBe(true)
832 })
833
834 test('a repository that appears later opens on first edit', async ($, on) => {
835 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
836 Fixtures.oneSecret()
837
838 const script: Record<string, string> = notYet
839 const world = Fixtures.inRepository(on, script)
840
841 on('tool.call', () => ({ result: 'edited' }))
842
843 await $.session.start(Fixtures.WORKTREE_SESSION)
844 await $.ui.render(Fixtures.HINT)
845
846 script['rev-parse --path-format=absolute'] = worktree
847
848 await $.tool.call({
849 tool: 'Edit',
850 file_path: '/main/wt/.env',
851 old_string: 'old',
852 new_string: 'new',
853 })
854
855 await world.clock.advance(Fixtures.SETTLE_MS)
856
857 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
858 })
859
860 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
861 const runs: Args<'process.run'>[] = []
862
863 Fixtures.startsSession(on)
864
865 on('process.run', ($, e) => {
866 runs.push(e)
867
868 return e.argv.includes('--show-toplevel')
869 ? { deny: Fixtures.FAILED_START }
870 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
871 })
872
873 await $.session.start(Fixtures.WORKTREE_SESSION)
874
875 const { text } = await $.command.run(Fixtures.DIFF)
876 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
877
878 expect(text).toContain("isn't in a git repository")
879 expect(probes, "this /diff's alone; no retry").toHaveLength(1)
880 })
881
882 test('/diff on a narrow terminal: the resize line', async ($, on) => {
883 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
884
885 await $.session.start(Fixtures.WORKTREE_SESSION)
886
887 const narrow = await $.command.run(
888 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
889 )
890
891 const openedNarrow = [...world.opened]
892
893 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
894
895 expect(narrow.text).toContain(
896 'Resize your terminal to at least 110 columns to show the diff panel',
897 )
898
899 expect(openedNarrow).toEqual([])
900 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
901 })
902})