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
406 lines · 11.3 KBRaw
1import type { Args } 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('/diff at boot joins the boot probe, then asks again', async ($, on) => {
12 const probes: (readonly string[])[] = []
13 const clock = Fixtures.startsSession(on)
14
15 on('process.run', async ($, e) => {
16 probes.push(e.argv)
17
18 if (probes.length === 1) {
19 await clock.sleep(Limits.GIT_TIMEOUT_MS)
20
21 return { deny: Fixtures.GIT_HUNG }
22 }
23
24 return { value: Fixtures.NOT_A_REPOSITORY }
25 })
26
27 const booting = $.session.start(Fixtures.SESSION)
28
29 await clock.settle()
30
31 const ran = $.command.run(Fixtures.DIFF)
32
33 await clock.settle()
34
35 expect(probes, 'the boot probe, which /diff joined').toHaveLength(1)
36
37 await clock.advance(Limits.GIT_TIMEOUT_MS)
38 await booting
39
40 expect(await ran).toEqual({
41 text: expect.stringContaining("isn't in a git repository"),
42 })
43
44 expect(probes, 'then one more of its own').toHaveLength(2)
45 })
46
47 test('outside a repository /diff says so, opens nothing', async ($, on) => {
48 const opened: string[] = []
49
50 Fixtures.startsSession(on)
51 on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY }))
52
53 on('ui.open', ($, e, next) => {
54 opened.push(e.id)
55
56 return next(e)
57 })
58
59 await $.session.start(Fixtures.SESSION)
60
61 const { text } = await $.command.run(Fixtures.DIFF)
62
63 expect(text).toContain("isn't in a git repository")
64 expect(opened).toEqual([])
65 })
66
67 test('a git that never answers is not "no repository"', async ($, on) => {
68 Fixtures.startsSession(on)
69 on('process.run', () => ({ deny: Fixtures.GIT_HUNG }))
70
71 await $.session.start(Fixtures.SESSION)
72
73 const { text } = await $.command.run(Fixtures.DIFF)
74
75 expect(text).toContain("git didn't answer")
76 })
77
78 test('when the built-in holds /diff, the mod stands down', async ($, on) => {
79 const logged: string[] = []
80
81 mock.clock(on)
82 on('session.start', ($, e) => ({ cwd: e.cwd }))
83 on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS }))
84 on('command.run', () => ({ text: 'the built-in /diff ran' }))
85
86 on('ui.log', ($, e) => {
87 logged.push(e.text)
88
89 return { value: undefined }
90 })
91
92 await $.session.start(Fixtures.SESSION)
93
94 expect(await $.command.run(Fixtures.DIFF)).toEqual({
95 text: 'the built-in /diff ran',
96 })
97
98 expect(logged).toEqual([])
99 })
100
101 test('a refusal the built-in did not cause is said aloud', async ($, on) => {
102 const logged: string[] = []
103
104 mock.clock(on)
105 on('session.start', ($, e) => ({ cwd: e.cwd }))
106
107 on('command.register', () => ({
108 deny: '32 commands are registered already',
109 }))
110
111 on('ui.log', ($, e) => {
112 logged.push(e.text)
113
114 return { value: undefined }
115 })
116
117 await $.session.start(Fixtures.SESSION)
118
119 expect(logged).toEqual([
120 'could not register /diff: diff: $.command.register: 32 commands are ' +
121 'registered already; the diff panel is unavailable this session',
122 ])
123 })
124
125 test('/diff opens the pane, unfocused, and says so', async ($, on) => {
126 const world = Fixtures.inRepository(on)
127
128 await $.session.start(Fixtures.SESSION)
129
130 expect(await $.command.run(Fixtures.DIFF)).toEqual({
131 text: 'Diff panel shown',
132 })
133
134 expect(world.opened).toEqual([
135 { id: 'diff', title: 'Diff', holdToasts: true },
136 ])
137
138 await world.clock.advance(Fixtures.SETTLE_MS)
139
140 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
141
142 expect(drawn).toContain('1 file changed')
143 expect(drawn).toContain('app.ts')
144 })
145
146 test('off fullscreen, /diff opens the dialog, focused', async ($, on) => {
147 const world = Fixtures.inRepository(on)
148
149 await $.session.start(Fixtures.SESSION)
150
151 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
152
153 expect(world.opened[0]).toEqual({
154 id: 'diff',
155 title: 'Diff',
156 holdToasts: true,
157 closeOnEscape: true,
158 rows: expect.any(Number),
159 focus: true,
160 })
161
162 expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({
163 text: 'Diff dialog dismissed',
164 })
165 })
166
167 test('/diff again closes the pane and says so', async ($, on) => {
168 const world = Fixtures.inRepository(on)
169
170 await $.session.start(Fixtures.SESSION)
171 await $.command.run(Fixtures.DIFF)
172
173 expect(await $.command.run(Fixtures.DIFF)).toEqual({
174 text: 'Diff panel hidden',
175 })
176
177 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
178 })
179
180 test('a wide terminal opens the pane at the first edit', async ($, on) => {
181 const world = Fixtures.inRepository(on)
182
183 on('tool.call', () => ({ result: 'edited' }))
184
185 await $.session.start(Fixtures.SESSION)
186 await $.ui.render(Fixtures.HINT)
187
188 await $.tool.call({
189 tool: 'Edit',
190 file_path: '/work/app.ts',
191 old_string: '1',
192 new_string: '2',
193 })
194
195 await world.clock.advance(Fixtures.SETTLE_MS)
196
197 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
198 })
199
200 test('/clear closes the pane it finds open', async ($, on) => {
201 const world = Fixtures.inRepository(on)
202
203 on('command.run', { command: 'clear' }, () => ({}))
204
205 await $.session.start(Fixtures.SESSION)
206 await $.command.run(Fixtures.DIFF)
207 await $.command.run(Fixtures.CLEAR)
208
209 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
210 })
211
212 test('ask attaches the hunks on screen and calls no tool', async ($, on) => {
213 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
214 const called: Args<'tool.call'>[] = []
215
216 on('tool.call', ($, e) => {
217 called.push(e)
218
219 return { result: 'called' }
220 })
221
222 on('prompt.submit', ($, e) => ({ text: e.text, context: e.context }))
223
224 await $.session.start(Fixtures.WORKTREE_SESSION)
225 await $.command.run(Fixtures.DIFF)
226 await world.clock.advance(Fixtures.SETTLE_MS)
227 await $.ui.render(Fixtures.PANE)
228 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
229 await world.clock.settle()
230
231 const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE))
232
233 const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?'))
234
235 expect(armed).toContain('asked ✓')
236 expect(called).toEqual([])
237
238 expect(world.statuses.at(-2)).toBe(
239 '.env rides your next prompt (press asked ✓ to drop it)',
240 )
241
242 expect(submitted).toMatchObject({
243 context: [
244 'The user attached the diff of .env from the diff pane to this ' +
245 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new',
246 ],
247 })
248 })
249
250 test('an ask with no room in the context is dropped', async ($, on) => {
251 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
252 const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS)
253 const reached: (readonly string[] | undefined)[] = []
254
255 on('prompt.submit', ($, e) => {
256 reached.push(e.context)
257
258 return { text: e.text }
259 })
260
261 await $.session.start(Fixtures.WORKTREE_SESSION)
262 await $.command.run(Fixtures.DIFF)
263 await world.clock.advance(Fixtures.SETTLE_MS)
264 await $.ui.render(Fixtures.PANE)
265 await $.ui.press({ plugin: 'diff', key: 'ask:.env' })
266 await world.clock.settle()
267
268 await $.prompt.submit(Fixtures.typedPromptOf('why?', [full]))
269 await $.prompt.submit(Fixtures.typedPromptOf('and now?'))
270
271 expect(reached).toEqual([[full], undefined])
272
273 expect(world.statuses.at(-1)).toBe(
274 ".env's diff did not fit in the prompt and was dropped",
275 )
276 })
277
278 test('a worktree opens on the base it kept', async ($, on) => {
279 const world = Fixtures.inRepository(on, Fixtures.oneSecret(), {
280 'base:/main/wt': 'uncommitted',
281 })
282
283 await $.session.start(Fixtures.WORKTREE_SESSION)
284 await $.command.run(Fixtures.DIFF)
285 await world.clock.advance(Fixtures.SETTLE_MS)
286
287 expect(
288 Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)),
289 "the built-in's base line under the header",
290 ).toContain('uncommitted (vs HEAD)')
291 })
292
293 test('a repository made later is pinned once, on /diff', async ($, on) => {
294 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
295 Fixtures.oneSecret()
296
297 const script: Record<string, string> = notYet
298 const world = Fixtures.inRepository(on, script)
299
300 await $.session.start(Fixtures.WORKTREE_SESSION)
301
302 const before = await $.command.run(Fixtures.DIFF)
303
304 script['rev-parse --path-format=absolute'] = worktree
305
306 const after = await $.command.run(Fixtures.DIFF)
307
308 await world.clock.advance(Fixtures.SETTLE_MS)
309 await $.ui.render(Fixtures.PANE)
310
311 script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES
312
313 const third = $.command.run(Fixtures.DIFF)
314
315 await world.clock.advance(Fixtures.SETTLE_MS)
316 await third
317
318 const probes = world.runs.filter(run =>
319 run.argv.includes('--show-toplevel'),
320 )
321
322 expect(before.text).toContain("isn't in a git repository")
323 expect(after.text).toBe('Diff panel shown')
324 expect(world.opened[0]?.id).toBe('diff')
325 expect(probes).toHaveLength(3)
326
327 expect(
328 world.runs.some(run => run.argv.includes('--git-dir=/else/.git')),
329 ).toBe(false)
330
331 expect(
332 world.runs
333 .filter(run => run.argv.includes('--numstat'))
334 .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]),
335 ).toBe(true)
336 })
337
338 test('a repository that appears later opens on first edit', async ($, on) => {
339 const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } =
340 Fixtures.oneSecret()
341
342 const script: Record<string, string> = notYet
343 const world = Fixtures.inRepository(on, script)
344
345 on('tool.call', () => ({ result: 'edited' }))
346
347 await $.session.start(Fixtures.WORKTREE_SESSION)
348 await $.ui.render(Fixtures.HINT)
349
350 script['rev-parse --path-format=absolute'] = worktree
351
352 await $.tool.call({
353 tool: 'Edit',
354 file_path: '/main/wt/.env',
355 old_string: 'old',
356 new_string: 'new',
357 })
358
359 await world.clock.advance(Fixtures.SETTLE_MS)
360
361 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
362 })
363
364 test('a git that cannot start is an answer, not a timeout', async ($, on) => {
365 const runs: Args<'process.run'>[] = []
366
367 Fixtures.startsSession(on)
368
369 on('process.run', ($, e) => {
370 runs.push(e)
371
372 return e.argv.includes('--show-toplevel')
373 ? { deny: Fixtures.FAILED_START }
374 : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) }
375 })
376
377 await $.session.start(Fixtures.WORKTREE_SESSION)
378
379 const { text } = await $.command.run(Fixtures.DIFF)
380 const probes = runs.filter(run => run.argv.includes('--show-toplevel'))
381
382 expect(text).toContain("isn't in a git repository")
383 expect(probes, "the boot's, then this /diff's; no retry").toHaveLength(2)
384 })
385
386 test('/diff on a narrow terminal: the resize line', async ($, on) => {
387 const world = Fixtures.inRepository(on, Fixtures.oneSecret())
388
389 await $.session.start(Fixtures.WORKTREE_SESSION)
390
391 const narrow = await $.command.run(
392 Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1),
393 )
394
395 const openedNarrow = [...world.opened]
396
397 await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS))
398
399 expect(narrow.text).toContain(
400 'Resize your terminal to at least 110 columns to show the diff panel',
401 )
402
403 expect(openedNarrow).toEqual([])
404 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
405 })
406})