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
| 1 | import type { Args, ResultOf } from 'claude-code' |
| 2 | import { describe, expect, mock, test, tier } from 'claude-code/testing' |
| 3 | |
| 4 | import Limits from '../hooks/limits' |
| 5 | import Names from '../hooks/names' |
| 6 | import Fixtures from './fixtures' |
| 7 | |
| 8 | tier('builtin') |
| 9 | |
| 10 | describe('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)), |
| 569 | 'the repository was found, nothing fetched for a pane no one sees', |
| 570 | ).toEqual(['rev-parse --show-toplevel']) |
| 571 | |
| 572 | isNarrow = false |
| 573 | |
| 574 | expect( |
| 575 | await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)), |
| 576 | '/diff opens it, where a pane believed open would have been hidden', |
| 577 | ).toEqual({ text: 'Diff panel shown' }) |
| 578 | |
| 579 | await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 580 | await edit() |
| 581 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 582 | |
| 583 | expect( |
| 584 | world.opened.map(pane => pane.id), |
| 585 | 'closed by hand, no edit opens it again', |
| 586 | ).toEqual(['diff']) |
| 587 | }) |
| 588 | |
| 589 | test('an unplaced open is tried again at the next edit', async ($, on) => { |
| 590 | let isNarrow = true |
| 591 | |
| 592 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 593 | stored: { [Names.STORE_OPEN_KEY]: true }, |
| 594 | isLeftWaiting: () => isNarrow, |
| 595 | }) |
| 596 | |
| 597 | const edit = () => |
| 598 | $.tool.call({ |
| 599 | tool: 'Edit', |
| 600 | file_path: '/work/app.ts', |
| 601 | old_string: '1', |
| 602 | new_string: '2', |
| 603 | }) |
| 604 | |
| 605 | on('tool.call', () => ({ result: 'edited' })) |
| 606 | |
| 607 | await $.session.start(Fixtures.SESSION) |
| 608 | await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 609 | await edit() |
| 610 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 611 | |
| 612 | expect(world.opened, 'the first try waited and was withdrawn').toEqual([]) |
| 613 | |
| 614 | isNarrow = false |
| 615 | |
| 616 | await edit() |
| 617 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 618 | |
| 619 | expect( |
| 620 | world.opened.map(pane => pane.id), |
| 621 | 'the next edit opened it, as the built-in reads the width again then', |
| 622 | ).toEqual(['diff']) |
| 623 | }) |
| 624 | |
| 625 | test('/clear closes the pane it finds open', async ($, on) => { |
| 626 | const world = Fixtures.inRepository(on) |
| 627 | |
| 628 | on('command.run', { command: 'clear' }, () => ({})) |
| 629 | |
| 630 | await $.session.start(Fixtures.SESSION) |
| 631 | await $.command.run(Fixtures.DIFF) |
| 632 | await $.command.run(Fixtures.CLEAR) |
| 633 | |
| 634 | expect(world.closed.map(pane => pane.id)).toEqual(['diff']) |
| 635 | }) |
| 636 | |
| 637 | test('ask attaches the hunks on screen and calls no tool', async ($, on) => { |
| 638 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 639 | const called: Args<'tool.call'>[] = [] |
| 640 | |
| 641 | on('tool.call', ($, e) => { |
| 642 | called.push(e) |
| 643 | |
| 644 | return { result: 'called' } |
| 645 | }) |
| 646 | |
| 647 | on('prompt.submit', ($, e) => ({ text: e.text, context: e.context })) |
| 648 | |
| 649 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 650 | await $.command.run(Fixtures.DIFF) |
| 651 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 652 | await $.ui.render(Fixtures.PANE) |
| 653 | await $.ui.press({ plugin: 'diff', key: 'ask:.env' }) |
| 654 | await world.clock.settle() |
| 655 | |
| 656 | const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE)) |
| 657 | |
| 658 | const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?')) |
| 659 | |
| 660 | expect(armed).toContain('asked ✓') |
| 661 | expect(called).toEqual([]) |
| 662 | |
| 663 | expect(world.statuses.at(-2)).toBe( |
| 664 | '.env rides your next prompt (press asked ✓ to drop it)', |
| 665 | ) |
| 666 | |
| 667 | expect(submitted).toMatchObject({ |
| 668 | context: [ |
| 669 | 'The user attached the diff of .env from the diff pane to this ' + |
| 670 | 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new', |
| 671 | ], |
| 672 | }) |
| 673 | }) |
| 674 | |
| 675 | test('an ask with no room in the context is dropped', async ($, on) => { |
| 676 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 677 | const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS) |
| 678 | const reached: (readonly string[] | undefined)[] = [] |
| 679 | |
| 680 | on('prompt.submit', ($, e) => { |
| 681 | reached.push(e.context) |
| 682 | |
| 683 | return { text: e.text } |
| 684 | }) |
| 685 | |
| 686 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 687 | await $.command.run(Fixtures.DIFF) |
| 688 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 689 | await $.ui.render(Fixtures.PANE) |
| 690 | await $.ui.press({ plugin: 'diff', key: 'ask:.env' }) |
| 691 | await world.clock.settle() |
| 692 | |
| 693 | await $.prompt.submit(Fixtures.typedPromptOf('why?', [full])) |
| 694 | await $.prompt.submit(Fixtures.typedPromptOf('and now?')) |
| 695 | |
| 696 | expect(reached).toEqual([[full], undefined]) |
| 697 | |
| 698 | expect(world.statuses.at(-1)).toBe( |
| 699 | ".env's diff did not fit in the prompt and was dropped", |
| 700 | ) |
| 701 | }) |
| 702 | |
| 703 | test('a worktree opens on the base it kept', async ($, on) => { |
| 704 | const world = Fixtures.inRepository(on, Fixtures.oneSecret(), { |
| 705 | stored: { 'base:/main/wt': 'uncommitted' }, |
| 706 | }) |
| 707 | |
| 708 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 709 | await $.command.run(Fixtures.DIFF) |
| 710 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 711 | |
| 712 | expect( |
| 713 | Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)), |
| 714 | "the built-in's base line under the header", |
| 715 | ).toContain('uncommitted (vs HEAD)') |
| 716 | }) |
| 717 | |
| 718 | test('no repository is an answer kept until /clear', async ($, on) => { |
| 719 | const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } = |
| 720 | Fixtures.oneSecret() |
| 721 | |
| 722 | const script: Record<string, string> = notYet |
| 723 | const world = Fixtures.inRepository(on, script) |
| 724 | |
| 725 | const probesOf = () => |
| 726 | world.runs.filter(run => run.argv.includes('--show-toplevel')).length |
| 727 | |
| 728 | on('command.run', { command: 'clear' }, () => ({})) |
| 729 | |
| 730 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 731 | |
| 732 | const before = await $.command.run(Fixtures.DIFF) |
| 733 | |
| 734 | script['rev-parse --path-format=absolute'] = worktree |
| 735 | |
| 736 | const again = await $.command.run(Fixtures.DIFF) |
| 737 | const probesKept = probesOf() |
| 738 | |
| 739 | await $.command.run(Fixtures.CLEAR) |
| 740 | |
| 741 | const after = await $.command.run(Fixtures.DIFF) |
| 742 | |
| 743 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 744 | await $.ui.render(Fixtures.PANE) |
| 745 | |
| 746 | script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES |
| 747 | |
| 748 | const third = $.command.run(Fixtures.DIFF) |
| 749 | |
| 750 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 751 | await third |
| 752 | |
| 753 | expect(before.text).toContain("isn't in a git repository") |
| 754 | |
| 755 | expect(again.text, "kept, as the built-in's answer is").toContain( |
| 756 | "isn't in a git repository", |
| 757 | ) |
| 758 | |
| 759 | expect(probesKept, 'one probe answered both').toBe(1) |
| 760 | expect(after.text, '/clear forgot it').toBe('Diff panel shown') |
| 761 | expect(world.opened[0]?.id).toBe('diff') |
| 762 | expect(probesOf(), 'then one more, pinned from there on').toBe(2) |
| 763 | |
| 764 | expect( |
| 765 | world.runs.some(run => run.argv.includes('--git-dir=/else/.git')), |
| 766 | ).toBe(false) |
| 767 | |
| 768 | expect( |
| 769 | world.runs |
| 770 | .filter(run => run.argv.includes('--numstat')) |
| 771 | .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]), |
| 772 | ).toBe(true) |
| 773 | }) |
| 774 | |
| 775 | test('a repository that appears later opens on first edit', async ($, on) => { |
| 776 | const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } = |
| 777 | Fixtures.oneSecret() |
| 778 | |
| 779 | const script: Record<string, string> = notYet |
| 780 | const world = Fixtures.inRepository(on, script) |
| 781 | |
| 782 | on('tool.call', () => ({ result: 'edited' })) |
| 783 | |
| 784 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 785 | await $.ui.render(Fixtures.HINT) |
| 786 | |
| 787 | script['rev-parse --path-format=absolute'] = worktree |
| 788 | |
| 789 | await $.tool.call({ |
| 790 | tool: 'Edit', |
| 791 | file_path: '/main/wt/.env', |
| 792 | old_string: 'old', |
| 793 | new_string: 'new', |
| 794 | }) |
| 795 | |
| 796 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 797 | |
| 798 | expect(world.opened.map(pane => pane.id)).toEqual(['diff']) |
| 799 | }) |
| 800 | |
| 801 | test('a git that cannot start is an answer, not a timeout', async ($, on) => { |
| 802 | const runs: Args<'process.run'>[] = [] |
| 803 | |
| 804 | Fixtures.startsSession(on) |
| 805 | |
| 806 | on('process.run', ($, e) => { |
| 807 | runs.push(e) |
| 808 | |
| 809 | return e.argv.includes('--show-toplevel') |
| 810 | ? { deny: Fixtures.FAILED_START } |
| 811 | : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) } |
| 812 | }) |
| 813 | |
| 814 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 815 | |
| 816 | const { text } = await $.command.run(Fixtures.DIFF) |
| 817 | const probes = runs.filter(run => run.argv.includes('--show-toplevel')) |
| 818 | |
| 819 | expect(text).toContain("isn't in a git repository") |
| 820 | expect(probes, "this /diff's alone; no retry").toHaveLength(1) |
| 821 | }) |
| 822 | |
| 823 | test('/diff on a narrow terminal: the resize line', async ($, on) => { |
| 824 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 825 | |
| 826 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 827 | |
| 828 | const narrow = await $.command.run( |
| 829 | Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1), |
| 830 | ) |
| 831 | |
| 832 | const openedNarrow = [...world.opened] |
| 833 | |
| 834 | await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS)) |
| 835 | |
| 836 | expect(narrow.text).toContain( |
| 837 | 'Resize your terminal to at least 110 columns to show the diff panel', |
| 838 | ) |
| 839 | |
| 840 | expect(openedNarrow).toEqual([]) |
| 841 | expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID]) |
| 842 | }) |
| 843 | }) |