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,023 lines · 28.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('/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 leaves the pane it finds open up and reads the repository afresh', 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 world.clock.advance(Fixtures.SETTLE_MS)
692
693 const read = world.runs.length
694
695 await $.command.run(Fixtures.CLEAR)
696 await world.clock.advance(Fixtures.SETTLE_MS)
697
698 expect(world.closed, 'as the built-in panel stays across /clear').toEqual(
699 [],
700 )
701
702 expect(
703 world.runs.slice(read).map(run => Fixtures.gitWordOf(run.argv))[0],
704 'the repository found again for the conversation that starts over',
705 ).toBe('rev-parse --show-toplevel')
706
707 expect(
708 Fixtures.textOf(await $.ui.render(Fixtures.PANE)),
709 'drawn from the fresh read',
710 ).toContain('1 file changed')
711 })
712
713 test('a resumed session whose turns edited opens the pane before any new edit', async ($, on) => {
714 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
715 messages: () => Fixtures.EDITED_TRANSCRIPT,
716 })
717
718 await $.session.start(Fixtures.SESSION)
719
720 expect(world.runs, 'the start itself still runs no git').toEqual([])
721
722 await $.ui.render(Fixtures.HINT)
723 await world.clock.advance(Fixtures.SETTLE_MS)
724
725 expect(
726 world.opened.map(pane => pane.id),
727 'open once the width is known, as the built-in opens on the restore',
728 ).toEqual(['diff'])
729 })
730
731 test('a resumed session opens nothing where its first edit would not', async ($, on) => {
732 const narrow = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
733 messages: () => Fixtures.EDITED_TRANSCRIPT,
734 })
735
736 await $.session.start(Fixtures.SESSION)
737 await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1))
738 await narrow.clock.advance(Fixtures.SETTLE_MS)
739
740 expect(narrow.opened, 'under the unasked floor, never kept open').toEqual(
741 [],
742 )
743
744 expect(narrow.runs, 'and no git for it').toEqual([])
745 })
746
747 test('a resumed session the person kept the pane open in opens it from the lower floor', async ($, on) => {
748 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
749 messages: () => Fixtures.EDITED_TRANSCRIPT,
750 stored: { [Names.STORE_OPEN_KEY]: true },
751 })
752
753 await $.session.start(Fixtures.SESSION)
754 await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS))
755 await world.clock.advance(Fixtures.SETTLE_MS)
756
757 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
758 })
759
760 test('a resumed session with no edits opens nothing', async ($, on) => {
761 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
762 messages: () => [Fixtures.promptOf('just talk')],
763 })
764
765 await $.session.start(Fixtures.SESSION)
766 await $.ui.render(Fixtures.HINT)
767 await world.clock.advance(Fixtures.SETTLE_MS)
768
769 expect(world.opened).toEqual([])
770 expect(world.runs).toEqual([])
771 })
772
773 test('a resumed session whose pane the person closed opens nothing', async ($, on) => {
774 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
775 messages: () => Fixtures.EDITED_TRANSCRIPT,
776 stored: { [Names.STORE_OPEN_KEY]: false },
777 })
778
779 await $.session.start(Fixtures.SESSION)
780 await $.ui.render(Fixtures.HINT)
781 await world.clock.advance(Fixtures.SETTLE_MS)
782
783 expect(world.opened).toEqual([])
784 expect(world.runs).toEqual([])
785 })
786
787 test('/resume closes the pane, then opens it for the turns it brought back', async ($, on) => {
788 let transcript: readonly SessionMessage[] = []
789
790 const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
791 messages: () => transcript,
792 })
793
794 on('command.run', { command: 'resume' }, () => {
795 transcript = Fixtures.EDITED_TRANSCRIPT
796
797 return {}
798 })
799
800 await $.session.start(Fixtures.SESSION)
801 await $.ui.render(Fixtures.HINT)
802 await $.command.run(Fixtures.DIFF)
803 await world.clock.advance(Fixtures.SETTLE_MS)
804 await $.command.run(Fixtures.RESUME)
805 await world.clock.advance(Fixtures.SETTLE_MS)
806
807 expect(world.closed.map(pane => pane.id), 'closed at /resume').toEqual([
808 'diff',
809 ])
810
811 expect(
812 world.opened.map(pane => pane.id),
813 '/diff opened it, /resume opened it again for the restored turns',
814 ).toEqual(['diff', 'diff'])
815 })
816
817 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
818 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
819 const called: Args<'tool.call'>[] = []
820
821 on('tool.call', ($, e) => {
822 called.push(e)
823
824 return { result: 'called' }
825 })
826
827 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
828
829 await $.session.start(Fixtures.WORKTREE_SESSION)
830 await $.command.run(Fixtures.DIFF)
831 await world.clock.advance(Fixtures.SETTLE_MS)
832 await $.ui.render(Fixtures.PANE)
833 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
834 await world.clock.settle()
835
836 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
837
838 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
839
840 expect(armed).toContain('asked ✓')
841 expect(called).toEqual([])
842
843 expect(world.statuses.at(-2)).toBe(
844 '.env rides your next prompt (press asked ✓ to drop it)',
845 )
846
847 expect(submitted).toMatchObject({
848 context: [
849 'The user attached the diff of .env from the diff pane to this ' +
850 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
851 ],
852 })
853 })
854
855 test('an ask with no room in the context is dropped', async ($, on) => {
856 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
857 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
858 const reached: (readonly string[] | undefined)[] = []
859
860 on('prompt.submit', ($, e) => {
861 reached.push(e.context)
862
863 return { text: e.text }
864 })
865
866 await $.session.start(Fixtures.WORKTREE_SESSION)
867 await $.command.run(Fixtures.DIFF)
868 await world.clock.advance(Fixtures.SETTLE_MS)
869 await $.ui.render(Fixtures.PANE)
870 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
871 await world.clock.settle()
872
873 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
874 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
875
876 expect(reached).toEqual([[full], undefined])
877
878 expect(world.statuses.at(-1)).toBe(
879 ".env's diff did not fit in the prompt and was dropped",
880 )
881 })
882
883 test('a worktree opens on the base it kept', async ($, on) => {
884 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
885 stored: { 'base:/main/wt': 'uncommitted' },
886 })
887
888 await $.session.start(Fixtures.WORKTREE_SESSION)
889 await $.command.run(Fixtures.DIFF)
890 await world.clock.advance(Fixtures.SETTLE_MS)
891
892 expect(
893 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
894 "the built-in's base line under the header",
895 ).toContain('uncommitted (vs HEAD)')
896 })
897
898 test('no repository is an answer kept until /clear', async ($, on) => {
899 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
900 Fixtures.oneSecret()
901
902 const script: Record<string, string> = notYet
903 const world = Fixtures.inRepository(on, script)
904
905 const probesOf = () =>
906 world.runs.filter(run => run.argv.includes('--show-toplevel')).length
907
908 on('command.run', { command: 'clear' }, () => ({}))
909
910 await $.session.start(Fixtures.WORKTREE_SESSION)
911
912 const before = await $.command.run(Fixtures.DIFF)
913
914 script['rev-parse --path-format=absolute'] = worktree
915
916 const again = await $.command.run(Fixtures.DIFF)
917 const probesKept = probesOf()
918
919 await $.command.run(Fixtures.CLEAR)
920
921 const after = await $.command.run(Fixtures.DIFF)
922
923 await world.clock.advance(Fixtures.SETTLE_MS)
924 await $.ui.render(Fixtures.PANE)
925
926 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
927
928 const third = $.command.run(Fixtures.DIFF)
929
930 await world.clock.advance(Fixtures.SETTLE_MS)
931 await third
932
933 expect(before.text).toContain("isn't in a git repository")
934
935 expect(again.text, "kept, as the built-in's answer is").toContain(
936 "isn't in a git repository",
937 )
938
939 expect(probesKept, 'one probe answered both').toBe(1)
940 expect(after.text, '/clear forgot it').toBe('Diff panel shown')
941 expect(world.opened[0]?.id).toBe('diff')
942 expect(probesOf(), 'then one more, pinned from there on').toBe(2)
943
944 expect(
945 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
946 ).toBe(false)
947
948 expect(
949 world.runs
950 .filter(run => run.argv.includes('--numstat'))
951 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
952 ).toBe(true)
953 })
954
955 test('a repository that appears later opens on first edit', async ($, on) => {
956 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
957 Fixtures.oneSecret()
958
959 const script: Record<string, string> = notYet
960 const world = Fixtures.inRepository(on, script)
961
962 on('tool.call', () => ({ result: 'edited' }))
963
964 await $.session.start(Fixtures.WORKTREE_SESSION)
965 await $.ui.render(Fixtures.HINT)
966
967 script['rev-parse --path-format=absolute'] = worktree
968
969 await $.tool.call({
970 tool: 'Edit',
971 file_path: '/main/wt/.env',
972 old_string: 'old',
973 new_string: 'new',
974 })
975
976 await world.clock.advance(Fixtures.SETTLE_MS)
977
978 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
979 })
980
981 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
982 const runs: Args<'process.run'>[] = []
983
984 Fixtures.startsSession(on)
985
986 on('process.run', ($, e) => {
987 runs.push(e)
988
989 return e.argv.includes('--show-toplevel')
990 ? { deny: Fixtures.FAILED_START }
991 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
992 })
993
994 await $.session.start(Fixtures.WORKTREE_SESSION)
995
996 const { text } = await $.command.run(Fixtures.DIFF)
997 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
998
999 expect(text).toContain("isn't in a git repository")
1000 expect(probes, "this /diff's alone; no retry").toHaveLength(1)
1001 })
1002
1003 test('/diff on a narrow terminal: the resize line', async ($, on) => {
1004 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
1005
1006 await $.session.start(Fixtures.WORKTREE_SESSION)
1007
1008 const narrow = await $.command.run(
1009 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
1010 )
1011
1012 const openedNarrow = [...world.opened]
1013
1014 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
1015
1016 expect(narrow.text).toContain(
1017 'Resize your terminal to at least 110 columns to show the diff panel',
1018 )
1019
1020 expect(openedNarrow).toEqual([])
1021 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
1022 })
1023})