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
1,556 lines · 43.2 KBRaw
1import type { Args, ResultOf, SessionMessage } 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('an edit outside the repository opens nothing, the next one inside does', async ($, on) => {
452 const world = Fixtures.inRepository(on)
453
454 on('tool.call', () => ({ result: 'written' }))
455
456 await $.session.start(Fixtures.SESSION)
457 await $.ui.render(Fixtures.HINT)
458
459 await $.tool.call({
460 tool: 'Write',
461 file_path: '/tmp/notes.md',
462 content: 'a note',
463 })
464
465 await $.tool.call({
466 tool: 'NotebookEdit',
467 notebook_path: '../elsewhere/plot.ipynb',
468 new_source: '1',
469 })
470
471 await world.clock.advance(Fixtures.SETTLE_MS)
472
473 expect(world.opened, 'no row could show either file').toEqual([])
474
475 expect(
476 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
477 'the repository was found, its working tree never read',
478 ).toEqual(['rev-parse --show-toplevel'])
479
480 await $.tool.call({
481 tool: 'Write',
482 file_path: 'app.ts',
483 content: 'const a = 2',
484 })
485
486 await world.clock.advance(Fixtures.SETTLE_MS)
487
488 expect(
489 world.opened.map(pane => pane.id),
490 "a path from the session's directory, inside: the first edit still",
491 ).toEqual(['diff'])
492 })
493
494 test('a directory beside the repository that starts with its name is outside', async ($, on) => {
495 const world = Fixtures.inRepository(on)
496
497 on('tool.call', () => ({ result: 'written' }))
498
499 await $.session.start(Fixtures.SESSION)
500 await $.ui.render(Fixtures.HINT)
501
502 await $.tool.call({
503 tool: 'Write',
504 file_path: '/workshop/a.ts',
505 content: 'const a = 1',
506 })
507
508 await world.clock.advance(Fixtures.SETTLE_MS)
509
510 expect(world.opened, '/workshop is not under /work').toEqual([])
511 })
512
513 test('an edit the diff lists no row for opens nothing, a later one it lists does', async ($, on) => {
514 const script: Record<string, string> = {
515 ...Fixtures.REPOSITORY,
516 'HEAD --shortstat': '',
517 'HEAD --numstat': '',
518 }
519
520 const world = Fixtures.inRepository(on, script)
521
522 const fetches = () =>
523 world.runs.filter(run => run.argv.includes('--numstat')).length
524
525 const edit = (path: string) =>
526 $.tool.call({
527 tool: 'Edit',
528 file_path: path,
529 old_string: '1',
530 new_string: '2',
531 })
532
533 on('tool.call', () => ({ result: 'edited' }))
534
535 await $.session.start(Fixtures.SESSION)
536 await $.ui.render(Fixtures.HINT)
537 await edit('/work/ignored/cache.json')
538 await world.clock.advance(Fixtures.SETTLE_MS)
539
540 expect(world.opened, 'it would say "No changes this session"').toEqual([])
541 expect(fetches(), 'one fetch said so').toBe(1)
542
543 await edit('/work/ignored/cache.json')
544 await world.clock.advance(Fixtures.SETTLE_MS)
545
546 expect(world.opened, 'nor the second time').toEqual([])
547 expect(fetches(), 'a fetch an edit, no more').toBe(2)
548
549 Object.assign(script, Fixtures.REPOSITORY)
550
551 await edit('/work/app.ts')
552 await world.clock.advance(Fixtures.SETTLE_MS)
553
554 expect(
555 world.opened.map(pane => pane.id),
556 'the first edit with a row to list',
557 ).toEqual(['diff'])
558
559 expect(fetches(), 'opened on the fetch that found it').toBe(3)
560 })
561
562 test('a diff git could not read, or no tracked row with the untracked withheld, opens nothing', async ($, on) => {
563 const script: Record<string, string> = { ...Fixtures.REPOSITORY }
564 const world = Fixtures.inRepository(on, script)
565
566 const edit = () =>
567 $.tool.call({
568 tool: 'Edit',
569 file_path: '/work/app.ts',
570 old_string: '1',
571 new_string: '2',
572 })
573
574 on('tool.call', () => ({ result: 'edited' }))
575
576 delete script['HEAD --numstat']
577 delete script['ls-files']
578
579 await $.session.start(Fixtures.SESSION)
580 await $.ui.render(Fixtures.HINT)
581 await edit()
582 await world.clock.advance(Fixtures.SETTLE_MS)
583
584 expect(world.opened, 'it would say "Diff unavailable"').toEqual([])
585
586 script['HEAD --shortstat'] = ''
587 script['HEAD --numstat'] = ''
588
589 await edit()
590 await world.clock.advance(Fixtures.SETTLE_MS)
591
592 expect(world.opened, 'it would say "No tracked changes"').toEqual([])
593
594 Object.assign(script, Fixtures.REPOSITORY)
595
596 await edit()
597 await world.clock.advance(Fixtures.SETTLE_MS)
598
599 expect(
600 world.opened.map(pane => pane.id),
601 'read whole, with a row: open',
602 ).toEqual(['diff'])
603 })
604
605 test('the first edit opens the pane on the one fetch that found its rows', async ($, on) => {
606 const runs: Args<'process.run'>[] = []
607 const drawnAtOpen: string[] = []
608 const clock = Fixtures.startsSession(on)
609
610 on('process.run', (_engine, e) => {
611 runs.push(e)
612
613 return { value: Fixtures.gitIn(e.argv) }
614 })
615
616 on('ui.open', async () => {
617 drawnAtOpen.push(Fixtures.textOf(await $.ui.render(Fixtures.PANE)))
618
619 return { value: undefined }
620 })
621
622 on('ui.invalidate', () => ({ value: undefined }))
623 on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN)
624 on('session.messages', () => ({ value: [] }))
625 on('tool.call', () => ({ result: 'edited' }))
626 mock.store(on, {})
627
628 await $.session.start(Fixtures.SESSION)
629 await $.ui.render(Fixtures.HINT)
630
631 await $.tool.call({
632 tool: 'Edit',
633 file_path: '/work/app.ts',
634 old_string: '1',
635 new_string: '2',
636 })
637
638 await clock.advance(Fixtures.SETTLE_MS)
639
640 expect(drawnAtOpen, 'one pane, listing as it opens').toEqual([
641 expect.stringContaining('1 file changed'),
642 ])
643
644 expect(
645 runs
646 .map(run => Fixtures.gitWordOf(run.argv))
647 .filter(word => word !== Fixtures.POLL_WORD),
648 "/diff's spawns, once",
649 ).toEqual([
650 'rev-parse --show-toplevel',
651 'status',
652 'diff --shortstat',
653 'diff --numstat',
654 'ls-files',
655 'diff -- app.ts',
656 ])
657 })
658
659 test('edits landing together open one pane', async ($, on) => {
660 const world = Fixtures.inRepository(on)
661
662 const edit = () =>
663 $.tool.call({
664 tool: 'Edit',
665 file_path: '/work/app.ts',
666 old_string: '1',
667 new_string: '2',
668 })
669
670 on('tool.call', () => ({ result: 'edited' }))
671
672 await $.session.start(Fixtures.SESSION)
673 await $.ui.render(Fixtures.HINT)
674 await Promise.all([edit(), edit(), edit()])
675 await world.clock.advance(Fixtures.SETTLE_MS)
676
677 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
678
679 expect(
680 world.runs.filter(run => run.argv.includes('--numstat')),
681 'on one fetch',
682 ).toHaveLength(1)
683 })
684
685 test('an edit or a command that lands while the diff is read is read too', async ($, on) => {
686 const script: Record<string, string> = {
687 ...Fixtures.REPOSITORY,
688 'HEAD --shortstat': '',
689 'HEAD --numstat': '',
690 }
691
692 const world = Fixtures.inSlowRepository(on, script)
693 const openedIds = () => world.opened.map(pane => pane.id)
694
695 const edit = (path: string) =>
696 $.tool.call({
697 tool: 'Edit',
698 file_path: path,
699 old_string: '1',
700 new_string: '2',
701 })
702
703 on('tool.call', () => ({ result: 'edited' }))
704
705 await $.session.start(Fixtures.SESSION)
706 await $.ui.render(Fixtures.HINT)
707 await edit('/work/ignored/cache.json')
708 await world.clock.settle()
709
710 Object.assign(script, Fixtures.REPOSITORY)
711
712 await edit('/work/app.ts')
713 await world.clock.settle()
714
715 expect(world.reads, 'the second edit began no read of its own').toEqual([0])
716
717 await world.clock.advance(1)
718
719 expect(openedIds(), 'the first read had found nothing').toEqual([])
720 expect(world.reads, 'so the edit it missed is read').toEqual([0, 1])
721
722 await $.tool.call({ tool: 'Bash', command: 'make' })
723 await world.clock.advance(1)
724
725 expect(openedIds(), 'that read lists a file').toEqual(['diff'])
726
727 await world.clock.advance(Fixtures.SETTLE_MS)
728
729 expect(
730 world.reads,
731 'and the pane, open, reads what the command may have changed since',
732 ).toHaveLength(3)
733 })
734
735 test('a read that /clear overtakes opens nothing, the next edit reads again', async ($, on) => {
736 const world = Fixtures.inSlowRepository(on)
737
738 const edit = () =>
739 $.tool.call({
740 tool: 'Edit',
741 file_path: '/work/app.ts',
742 old_string: '1',
743 new_string: '2',
744 })
745
746 on('tool.call', () => ({ result: 'edited' }))
747 on('command.run', { command: 'clear' }, () => ({}))
748
749 await $.session.start(Fixtures.SESSION)
750 await $.ui.render(Fixtures.HINT)
751 await edit()
752 await world.clock.settle()
753 await $.command.run(Fixtures.CLEAR)
754 await world.clock.advance(Fixtures.SETTLE_MS)
755
756 expect(world.reads, 'a read was under way at /clear').toEqual([0])
757 expect(world.opened, 'it read for the conversation before').toEqual([])
758
759 await edit()
760 await world.clock.advance(Fixtures.SETTLE_MS)
761
762 expect(
763 world.opened.map(pane => pane.id),
764 "the new conversation's first edit",
765 ).toEqual(['diff'])
766 })
767
768 test('a pane the person hid while the diff was read stays hidden', async ($, on) => {
769 const world = Fixtures.inSlowRepository(on)
770
771 on('tool.call', () => ({ result: 'edited' }))
772
773 await $.session.start(Fixtures.SESSION)
774 await $.ui.render(Fixtures.HINT)
775
776 await $.tool.call({
777 tool: 'Edit',
778 file_path: '/work/app.ts',
779 old_string: '1',
780 new_string: '2',
781 })
782
783 await world.clock.settle()
784
785 expect(world.reads, "the edit's read is under way").toEqual([0])
786
787 const shown = $.command.run(Fixtures.DIFF)
788
789 await world.clock.advance(1)
790
791 expect(
792 await shown,
793 '/diff reads before it opens, as the edit does',
794 ).toEqual({ text: 'Diff panel shown' })
795
796 expect(await $.command.run(Fixtures.DIFF)).toEqual({
797 text: 'Diff panel hidden',
798 })
799
800 await world.clock.advance(Fixtures.SETTLE_MS)
801
802 expect(
803 world.opened.map(pane => pane.id),
804 "/diff's open alone: the read gave way to the person's close",
805 ).toEqual(['diff'])
806 })
807
808 test('nor does a command landing in that read open it again', async ($, on) => {
809 const world = Fixtures.inSlowRepository(on)
810
811 on('tool.call', () => ({ result: 'done' }))
812
813 await $.session.start(Fixtures.SESSION)
814 await $.ui.render(Fixtures.HINT)
815
816 await $.tool.call({
817 tool: 'Edit',
818 file_path: '/work/app.ts',
819 old_string: '1',
820 new_string: '2',
821 })
822
823 await world.clock.settle()
824
825 const shown = $.command.run(Fixtures.DIFF)
826
827 await world.clock.advance(1)
828 await shown
829 await $.command.run(Fixtures.DIFF)
830 await $.tool.call({ tool: 'Bash', command: 'make' })
831 await world.clock.advance(Fixtures.SETTLE_MS)
832
833 expect(
834 world.opened.map(pane => pane.id),
835 'the attempt stood down; it did not read again for what landed',
836 ).toEqual(['diff'])
837 })
838
839 test('/diff typed while the diff is read opens the one pane', async ($, on) => {
840 const world = Fixtures.inSlowRepository(on)
841
842 on('tool.call', () => ({ result: 'edited' }))
843
844 await $.session.start(Fixtures.SESSION)
845 await $.ui.render(Fixtures.HINT)
846
847 await $.tool.call({
848 tool: 'Edit',
849 file_path: '/work/app.ts',
850 old_string: '1',
851 new_string: '2',
852 })
853
854 await world.clock.settle()
855
856 const shown = $.command.run(Fixtures.DIFF)
857
858 await world.clock.advance(Fixtures.SETTLE_MS)
859 await shown
860
861 expect(
862 world.opened.map(pane => pane.id),
863 "/diff's, not a second",
864 ).toEqual(['diff'])
865 })
866
867 test('a terminal narrowed while the diff is read opens nothing', async ($, on) => {
868 const world = Fixtures.inSlowRepository(on)
869
870 on('tool.call', () => ({ result: 'edited' }))
871
872 await $.session.start(Fixtures.SESSION)
873 await $.ui.render(Fixtures.HINT)
874
875 await $.tool.call({
876 tool: 'Edit',
877 file_path: '/work/app.ts',
878 old_string: '1',
879 new_string: '2',
880 })
881
882 await world.clock.settle()
883 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
884 await world.clock.advance(Fixtures.SETTLE_MS)
885
886 expect(world.reads, 'a read was under way as it narrowed').toEqual([0])
887 expect(world.opened, 'the room it read for is gone').toEqual([])
888 })
889
890 test('nor does a command landing in that read have it read again, the next edit with room does', async ($, on) => {
891 const world = Fixtures.inSlowRepository(on)
892
893 const edit = () =>
894 $.tool.call({
895 tool: 'Edit',
896 file_path: '/work/app.ts',
897 old_string: '1',
898 new_string: '2',
899 })
900
901 on('tool.call', () => ({ result: 'done' }))
902
903 await $.session.start(Fixtures.SESSION)
904 await $.ui.render(Fixtures.HINT)
905 await edit()
906 await world.clock.settle()
907 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
908 await $.tool.call({ tool: 'Bash', command: 'make' })
909 await world.clock.advance(Fixtures.SETTLE_MS)
910
911 expect(
912 world.reads,
913 'the attempt gave way with the room; it read for nothing more',
914 ).toEqual([0])
915
916 expect(world.opened).toEqual([])
917
918 await $.ui.render(Fixtures.HINT)
919 await edit()
920 await world.clock.advance(Fixtures.SETTLE_MS)
921
922 expect(
923 world.opened.map(pane => pane.id),
924 'wide again, the next edit starts an attempt of its own',
925 ).toEqual(['diff'])
926 })
927
928 test("only the main loop's edit opens the pane", async ($, on) => {
929 const world = Fixtures.inRepository(on)
930
931 const edit = (agentId?: string) =>
932 $.tool.call({
933 tool: 'Write',
934 file_path: '/work/app.ts',
935 content: '2',
936 ...(agentId !== undefined && { agentId }),
937 })
938
939 on('tool.call', () => ({ result: 'written' }))
940
941 await $.session.start(Fixtures.SESSION)
942 await $.ui.render(Fixtures.HINT)
943 await edit('a-explorer')
944 await world.clock.advance(Fixtures.SETTLE_MS)
945
946 expect(world.opened, 'the built-in checkpoints no subagent edit').toEqual(
947 [],
948 )
949
950 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
951
952 await edit()
953 await world.clock.advance(Fixtures.SETTLE_MS)
954
955 expect(
956 world.opened.map(pane => pane.id),
957 "the main loop's",
958 ).toEqual(['diff'])
959 })
960
961 test('checkpointing off in settings: edits open nothing', async ($, on) => {
962 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
963 settings: { fileCheckpointingEnabled: false },
964 })
965
966 on('tool.call', () => ({ result: 'edited' }))
967
968 await $.session.start(Fixtures.SESSION)
969 await $.ui.render(Fixtures.HINT)
970
971 await $.tool.call({
972 tool: 'Edit',
973 file_path: '/work/app.ts',
974 old_string: '1',
975 new_string: '2',
976 })
977
978 await world.clock.advance(Fixtures.SETTLE_MS)
979
980 expect(world.opened, 'the built-in opens on a checkpoint alone').toEqual([])
981
982 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
983
984 expect(await $.command.run(Fixtures.DIFF)).toEqual({
985 text: 'Diff panel shown',
986 })
987 })
988
989 test('checkpointing off by variable: edits open nothing', async ($, on) => {
990 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
991 env: { CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: 'true' },
992 })
993
994 on('tool.call', () => ({ result: 'edited' }))
995
996 await $.session.start(Fixtures.SESSION)
997 await $.ui.render(Fixtures.HINT)
998
999 await $.tool.call({
1000 tool: 'Edit',
1001 file_path: '/work/app.ts',
1002 old_string: '1',
1003 new_string: '2',
1004 })
1005
1006 await world.clock.advance(Fixtures.SETTLE_MS)
1007
1008 expect(world.opened).toEqual([])
1009 })
1010
1011 test('an open left waiting is withdrawn; /diff opens it', async ($, on) => {
1012 let isNarrow = true
1013
1014 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1015 stored: { [Names.STORE_OPEN_KEY]: true },
1016 isLeftWaiting: () => isNarrow,
1017 })
1018
1019 const edit = () =>
1020 $.tool.call({
1021 tool: 'Edit',
1022 file_path: '/work/app.ts',
1023 old_string: '1',
1024 new_string: '2',
1025 })
1026
1027 on('tool.call', () => ({ result: 'edited' }))
1028
1029 await $.session.start(Fixtures.SESSION)
1030 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
1031 await edit()
1032 await world.clock.advance(Fixtures.SETTLE_MS)
1033
1034 expect(
1035 world.waiting.map(pane => pane.id),
1036 'the edit opened',
1037 ).toEqual(['diff'])
1038
1039 expect(
1040 world.closed.map(pane => pane.id),
1041 'and withdrew the pane the engine left waiting, so no resize seats it',
1042 ).toEqual(['diff'])
1043
1044 expect(
1045 world.runs.map(run => Fixtures.gitWordOf(run.argv)).slice(0, 2),
1046 'the repository was found and read once before the open, as the ' +
1047 'built-in primes; nothing polls for a pane no one sees',
1048 ).toEqual(['rev-parse --show-toplevel', 'status'])
1049
1050 expect(
1051 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
1052 'the repository was found and read once to decide, no hunks fetched ' +
1053 'for a pane no one sees',
1054 ).toEqual([
1055 'rev-parse --show-toplevel',
1056 'status',
1057 'diff --shortstat',
1058 'diff --numstat',
1059 'ls-files',
1060 ])
1061
1062 expect(
1063 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
1064 'no HEAD poll for a withdrawn pane',
1065 ).not.toContain(Fixtures.POLL_WORD)
1066
1067 isNarrow = false
1068
1069 expect(
1070 await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)),
1071 '/diff opens it, where a pane believed open would have been hidden',
1072 ).toEqual({ text: 'Diff panel shown' })
1073
1074 await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
1075 await edit()
1076 await world.clock.advance(Fixtures.SETTLE_MS)
1077
1078 expect(
1079 world.opened.map(pane => pane.id),
1080 'closed by hand, no edit opens it again',
1081 ).toEqual(['diff'])
1082 })
1083
1084 test('an unplaced open is tried again at the next edit', async ($, on) => {
1085 let isNarrow = true
1086
1087 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1088 stored: { [Names.STORE_OPEN_KEY]: true },
1089 isLeftWaiting: () => isNarrow,
1090 })
1091
1092 const edit = () =>
1093 $.tool.call({
1094 tool: 'Edit',
1095 file_path: '/work/app.ts',
1096 old_string: '1',
1097 new_string: '2',
1098 })
1099
1100 on('tool.call', () => ({ result: 'edited' }))
1101
1102 await $.session.start(Fixtures.SESSION)
1103 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
1104 await edit()
1105 await world.clock.advance(Fixtures.SETTLE_MS)
1106
1107 expect(world.opened, 'the first try waited and was withdrawn').toEqual([])
1108
1109 isNarrow = false
1110
1111 await edit()
1112 await world.clock.advance(Fixtures.SETTLE_MS)
1113
1114 expect(
1115 world.opened.map(pane => pane.id),
1116 'the next edit opened it, as the built-in reads the width again then',
1117 ).toEqual(['diff'])
1118 })
1119
1120 test('a docked pane opens once its first fetch settled', async ($, on) => {
1121 const opened: string[] = []
1122 const clock = Fixtures.startsSession(on)
1123
1124 on('process.run', async ($, e) => {
1125 if (e.argv.includes('--shortstat')) {
1126 await clock.sleep(Fixtures.SLOW_DIFF_MS)
1127 }
1128
1129 return { value: Fixtures.gitIn(e.argv) }
1130 })
1131
1132 on('ui.open', ($, e) => {
1133 opened.push(e.id)
1134
1135 return { value: undefined }
1136 })
1137
1138 on('ui.close', () => ({ value: undefined }))
1139 on('ui.invalidate', () => ({ value: undefined }))
1140 on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN)
1141 on('session.messages', () => ({ value: [] }))
1142 on('settings.read', () => ({ value: {} }))
1143 on('tool.call', () => ({ result: 'edited' }))
1144 mock.store(on)
1145 mock.env(on, {})
1146
1147 await $.session.start(Fixtures.SESSION)
1148 await $.ui.render(Fixtures.HINT)
1149
1150 await $.tool.call({
1151 tool: 'Edit',
1152 file_path: '/work/app.ts',
1153 old_string: '1',
1154 new_string: '2',
1155 })
1156
1157 await clock.advance(Fixtures.SLOW_DIFF_MS - 1)
1158
1159 expect(opened, 'git has not answered: no pane, no Loading frame').toEqual(
1160 [],
1161 )
1162
1163 await clock.advance(Fixtures.SETTLE_MS)
1164
1165 expect(opened, 'the fetch settled: the pane opens filled').toEqual(['diff'])
1166
1167 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
1168
1169 expect(drawn).toContain('1 file changed')
1170 expect(drawn).not.toContain('Loading diff')
1171 })
1172
1173 test('/clear leaves the pane it finds open up and reads the repository afresh', async ($, on) => {
1174 const world = Fixtures.inRepository(on)
1175
1176 on('command.run', { command: 'clear' }, () => ({}))
1177
1178 await $.session.start(Fixtures.SESSION)
1179 await $.command.run(Fixtures.DIFF)
1180 await world.clock.advance(Fixtures.SETTLE_MS)
1181
1182 const read = world.runs.length
1183
1184 await $.command.run(Fixtures.CLEAR)
1185 await world.clock.advance(Fixtures.SETTLE_MS)
1186
1187 expect(world.closed, 'as the built-in panel stays across /clear').toEqual(
1188 [],
1189 )
1190
1191 expect(
1192 world.runs.slice(read).map(run => Fixtures.gitWordOf(run.argv))[0],
1193 'the repository found again for the conversation that starts over',
1194 ).toBe('rev-parse --show-toplevel')
1195
1196 expect(
1197 Fixtures.textOf(await $.ui.render(Fixtures.PANE)),
1198 'drawn from the fresh read',
1199 ).toContain('1 file changed')
1200 })
1201
1202 test('a resumed session whose turns edited opens the pane before any new edit', async ($, on) => {
1203 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1204 messages: () => Fixtures.EDITED_TRANSCRIPT,
1205 })
1206
1207 await $.session.start(Fixtures.SESSION)
1208
1209 expect(world.runs, 'the start itself still runs no git').toEqual([])
1210
1211 await $.ui.render(Fixtures.HINT)
1212 await world.clock.advance(Fixtures.SETTLE_MS)
1213
1214 expect(
1215 world.opened.map(pane => pane.id),
1216 'open once the width is known, as the built-in opens on the restore',
1217 ).toEqual(['diff'])
1218 })
1219
1220 test('a resumed session whose edits the diff no longer lists opens nothing, the next edit it lists does', async ($, on) => {
1221 const script: Record<string, string> = {
1222 ...Fixtures.REPOSITORY,
1223 'HEAD --shortstat': '',
1224 'HEAD --numstat': '',
1225 }
1226
1227 const world = Fixtures.inRepository(on, script, {
1228 messages: () => Fixtures.EDITED_TRANSCRIPT,
1229 })
1230
1231 on('tool.call', () => ({ result: 'edited' }))
1232
1233 await $.session.start(Fixtures.SESSION)
1234 await $.ui.render(Fixtures.HINT)
1235 await world.clock.advance(Fixtures.SETTLE_MS)
1236
1237 expect(
1238 world.opened,
1239 'committed since: it would say "No changes this session"',
1240 ).toEqual([])
1241
1242 Object.assign(script, Fixtures.REPOSITORY)
1243
1244 await $.tool.call({
1245 tool: 'Edit',
1246 file_path: '/work/app.ts',
1247 old_string: '1',
1248 new_string: '2',
1249 })
1250
1251 await world.clock.advance(Fixtures.SETTLE_MS)
1252
1253 expect(
1254 world.opened.map(pane => pane.id),
1255 'the restore left the opening to the first edit with a row to list',
1256 ).toEqual(['diff'])
1257
1258 expect(
1259 Fixtures.textOf(await $.ui.render(Fixtures.PANE)),
1260 'listed as it opened',
1261 ).toContain('1 file changed')
1262 })
1263
1264 test('a resumed session opens nothing where its first edit would not', async ($, on) => {
1265 const narrow = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1266 messages: () => Fixtures.EDITED_TRANSCRIPT,
1267 })
1268
1269 await $.session.start(Fixtures.SESSION)
1270 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
1271 await narrow.clock.advance(Fixtures.SETTLE_MS)
1272
1273 expect(narrow.opened, 'under the unasked floor, never kept open').toEqual(
1274 [],
1275 )
1276
1277 expect(narrow.runs, 'and no git for it').toEqual([])
1278 })
1279
1280 test('a resumed session the person kept the pane open in opens it from the lower floor', async ($, on) => {
1281 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1282 messages: () => Fixtures.EDITED_TRANSCRIPT,
1283 stored: { [Names.STORE_OPEN_KEY]: true },
1284 })
1285
1286 await $.session.start(Fixtures.SESSION)
1287 await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS))
1288 await world.clock.advance(Fixtures.SETTLE_MS)
1289
1290 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
1291 })
1292
1293 test('a resumed session with no edits opens nothing', async ($, on) => {
1294 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1295 messages: () => [Fixtures.promptOf('just talk')],
1296 })
1297
1298 await $.session.start(Fixtures.SESSION)
1299 await $.ui.render(Fixtures.HINT)
1300 await world.clock.advance(Fixtures.SETTLE_MS)
1301
1302 expect(world.opened).toEqual([])
1303 expect(world.runs).toEqual([])
1304 })
1305
1306 test('a resumed session whose pane the person closed opens nothing', async ($, on) => {
1307 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1308 messages: () => Fixtures.EDITED_TRANSCRIPT,
1309 stored: { [Names.STORE_OPEN_KEY]: false },
1310 })
1311
1312 await $.session.start(Fixtures.SESSION)
1313 await $.ui.render(Fixtures.HINT)
1314 await world.clock.advance(Fixtures.SETTLE_MS)
1315
1316 expect(world.opened).toEqual([])
1317 expect(world.runs).toEqual([])
1318 })
1319
1320 test('/resume closes the pane, then opens it for the turns it brought back', async ($, on) => {
1321 let transcript: readonly SessionMessage[] = []
1322
1323 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
1324 messages: () => transcript,
1325 })
1326
1327 on('command.run', { command: 'resume' }, () => {
1328 transcript = Fixtures.EDITED_TRANSCRIPT
1329
1330 return {}
1331 })
1332
1333 await $.session.start(Fixtures.SESSION)
1334 await $.ui.render(Fixtures.HINT)
1335 await $.command.run(Fixtures.DIFF)
1336 await world.clock.advance(Fixtures.SETTLE_MS)
1337 await $.command.run(Fixtures.RESUME)
1338 await world.clock.advance(Fixtures.SETTLE_MS)
1339
1340 expect(world.closed.map(pane => pane.id), 'closed at /resume').toEqual([
1341 'diff',
1342 ])
1343
1344 expect(
1345 world.opened.map(pane => pane.id),
1346 '/diff opened it, /resume opened it again for the restored turns',
1347 ).toEqual(['diff', 'diff'])
1348 })
1349
1350 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
1351 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
1352 const called: Args<'tool.call'>[] = []
1353
1354 on('tool.call', ($, e) => {
1355 called.push(e)
1356
1357 return { result: 'called' }
1358 })
1359
1360 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
1361
1362 await $.session.start(Fixtures.WORKTREE_SESSION)
1363 await $.command.run(Fixtures.DIFF)
1364 await world.clock.advance(Fixtures.SETTLE_MS)
1365 await $.ui.render(Fixtures.PANE)
1366 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
1367 await world.clock.settle()
1368
1369 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
1370
1371 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
1372
1373 expect(armed).toContain('asked ✓')
1374 expect(called).toEqual([])
1375
1376 expect(world.statuses.at(-2)).toBe(
1377 '.env rides your next prompt (press asked ✓ to drop it)',
1378 )
1379
1380 expect(submitted).toMatchObject({
1381 context: [
1382 'The user attached the diff of .env from the diff pane to this ' +
1383 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
1384 ],
1385 })
1386 })
1387
1388 test('an ask with no room in the context is dropped', async ($, on) => {
1389 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
1390 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
1391 const reached: (readonly string[] | undefined)[] = []
1392
1393 on('prompt.submit', ($, e) => {
1394 reached.push(e.context)
1395
1396 return { text: e.text }
1397 })
1398
1399 await $.session.start(Fixtures.WORKTREE_SESSION)
1400 await $.command.run(Fixtures.DIFF)
1401 await world.clock.advance(Fixtures.SETTLE_MS)
1402 await $.ui.render(Fixtures.PANE)
1403 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
1404 await world.clock.settle()
1405
1406 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
1407 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
1408
1409 expect(reached).toEqual([[full], undefined])
1410
1411 expect(world.statuses.at(-1)).toBe(
1412 ".env's diff did not fit in the prompt and was dropped",
1413 )
1414 })
1415
1416 test('a worktree opens on the base it kept', async ($, on) => {
1417 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
1418 stored: { 'base:/main/wt': 'uncommitted' },
1419 })
1420
1421 await $.session.start(Fixtures.WORKTREE_SESSION)
1422 await $.command.run(Fixtures.DIFF)
1423 await world.clock.advance(Fixtures.SETTLE_MS)
1424
1425 expect(
1426 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
1427 "the built-in's base line under the header",
1428 ).toContain('uncommitted (vs HEAD)')
1429 })
1430
1431 test('no repository is an answer kept until /clear', async ($, on) => {
1432 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
1433 Fixtures.oneSecret()
1434
1435 const script: Record<string, string> = notYet
1436 const world = Fixtures.inRepository(on, script)
1437
1438 const probesOf = () =>
1439 world.runs.filter(run => run.argv.includes('--show-toplevel')).length
1440
1441 on('command.run', { command: 'clear' }, () => ({}))
1442
1443 await $.session.start(Fixtures.WORKTREE_SESSION)
1444
1445 const before = await $.command.run(Fixtures.DIFF)
1446
1447 script['rev-parse --path-format=absolute'] = worktree
1448
1449 const again = await $.command.run(Fixtures.DIFF)
1450 const probesKept = probesOf()
1451
1452 await $.command.run(Fixtures.CLEAR)
1453
1454 const after = await $.command.run(Fixtures.DIFF)
1455
1456 await world.clock.advance(Fixtures.SETTLE_MS)
1457 await $.ui.render(Fixtures.PANE)
1458
1459 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
1460
1461 const third = $.command.run(Fixtures.DIFF)
1462
1463 await world.clock.advance(Fixtures.SETTLE_MS)
1464 await third
1465
1466 expect(before.text).toContain("isn't in a git repository")
1467
1468 expect(again.text, "kept, as the built-in's answer is").toContain(
1469 "isn't in a git repository",
1470 )
1471
1472 expect(probesKept, 'one probe answered both').toBe(1)
1473 expect(after.text, '/clear forgot it').toBe('Diff panel shown')
1474 expect(world.opened[0]?.id).toBe('diff')
1475 expect(probesOf(), 'then one more, pinned from there on').toBe(2)
1476
1477 expect(
1478 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
1479 ).toBe(false)
1480
1481 expect(
1482 world.runs
1483 .filter(run => run.argv.includes('--numstat'))
1484 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
1485 ).toBe(true)
1486 })
1487
1488 test('a repository that appears later opens on first edit', async ($, on) => {
1489 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
1490 Fixtures.oneSecret()
1491
1492 const script: Record<string, string> = notYet
1493 const world = Fixtures.inRepository(on, script)
1494
1495 on('tool.call', () => ({ result: 'edited' }))
1496
1497 await $.session.start(Fixtures.WORKTREE_SESSION)
1498 await $.ui.render(Fixtures.HINT)
1499
1500 script['rev-parse --path-format=absolute'] = worktree
1501
1502 await $.tool.call({
1503 tool: 'Edit',
1504 file_path: '/main/wt/.env',
1505 old_string: 'old',
1506 new_string: 'new',
1507 })
1508
1509 await world.clock.advance(Fixtures.SETTLE_MS)
1510
1511 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
1512 })
1513
1514 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
1515 const runs: Args<'process.run'>[] = []
1516
1517 Fixtures.startsSession(on)
1518
1519 on('process.run', ($, e) => {
1520 runs.push(e)
1521
1522 return e.argv.includes('--show-toplevel')
1523 ? { deny: Fixtures.FAILED_START }
1524 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
1525 })
1526
1527 await $.session.start(Fixtures.WORKTREE_SESSION)
1528
1529 const { text } = await $.command.run(Fixtures.DIFF)
1530 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
1531
1532 expect(text).toContain("isn't in a git repository")
1533 expect(probes, "this /diff's alone; no retry").toHaveLength(1)
1534 })
1535
1536 test('/diff on a narrow terminal: the resize line', async ($, on) => {
1537 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
1538
1539 await $.session.start(Fixtures.WORKTREE_SESSION)
1540
1541 const narrow = await $.command.run(
1542 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
1543 )
1544
1545 const openedNarrow = [...world.opened]
1546
1547 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
1548
1549 expect(narrow.text).toContain(
1550 'Resize your terminal to at least 110 columns to show the diff panel',
1551 )
1552
1553 expect(openedNarrow).toEqual([])
1554 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
1555 })
1556})