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,058 lines · 29.9 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('a shell command the tool held read-only fetches nothing', async ($, on) => {
125 const world = Fixtures.inRepository(on)
126
127 const fetchesSince = (read: number) =>
128 world.runs.slice(read).filter(run => run.argv.includes('--numstat'))
129 .length
130
131 on('tool.call', ($, e) =>
132 e.tool === 'Bash' && e.command === 'ls'
133 ? Fixtures.READ_ONLY_ANSWER
134 : { result: 'done' },
135 )
136
137 await $.session.start(Fixtures.SESSION)
138 await $.command.run(Fixtures.DIFF)
139 await world.clock.advance(Fixtures.SETTLE_MS)
140
141 const opened = world.runs.length
142
143 await $.tool.call({ tool: 'Bash', command: 'ls' })
144 await world.clock.advance(Fixtures.SETTLE_MS)
145
146 expect(
147 fetchesSince(opened),
148 "read-only, the pane open: nothing, as the built-in's touch is gated",
149 ).toBe(0)
150
151 const listed = world.runs.length
152
153 await $.tool.call({ tool: 'Bash', command: 'make' })
154 await world.clock.advance(Fixtures.SETTLE_MS)
155
156 expect(fetchesSince(listed), 'a command that may write: one fetch').toBe(1)
157 })
158
159 test('/diff whose probe never answers probes once more', async ($, on) => {
160 const probes: (readonly string[])[] = []
161 const clock = Fixtures.startsSession(on)
162
163 on('process.run', async ($, e) => {
164 probes.push(e.argv)
165
166 if (probes.length === 1) {
167 await clock.sleep(Limits.GIT_TIMEOUT_MS)
168
169 return { deny: Fixtures.GIT_HUNG }
170 }
171
172 return { value: Fixtures.NOT_A_REPOSITORY }
173 })
174
175 await $.session.start(Fixtures.SESSION)
176
177 const ran = $.command.run(Fixtures.DIFF)
178
179 await clock.settle()
180
181 expect(probes, "this /diff's probe, unanswered yet").toHaveLength(1)
182
183 await clock.advance(Limits.GIT_TIMEOUT_MS)
184
185 expect(await ran).toEqual({
186 text: expect.stringContaining("isn't in a git repository"),
187 })
188
189 expect(probes, 'then one more, which answered').toHaveLength(2)
190 })
191
192 test('two /diff typed together probe the repository once', async ($, on) => {
193 const world = Fixtures.inRepository(on)
194
195 await $.session.start(Fixtures.SESSION)
196
197 const [first, second] = await Promise.all([
198 $.command.run(Fixtures.DIFF),
199 $.command.run(Fixtures.DIFF),
200 ])
201
202 const probes = world.runs.filter(run =>
203 run.argv.includes('--show-toplevel'),
204 )
205
206 expect(first.text).toMatch(/^Diff panel (shown|hidden)$/)
207 expect(second.text).toMatch(/^Diff panel (shown|hidden)$/)
208 expect(probes, 'the second joined the probe in flight').toHaveLength(1)
209 })
210
211 test('outside a repository /diff says so, opens nothing', async ($, on) => {
212 const opened: string[] = []
213
214 Fixtures.startsSession(on)
215 on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY }))
216
217 on('ui.open', ($, e, next) => {
218 opened.push(e.id)
219
220 return next(e)
221 })
222
223 await $.session.start(Fixtures.SESSION)
224
225 const { text } = await $.command.run(Fixtures.DIFF)
226
227 expect(text).toContain("isn't in a git repository")
228 expect(opened).toEqual([])
229 })
230
231 test('a git that never answers is not "no repository"', async ($, on) => {
232 Fixtures.startsSession(on)
233 on('process.run', () => ({ deny: Fixtures.GIT_HUNG }))
234
235 await $.session.start(Fixtures.SESSION)
236
237 const { text } = await $.command.run(Fixtures.DIFF)
238
239 expect(text).toContain("git didn't answer")
240 })
241
242 test('when the built-in holds /diff, the mod stands down', async ($, on) => {
243 const logged: string[] = []
244 const runs: Args<'process.run'>[] = []
245
246 mock.clock(on)
247 on('session.start', ($, e) => ({ cwd: e.cwd }))
248 on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS }))
249 on('command.run', () => ({ text: 'the built-in /diff ran' }))
250 on('tool.call', () => ({ result: 'edited' }))
251
252 on('process.run', ($, e) => {
253 runs.push(e)
254
255 return { value: Fixtures.gitIn(e.argv) }
256 })
257
258 on('ui.log', ($, e) => {
259 logged.push(e.text)
260
261 return { value: undefined }
262 })
263
264 await $.session.start(Fixtures.SESSION)
265
266 expect(await $.command.run(Fixtures.DIFF)).toEqual({
267 text: 'the built-in /diff ran',
268 })
269
270 await $.tool.call({
271 tool: 'Edit',
272 file_path: '/work/app.ts',
273 old_string: '1',
274 new_string: '2',
275 })
276
277 expect(logged).toEqual([])
278 expect(runs, 'idle: no git for /diff or the edit').toEqual([])
279 })
280
281 test('a refusal the built-in did not cause is said aloud', async ($, on) => {
282 const logged: string[] = []
283
284 mock.clock(on)
285 on('session.start', ($, e) => ({ cwd: e.cwd }))
286
287 on('command.register', () => ({
288 deny: '32 commands are registered already',
289 }))
290
291 on('ui.log', ($, e) => {
292 logged.push(e.text)
293
294 return { value: undefined }
295 })
296
297 await $.session.start(Fixtures.SESSION)
298
299 expect(logged).toEqual([
300 'could not register /diff: diff: $.command.register: 32 commands are ' +
301 'registered already; the diff panel is unavailable this session',
302 ])
303 })
304
305 test('/diff opens the pane, unfocused, and says so', async ($, on) => {
306 const world = Fixtures.inRepository(on)
307
308 await $.session.start(Fixtures.SESSION)
309
310 expect(world.runs, 'the start ran nothing').toEqual([])
311
312 expect(await $.command.run(Fixtures.DIFF)).toEqual({
313 text: 'Diff panel shown',
314 })
315
316 expect(world.runs[0]?.argv, '/diff found the repository first').toContain(
317 '--show-toplevel',
318 )
319
320 expect(world.opened).toEqual([
321 { id: 'diff', title: 'Diff', holdToasts: true },
322 ])
323
324 await world.clock.advance(Fixtures.SETTLE_MS)
325
326 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
327
328 expect(drawn).toContain('1 file changed')
329 expect(drawn).toContain('app.ts')
330 })
331
332 test('off fullscreen, /diff opens the dialog, focused', async ($, on) => {
333 const world = Fixtures.inRepository(on)
334
335 await $.session.start(Fixtures.SESSION)
336
337 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
338
339 expect(world.opened[0]).toEqual({
340 id: 'diff',
341 title: 'Diff',
342 holdToasts: true,
343 closeOnEscape: true,
344 rows: expect.any(Number),
345 focus: true,
346 })
347
348 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({
349 text: 'Diff dialog dismissed',
350 })
351 })
352
353 test('/diff again closes the pane and says so', async ($, on) => {
354 const world = Fixtures.inRepository(on)
355
356 await $.session.start(Fixtures.SESSION)
357 await $.command.run(Fixtures.DIFF)
358
359 expect(await $.command.run(Fixtures.DIFF)).toEqual({
360 text: 'Diff panel hidden',
361 })
362
363 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
364 })
365
366 test('a wide terminal opens the pane at the first edit', async ($, on) => {
367 const world = Fixtures.inRepository(on)
368
369 on('tool.call', () => ({ result: 'edited' }))
370
371 await $.session.start(Fixtures.SESSION)
372 await $.ui.render(Fixtures.HINT)
373
374 await $.tool.call({
375 tool: 'Edit',
376 file_path: '/work/app.ts',
377 old_string: '1',
378 new_string: '2',
379 })
380
381 await world.clock.advance(Fixtures.SETTLE_MS)
382
383 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
384 })
385
386 test('on the main screen the first edit opens nothing, and /diff still opens the dialog', async ($, on) => {
387 const world = Fixtures.inRepository(on)
388
389 on('tool.call', () => ({ result: 'edited' }))
390
391 await $.session.start(Fixtures.SESSION)
392 await $.ui.render(Fixtures.MAIN_SCREEN_HINT)
393
394 await $.tool.call({
395 tool: 'Edit',
396 file_path: '/work/app.ts',
397 old_string: '1',
398 new_string: '2',
399 })
400
401 await world.clock.advance(Fixtures.SETTLE_MS)
402
403 expect(world.opened, 'a pane there would be an unasked dialog').toEqual([])
404 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
405
406 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
407
408 expect(
409 world.opened[0],
410 "the person's /diff opens the dialog, not dismisses one",
411 ).toMatchObject({ id: 'diff', focus: true })
412 })
413
414 test('a surface that does not say whether it docks a pane opens nothing at the first edit', async ($, on) => {
415 const world = Fixtures.inRepository(on)
416
417 on('tool.call', () => ({ result: 'edited' }))
418
419 await $.session.start(Fixtures.SESSION)
420 await $.ui.render(Fixtures.UNSAID_HINT)
421
422 await $.tool.call({
423 tool: 'Edit',
424 file_path: '/work/app.ts',
425 old_string: '1',
426 new_string: '2',
427 })
428
429 await world.clock.advance(Fixtures.SETTLE_MS)
430
431 expect(world.opened, 'unknown is not a dock').toEqual([])
432
433 await $.ui.render(Fixtures.HINT)
434
435 await $.tool.call({
436 tool: 'Edit',
437 file_path: '/work/app.ts',
438 old_string: '2',
439 new_string: '3',
440 })
441
442 await world.clock.advance(Fixtures.SETTLE_MS)
443
444 expect(
445 world.opened.map(pane => pane.id),
446 'once a drawing says the layout docks, the next edit opens it',
447 ).toEqual(['diff'])
448 })
449
450 test('an edit that failed or was refused opens nothing', async ($, on) => {
451 const world = Fixtures.inRepository(on)
452
453 const answers: ResultOf['tool.call'][] = [
454 { isError: true, result: 'no such text' },
455 { deny: 'not allowed' },
456 ]
457
458 const edit = () =>
459 $.tool.call({
460 tool: 'Edit',
461 file_path: '/work/app.ts',
462 old_string: '1',
463 new_string: '2',
464 })
465
466 on('tool.call', () => answers.shift() ?? { result: 'edited' })
467
468 await $.session.start(Fixtures.SESSION)
469 await $.ui.render(Fixtures.HINT)
470 await edit()
471 await edit()
472 await world.clock.advance(Fixtures.SETTLE_MS)
473
474 expect(world.opened, 'neither edit landed').toEqual([])
475 expect(world.runs, 'so nothing asked after the repository').toEqual([])
476
477 await edit()
478 await world.clock.advance(Fixtures.SETTLE_MS)
479
480 expect(
481 world.opened.map(pane => pane.id),
482 'the third did',
483 ).toEqual(['diff'])
484 })
485
486 test("only the main loop's edit opens the pane", async ($, on) => {
487 const world = Fixtures.inRepository(on)
488
489 const edit = (agentId?: string) =>
490 $.tool.call({
491 tool: 'Write',
492 file_path: '/work/app.ts',
493 content: '2',
494 ...(agentId !== undefined && { agentId }),
495 })
496
497 on('tool.call', () => ({ result: 'written' }))
498
499 await $.session.start(Fixtures.SESSION)
500 await $.ui.render(Fixtures.HINT)
501 await edit('a-explorer')
502 await world.clock.advance(Fixtures.SETTLE_MS)
503
504 expect(world.opened, 'the built-in checkpoints no subagent edit').toEqual(
505 [],
506 )
507
508 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
509
510 await edit()
511 await world.clock.advance(Fixtures.SETTLE_MS)
512
513 expect(
514 world.opened.map(pane => pane.id),
515 "the main loop's",
516 ).toEqual(['diff'])
517 })
518
519 test('checkpointing off in settings: edits open nothing', async ($, on) => {
520 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
521 settings: { fileCheckpointingEnabled: false },
522 })
523
524 on('tool.call', () => ({ result: 'edited' }))
525
526 await $.session.start(Fixtures.SESSION)
527 await $.ui.render(Fixtures.HINT)
528
529 await $.tool.call({
530 tool: 'Edit',
531 file_path: '/work/app.ts',
532 old_string: '1',
533 new_string: '2',
534 })
535
536 await world.clock.advance(Fixtures.SETTLE_MS)
537
538 expect(world.opened, 'the built-in opens on a checkpoint alone').toEqual([])
539
540 expect(world.runs, 'so nothing was asked of the repository').toEqual([])
541
542 expect(await $.command.run(Fixtures.DIFF)).toEqual({
543 text: 'Diff panel shown',
544 })
545 })
546
547 test('checkpointing off by variable: edits open nothing', async ($, on) => {
548 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
549 env: { CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: 'true' },
550 })
551
552 on('tool.call', () => ({ result: 'edited' }))
553
554 await $.session.start(Fixtures.SESSION)
555 await $.ui.render(Fixtures.HINT)
556
557 await $.tool.call({
558 tool: 'Edit',
559 file_path: '/work/app.ts',
560 old_string: '1',
561 new_string: '2',
562 })
563
564 await world.clock.advance(Fixtures.SETTLE_MS)
565
566 expect(world.opened).toEqual([])
567 })
568
569 test('an open left waiting is withdrawn; /diff opens it', async ($, on) => {
570 let isNarrow = true
571
572 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
573 stored: { [Names.STORE_OPEN_KEY]: true },
574 isLeftWaiting: () => isNarrow,
575 })
576
577 const edit = () =>
578 $.tool.call({
579 tool: 'Edit',
580 file_path: '/work/app.ts',
581 old_string: '1',
582 new_string: '2',
583 })
584
585 on('tool.call', () => ({ result: 'edited' }))
586
587 await $.session.start(Fixtures.SESSION)
588 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
589 await edit()
590 await world.clock.advance(Fixtures.SETTLE_MS)
591
592 expect(
593 world.waiting.map(pane => pane.id),
594 'the edit opened',
595 ).toEqual(['diff'])
596
597 expect(
598 world.closed.map(pane => pane.id),
599 'and withdrew the pane the engine left waiting, so no resize seats it',
600 ).toEqual(['diff'])
601
602 expect(
603 world.runs.map(run => Fixtures.gitWordOf(run.argv)).slice(0, 2),
604 'the repository was found and read once before the open, as the ' +
605 'built-in primes; nothing polls for a pane no one sees',
606 ).toEqual(['rev-parse --show-toplevel', 'status'])
607
608 expect(
609 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
610 'no HEAD poll for a withdrawn pane',
611 ).not.toContain(Fixtures.POLL_WORD)
612
613 isNarrow = false
614
615 expect(
616 await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)),
617 '/diff opens it, where a pane believed open would have been hidden',
618 ).toEqual({ text: 'Diff panel shown' })
619
620 await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
621 await edit()
622 await world.clock.advance(Fixtures.SETTLE_MS)
623
624 expect(
625 world.opened.map(pane => pane.id),
626 'closed by hand, no edit opens it again',
627 ).toEqual(['diff'])
628 })
629
630 test('an unplaced open is tried again at the next edit', async ($, on) => {
631 let isNarrow = true
632
633 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
634 stored: { [Names.STORE_OPEN_KEY]: true },
635 isLeftWaiting: () => isNarrow,
636 })
637
638 const edit = () =>
639 $.tool.call({
640 tool: 'Edit',
641 file_path: '/work/app.ts',
642 old_string: '1',
643 new_string: '2',
644 })
645
646 on('tool.call', () => ({ result: 'edited' }))
647
648 await $.session.start(Fixtures.SESSION)
649 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
650 await edit()
651 await world.clock.advance(Fixtures.SETTLE_MS)
652
653 expect(world.opened, 'the first try waited and was withdrawn').toEqual([])
654
655 isNarrow = false
656
657 await edit()
658 await world.clock.advance(Fixtures.SETTLE_MS)
659
660 expect(
661 world.opened.map(pane => pane.id),
662 'the next edit opened it, as the built-in reads the width again then',
663 ).toEqual(['diff'])
664 })
665
666 test('a docked pane opens once its first fetch settled', async ($, on) => {
667 const opened: string[] = []
668 const clock = Fixtures.startsSession(on)
669
670 on('process.run', async ($, e) => {
671 if (e.argv.includes('--shortstat')) {
672 await clock.sleep(Fixtures.SLOW_DIFF_MS)
673 }
674
675 return { value: Fixtures.gitIn(e.argv) }
676 })
677
678 on('ui.open', ($, e) => {
679 opened.push(e.id)
680
681 return { value: undefined }
682 })
683
684 on('ui.close', () => ({ value: undefined }))
685 on('ui.invalidate', () => ({ value: undefined }))
686 on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN)
687 on('session.messages', () => ({ value: [] }))
688 on('settings.read', () => ({ value: {} }))
689 on('tool.call', () => ({ result: 'edited' }))
690 mock.store(on)
691 mock.env(on, {})
692
693 await $.session.start(Fixtures.SESSION)
694 await $.ui.render(Fixtures.HINT)
695
696 await $.tool.call({
697 tool: 'Edit',
698 file_path: '/work/app.ts',
699 old_string: '1',
700 new_string: '2',
701 })
702
703 await clock.advance(Fixtures.SLOW_DIFF_MS - 1)
704
705 expect(opened, 'git has not answered: no pane, no Loading frame').toEqual(
706 [],
707 )
708
709 await clock.advance(Fixtures.SETTLE_MS)
710
711 expect(opened, 'the fetch settled: the pane opens filled').toEqual(['diff'])
712
713 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
714
715 expect(drawn).toContain('1 file changed')
716 expect(drawn).not.toContain('Loading diff')
717 })
718
719 test('/clear leaves the pane it finds open up and reads the repository afresh', async ($, on) => {
720 const world = Fixtures.inRepository(on)
721
722 on('command.run', { command: 'clear' }, () => ({}))
723
724 await $.session.start(Fixtures.SESSION)
725 await $.command.run(Fixtures.DIFF)
726 await world.clock.advance(Fixtures.SETTLE_MS)
727
728 const read = world.runs.length
729
730 await $.command.run(Fixtures.CLEAR)
731 await world.clock.advance(Fixtures.SETTLE_MS)
732
733 expect(world.closed, 'as the built-in panel stays across /clear').toEqual(
734 [],
735 )
736
737 expect(
738 world.runs.slice(read).map(run => Fixtures.gitWordOf(run.argv))[0],
739 'the repository found again for the conversation that starts over',
740 ).toBe('rev-parse --show-toplevel')
741
742 expect(
743 Fixtures.textOf(await $.ui.render(Fixtures.PANE)),
744 'drawn from the fresh read',
745 ).toContain('1 file changed')
746 })
747
748 test('a resumed session whose turns edited opens the pane before any new edit', async ($, on) => {
749 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
750 messages: () => Fixtures.EDITED_TRANSCRIPT,
751 })
752
753 await $.session.start(Fixtures.SESSION)
754
755 expect(world.runs, 'the start itself still runs no git').toEqual([])
756
757 await $.ui.render(Fixtures.HINT)
758 await world.clock.advance(Fixtures.SETTLE_MS)
759
760 expect(
761 world.opened.map(pane => pane.id),
762 'open once the width is known, as the built-in opens on the restore',
763 ).toEqual(['diff'])
764 })
765
766 test('a resumed session opens nothing where its first edit would not', async ($, on) => {
767 const narrow = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
768 messages: () => Fixtures.EDITED_TRANSCRIPT,
769 })
770
771 await $.session.start(Fixtures.SESSION)
772 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
773 await narrow.clock.advance(Fixtures.SETTLE_MS)
774
775 expect(narrow.opened, 'under the unasked floor, never kept open').toEqual(
776 [],
777 )
778
779 expect(narrow.runs, 'and no git for it').toEqual([])
780 })
781
782 test('a resumed session the person kept the pane open in opens it from the lower floor', async ($, on) => {
783 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
784 messages: () => Fixtures.EDITED_TRANSCRIPT,
785 stored: { [Names.STORE_OPEN_KEY]: true },
786 })
787
788 await $.session.start(Fixtures.SESSION)
789 await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS))
790 await world.clock.advance(Fixtures.SETTLE_MS)
791
792 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
793 })
794
795 test('a resumed session with no edits opens nothing', async ($, on) => {
796 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
797 messages: () => [Fixtures.promptOf('just talk')],
798 })
799
800 await $.session.start(Fixtures.SESSION)
801 await $.ui.render(Fixtures.HINT)
802 await world.clock.advance(Fixtures.SETTLE_MS)
803
804 expect(world.opened).toEqual([])
805 expect(world.runs).toEqual([])
806 })
807
808 test('a resumed session whose pane the person closed opens nothing', async ($, on) => {
809 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
810 messages: () => Fixtures.EDITED_TRANSCRIPT,
811 stored: { [Names.STORE_OPEN_KEY]: false },
812 })
813
814 await $.session.start(Fixtures.SESSION)
815 await $.ui.render(Fixtures.HINT)
816 await world.clock.advance(Fixtures.SETTLE_MS)
817
818 expect(world.opened).toEqual([])
819 expect(world.runs).toEqual([])
820 })
821
822 test('/resume closes the pane, then opens it for the turns it brought back', async ($, on) => {
823 let transcript: readonly SessionMessage[] = []
824
825 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
826 messages: () => transcript,
827 })
828
829 on('command.run', { command: 'resume' }, () => {
830 transcript = Fixtures.EDITED_TRANSCRIPT
831
832 return {}
833 })
834
835 await $.session.start(Fixtures.SESSION)
836 await $.ui.render(Fixtures.HINT)
837 await $.command.run(Fixtures.DIFF)
838 await world.clock.advance(Fixtures.SETTLE_MS)
839 await $.command.run(Fixtures.RESUME)
840 await world.clock.advance(Fixtures.SETTLE_MS)
841
842 expect(world.closed.map(pane => pane.id), 'closed at /resume').toEqual([
843 'diff',
844 ])
845
846 expect(
847 world.opened.map(pane => pane.id),
848 '/diff opened it, /resume opened it again for the restored turns',
849 ).toEqual(['diff', 'diff'])
850 })
851
852 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
853 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
854 const called: Args<'tool.call'>[] = []
855
856 on('tool.call', ($, e) => {
857 called.push(e)
858
859 return { result: 'called' }
860 })
861
862 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
863
864 await $.session.start(Fixtures.WORKTREE_SESSION)
865 await $.command.run(Fixtures.DIFF)
866 await world.clock.advance(Fixtures.SETTLE_MS)
867 await $.ui.render(Fixtures.PANE)
868 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
869 await world.clock.settle()
870
871 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
872
873 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
874
875 expect(armed).toContain('asked ✓')
876 expect(called).toEqual([])
877
878 expect(world.statuses.at(-2)).toBe(
879 '.env rides your next prompt (press asked ✓ to drop it)',
880 )
881
882 expect(submitted).toMatchObject({
883 context: [
884 'The user attached the diff of .env from the diff pane to this ' +
885 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
886 ],
887 })
888 })
889
890 test('an ask with no room in the context is dropped', async ($, on) => {
891 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
892 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
893 const reached: (readonly string[] | undefined)[] = []
894
895 on('prompt.submit', ($, e) => {
896 reached.push(e.context)
897
898 return { text: e.text }
899 })
900
901 await $.session.start(Fixtures.WORKTREE_SESSION)
902 await $.command.run(Fixtures.DIFF)
903 await world.clock.advance(Fixtures.SETTLE_MS)
904 await $.ui.render(Fixtures.PANE)
905 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
906 await world.clock.settle()
907
908 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
909 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
910
911 expect(reached).toEqual([[full], undefined])
912
913 expect(world.statuses.at(-1)).toBe(
914 ".env's diff did not fit in the prompt and was dropped",
915 )
916 })
917
918 test('a worktree opens on the base it kept', async ($, on) => {
919 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
920 stored: { 'base:/main/wt': 'uncommitted' },
921 })
922
923 await $.session.start(Fixtures.WORKTREE_SESSION)
924 await $.command.run(Fixtures.DIFF)
925 await world.clock.advance(Fixtures.SETTLE_MS)
926
927 expect(
928 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
929 "the built-in's base line under the header",
930 ).toContain('uncommitted (vs HEAD)')
931 })
932
933 test('no repository is an answer kept until /clear', async ($, on) => {
934 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
935 Fixtures.oneSecret()
936
937 const script: Record<string, string> = notYet
938 const world = Fixtures.inRepository(on, script)
939
940 const probesOf = () =>
941 world.runs.filter(run => run.argv.includes('--show-toplevel')).length
942
943 on('command.run', { command: 'clear' }, () => ({}))
944
945 await $.session.start(Fixtures.WORKTREE_SESSION)
946
947 const before = await $.command.run(Fixtures.DIFF)
948
949 script['rev-parse --path-format=absolute'] = worktree
950
951 const again = await $.command.run(Fixtures.DIFF)
952 const probesKept = probesOf()
953
954 await $.command.run(Fixtures.CLEAR)
955
956 const after = await $.command.run(Fixtures.DIFF)
957
958 await world.clock.advance(Fixtures.SETTLE_MS)
959 await $.ui.render(Fixtures.PANE)
960
961 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
962
963 const third = $.command.run(Fixtures.DIFF)
964
965 await world.clock.advance(Fixtures.SETTLE_MS)
966 await third
967
968 expect(before.text).toContain("isn't in a git repository")
969
970 expect(again.text, "kept, as the built-in's answer is").toContain(
971 "isn't in a git repository",
972 )
973
974 expect(probesKept, 'one probe answered both').toBe(1)
975 expect(after.text, '/clear forgot it').toBe('Diff panel shown')
976 expect(world.opened[0]?.id).toBe('diff')
977 expect(probesOf(), 'then one more, pinned from there on').toBe(2)
978
979 expect(
980 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
981 ).toBe(false)
982
983 expect(
984 world.runs
985 .filter(run => run.argv.includes('--numstat'))
986 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
987 ).toBe(true)
988 })
989
990 test('a repository that appears later opens on first edit', async ($, on) => {
991 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
992 Fixtures.oneSecret()
993
994 const script: Record<string, string> = notYet
995 const world = Fixtures.inRepository(on, script)
996
997 on('tool.call', () => ({ result: 'edited' }))
998
999 await $.session.start(Fixtures.WORKTREE_SESSION)
1000 await $.ui.render(Fixtures.HINT)
1001
1002 script['rev-parse --path-format=absolute'] = worktree
1003
1004 await $.tool.call({
1005 tool: 'Edit',
1006 file_path: '/main/wt/.env',
1007 old_string: 'old',
1008 new_string: 'new',
1009 })
1010
1011 await world.clock.advance(Fixtures.SETTLE_MS)
1012
1013 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
1014 })
1015
1016 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
1017 const runs: Args<'process.run'>[] = []
1018
1019 Fixtures.startsSession(on)
1020
1021 on('process.run', ($, e) => {
1022 runs.push(e)
1023
1024 return e.argv.includes('--show-toplevel')
1025 ? { deny: Fixtures.FAILED_START }
1026 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
1027 })
1028
1029 await $.session.start(Fixtures.WORKTREE_SESSION)
1030
1031 const { text } = await $.command.run(Fixtures.DIFF)
1032 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
1033
1034 expect(text).toContain("isn't in a git repository")
1035 expect(probes, "this /diff's alone; no retry").toHaveLength(1)
1036 })
1037
1038 test('/diff on a narrow terminal: the resize line', async ($, on) => {
1039 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
1040
1041 await $.session.start(Fixtures.WORKTREE_SESSION)
1042
1043 const narrow = await $.command.run(
1044 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
1045 )
1046
1047 const openedNarrow = [...world.opened]
1048
1049 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
1050
1051 expect(narrow.text).toContain(
1052 'Resize your terminal to at least 110 columns to show the diff panel',
1053 )
1054
1055 expect(openedNarrow).toEqual([])
1056 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
1057 })
1058})