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, FsEntry, On, ResultOf, SessionMessage } from 'claude-code' |
| 2 | import { describe, expect, mock, test, tier } from 'claude-code/testing' |
| 3 | import type { Engine } from 'claude-code/testing' |
| 4 | |
| 5 | import Limits from '../hooks/limits' |
| 6 | import Names from '../hooks/names' |
| 7 | import Record from '../hooks/record' |
| 8 | import Fixtures from './fixtures' |
| 9 | |
| 10 | tier('builtin') |
| 11 | |
| 12 | describe('register', () => { |
| 13 | async function drawnRoundMerge($: Engine, on: On, finishedMs: number) { |
| 14 | const script = { ...Fixtures.REPOSITORY, 'rev-parse --verify': 'merging' } |
| 15 | |
| 16 | let listing = Fixtures.MERGING.map(([name, kind]): FsEntry => ({ |
| 17 | name, |
| 18 | kind, |
| 19 | size: 0, |
| 20 | isLink: false, |
| 21 | })) |
| 22 | |
| 23 | on('fs.list', (_engine, e) => { |
| 24 | const isGitDir = e.path === '/work/.git' |
| 25 | |
| 26 | return { value: isGitDir ? listing : [] } |
| 27 | }) |
| 28 | |
| 29 | const world = Fixtures.inRepository(on, script) |
| 30 | |
| 31 | const drawn = async () => Fixtures.textOf(await $.ui.render(Fixtures.PANE)) |
| 32 | |
| 33 | await $.session.start(Fixtures.SESSION) |
| 34 | await $.command.run(Fixtures.DIFF) |
| 35 | await world.clock.advance(finishedMs) |
| 36 | |
| 37 | const before = await drawn() |
| 38 | |
| 39 | listing = [] |
| 40 | script['rev-parse --verify'] = 'merged' |
| 41 | |
| 42 | await world.clock.advance(Limits.HEAD_POLL_MS + Fixtures.SETTLE_MS) |
| 43 | |
| 44 | return { before, after: await drawn() } |
| 45 | } |
| 46 | |
| 47 | const NOTICED = { |
| 48 | before: expect.stringContaining('Diff unavailable'), |
| 49 | after: expect.stringContaining('1 file changed'), |
| 50 | } |
| 51 | |
| 52 | test('the start asks nothing of git and registers /diff', async ($, on) => { |
| 53 | const world = Fixtures.inRepository(on) |
| 54 | |
| 55 | await $.session.start(Fixtures.SESSION) |
| 56 | |
| 57 | expect(world.runs, "no git, as the built-in's start runs none").toEqual([]) |
| 58 | |
| 59 | expect(await $.command.run(Fixtures.DIFF), '/diff was registered').toEqual({ |
| 60 | text: 'Diff panel shown', |
| 61 | }) |
| 62 | }) |
| 63 | |
| 64 | test("git runs at the built-in's moments and no others", async ($, on) => { |
| 65 | const world = Fixtures.inRepository(on) |
| 66 | |
| 67 | let read = 0 |
| 68 | |
| 69 | function spawnedSince(): string[] { |
| 70 | const words = world.runs |
| 71 | .slice(read) |
| 72 | .map(run => Fixtures.gitWordOf(run.argv)) |
| 73 | .filter(word => word !== Fixtures.POLL_WORD) |
| 74 | |
| 75 | read = world.runs.length |
| 76 | |
| 77 | return words |
| 78 | } |
| 79 | |
| 80 | const edit = () => |
| 81 | $.tool.call({ |
| 82 | tool: 'Edit', |
| 83 | file_path: '/work/app.ts', |
| 84 | old_string: '1', |
| 85 | new_string: '2', |
| 86 | }) |
| 87 | |
| 88 | on('tool.call', () => ({ result: 'done' })) |
| 89 | on('turn.complete', ($, e) => ({ text: e.answer })) |
| 90 | on('command.run', { command: 'clear' }, () => ({})) |
| 91 | |
| 92 | await $.session.start(Fixtures.SESSION) |
| 93 | await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 94 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 95 | |
| 96 | expect(spawnedSince(), 'the start and the footer: nothing').toEqual([]) |
| 97 | |
| 98 | await edit() |
| 99 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 100 | |
| 101 | expect( |
| 102 | spawnedSince(), |
| 103 | 'an edit with no room for a pane: nothing, the width is read first', |
| 104 | ).toEqual([]) |
| 105 | |
| 106 | await $.command.run(Fixtures.DIFF) |
| 107 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 108 | |
| 109 | expect(spawnedSince(), '/diff: found, then one fetch for the pane').toEqual( |
| 110 | [ |
| 111 | 'rev-parse --show-toplevel', |
| 112 | 'status', |
| 113 | 'diff --shortstat', |
| 114 | 'diff --numstat', |
| 115 | 'ls-files', |
| 116 | 'diff -- app.ts', |
| 117 | ], |
| 118 | ) |
| 119 | |
| 120 | await edit() |
| 121 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 122 | |
| 123 | expect(spawnedSince(), 'an edit, the pane open: one fetch').toEqual([ |
| 124 | 'diff --shortstat', |
| 125 | 'diff --numstat', |
| 126 | 'ls-files', |
| 127 | 'diff -- app.ts', |
| 128 | ]) |
| 129 | |
| 130 | await $.tool.call({ tool: 'Bash', command: 'make' }) |
| 131 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 132 | |
| 133 | expect(spawnedSince(), 'a shell command, the pane open: one fetch').toEqual( |
| 134 | ['diff --shortstat', 'diff --numstat', 'ls-files', 'diff -- app.ts'], |
| 135 | ) |
| 136 | |
| 137 | await $.turn.complete({ |
| 138 | answer: 'done', |
| 139 | durationMs: 1, |
| 140 | isAborted: false, |
| 141 | turnId: 't1', |
| 142 | reason: 'answer', |
| 143 | }) |
| 144 | |
| 145 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 146 | |
| 147 | expect(spawnedSince(), "a turn's end: nothing").toEqual([]) |
| 148 | |
| 149 | await $.command.run(Fixtures.DIFF) |
| 150 | await edit() |
| 151 | await $.tool.call({ tool: 'Bash', command: 'make' }) |
| 152 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 153 | |
| 154 | expect(spawnedSince(), 'the pane closed by hand: nothing').toEqual([]) |
| 155 | |
| 156 | await $.command.run(Fixtures.CLEAR) |
| 157 | await $.command.run(Fixtures.DIFF) |
| 158 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 159 | |
| 160 | expect(spawnedSince()[0], '/clear forgot the repository').toBe( |
| 161 | 'rev-parse --show-toplevel', |
| 162 | ) |
| 163 | }) |
| 164 | |
| 165 | test('a shell command held read-only fetches nothing', async ($, on) => { |
| 166 | const world = Fixtures.inRepository(on) |
| 167 | |
| 168 | const fetchesSince = (read: number) => |
| 169 | world.runs.slice(read).filter(run => run.argv.includes('--numstat')) |
| 170 | .length |
| 171 | |
| 172 | on('tool.call', ($, e) => { |
| 173 | const isListing = e.tool === 'Bash' && e.command === 'ls' |
| 174 | |
| 175 | return isListing ? Fixtures.READ_ONLY_ANSWER : { result: 'done' } |
| 176 | }) |
| 177 | |
| 178 | await $.session.start(Fixtures.SESSION) |
| 179 | await $.command.run(Fixtures.DIFF) |
| 180 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 181 | |
| 182 | const opened = world.runs.length |
| 183 | |
| 184 | await $.tool.call({ tool: 'Bash', command: 'ls' }) |
| 185 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 186 | |
| 187 | expect( |
| 188 | fetchesSince(opened), |
| 189 | "read-only, the pane open: nothing, as the built-in's touch is gated", |
| 190 | ).toBe(0) |
| 191 | |
| 192 | const listed = world.runs.length |
| 193 | |
| 194 | await $.tool.call({ tool: 'Bash', command: 'make' }) |
| 195 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 196 | |
| 197 | expect(fetchesSince(listed), 'a command that may write: one fetch').toBe(1) |
| 198 | }) |
| 199 | |
| 200 | test('/diff whose probe never answers probes once more', async ($, on) => { |
| 201 | const probes: (readonly string[])[] = [] |
| 202 | const clock = Fixtures.startsSession(on) |
| 203 | |
| 204 | on('process.run', async ($, e) => { |
| 205 | probes.push(e.argv) |
| 206 | |
| 207 | if (probes.length === 1) { |
| 208 | await clock.sleep(Limits.GIT_TIMEOUT_MS) |
| 209 | |
| 210 | return { deny: Fixtures.GIT_HUNG } |
| 211 | } |
| 212 | |
| 213 | return { value: Fixtures.NOT_A_REPOSITORY } |
| 214 | }) |
| 215 | |
| 216 | await $.session.start(Fixtures.SESSION) |
| 217 | |
| 218 | const ran = $.command.run(Fixtures.DIFF) |
| 219 | |
| 220 | await clock.settle() |
| 221 | |
| 222 | expect(probes, "this /diff's probe, unanswered yet").toHaveLength(1) |
| 223 | |
| 224 | await clock.advance(Limits.GIT_TIMEOUT_MS) |
| 225 | |
| 226 | expect(await ran).toEqual({ |
| 227 | text: expect.stringContaining("isn't in a git repository"), |
| 228 | }) |
| 229 | |
| 230 | expect(probes, 'then one more, which answered').toHaveLength(2) |
| 231 | }) |
| 232 | |
| 233 | test('two /diff typed together probe the repository once', async ($, on) => { |
| 234 | const world = Fixtures.inRepository(on) |
| 235 | |
| 236 | await $.session.start(Fixtures.SESSION) |
| 237 | |
| 238 | const [first, second] = await Promise.all([ |
| 239 | $.command.run(Fixtures.DIFF), |
| 240 | $.command.run(Fixtures.DIFF), |
| 241 | ]) |
| 242 | |
| 243 | const probes = world.runs.filter(run => |
| 244 | run.argv.includes('--show-toplevel'), |
| 245 | ) |
| 246 | |
| 247 | expect(first.text).toMatch(/^Diff panel (shown|hidden)$/) |
| 248 | expect(second.text).toMatch(/^Diff panel (shown|hidden)$/) |
| 249 | expect(probes, 'the second joined the probe in flight').toHaveLength(1) |
| 250 | }) |
| 251 | |
| 252 | test('outside a repository /diff says so, opens nothing', async ($, on) => { |
| 253 | const opened: string[] = [] |
| 254 | |
| 255 | Fixtures.startsSession(on) |
| 256 | on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY })) |
| 257 | |
| 258 | on('ui.open', ($, e, next) => { |
| 259 | opened.push(e.id) |
| 260 | |
| 261 | return next(e) |
| 262 | }) |
| 263 | |
| 264 | await $.session.start(Fixtures.SESSION) |
| 265 | |
| 266 | const { text } = await $.command.run(Fixtures.DIFF) |
| 267 | |
| 268 | expect(text).toContain("isn't in a git repository") |
| 269 | expect(opened).toEqual([]) |
| 270 | }) |
| 271 | |
| 272 | test('a git that never answers is not "no repository"', async ($, on) => { |
| 273 | Fixtures.startsSession(on) |
| 274 | on('process.run', () => ({ deny: Fixtures.GIT_HUNG })) |
| 275 | |
| 276 | await $.session.start(Fixtures.SESSION) |
| 277 | |
| 278 | const { text } = await $.command.run(Fixtures.DIFF) |
| 279 | |
| 280 | expect(text).toContain("git didn't answer") |
| 281 | }) |
| 282 | |
| 283 | test('when the built-in holds /diff, the mod stands down', async ($, on) => { |
| 284 | const logged: string[] = [] |
| 285 | const runs: Args<'process.run'>[] = [] |
| 286 | |
| 287 | mock.clock(on) |
| 288 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 289 | on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS })) |
| 290 | on('command.run', () => ({ text: 'the built-in /diff ran' })) |
| 291 | on('tool.call', () => ({ result: 'edited' })) |
| 292 | |
| 293 | on('process.run', ($, e) => { |
| 294 | runs.push(e) |
| 295 | |
| 296 | return { value: Fixtures.gitIn(e.argv) } |
| 297 | }) |
| 298 | |
| 299 | on('ui.log', ($, e) => { |
| 300 | logged.push(e.text) |
| 301 | |
| 302 | return { value: undefined } |
| 303 | }) |
| 304 | |
| 305 | await $.session.start(Fixtures.SESSION) |
| 306 | |
| 307 | expect(await $.command.run(Fixtures.DIFF)).toEqual({ |
| 308 | text: 'the built-in /diff ran', |
| 309 | }) |
| 310 | |
| 311 | await $.tool.call({ |
| 312 | tool: 'Edit', |
| 313 | file_path: '/work/app.ts', |
| 314 | old_string: '1', |
| 315 | new_string: '2', |
| 316 | }) |
| 317 | |
| 318 | expect(logged).toEqual([]) |
| 319 | expect(runs, 'idle: no git for /diff or the edit').toEqual([]) |
| 320 | }) |
| 321 | |
| 322 | test('a refusal the built-in did not cause is said aloud', async ($, on) => { |
| 323 | const logged: string[] = [] |
| 324 | |
| 325 | mock.clock(on) |
| 326 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 327 | |
| 328 | on('command.register', () => ({ |
| 329 | deny: '32 commands are registered already', |
| 330 | })) |
| 331 | |
| 332 | on('ui.log', ($, e) => { |
| 333 | logged.push(e.text) |
| 334 | |
| 335 | return { value: undefined } |
| 336 | }) |
| 337 | |
| 338 | await $.session.start(Fixtures.SESSION) |
| 339 | |
| 340 | expect(logged).toEqual([ |
| 341 | 'could not register /diff: diff: $.command.register: 32 commands are ' + |
| 342 | 'registered already; the diff panel is unavailable this session', |
| 343 | ]) |
| 344 | }) |
| 345 | |
| 346 | test('/diff opens the pane, unfocused, and says so', async ($, on) => { |
| 347 | const world = Fixtures.inRepository(on) |
| 348 | |
| 349 | await $.session.start(Fixtures.SESSION) |
| 350 | |
| 351 | expect(world.runs, 'the start ran nothing').toEqual([]) |
| 352 | |
| 353 | expect(await $.command.run(Fixtures.DIFF)).toEqual({ |
| 354 | text: 'Diff panel shown', |
| 355 | }) |
| 356 | |
| 357 | expect(world.runs[0]?.argv, '/diff found the repository first').toContain( |
| 358 | '--show-toplevel', |
| 359 | ) |
| 360 | |
| 361 | expect(world.opened).toEqual([{ id: 'diff', title: 'Diff' }]) |
| 362 | |
| 363 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 364 | |
| 365 | const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) |
| 366 | |
| 367 | expect(drawn).toContain('1 file changed') |
| 368 | expect(drawn).toContain('app.ts') |
| 369 | }) |
| 370 | |
| 371 | test('off fullscreen, /diff opens the dialog, focused', async ($, on) => { |
| 372 | const world = Fixtures.inRepository(on) |
| 373 | |
| 374 | await $.session.start(Fixtures.SESSION) |
| 375 | |
| 376 | expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({}) |
| 377 | |
| 378 | expect(world.opened[0]).toEqual({ |
| 379 | id: 'diff', |
| 380 | title: 'Diff', |
| 381 | closeOnEscape: true, |
| 382 | rows: expect.any(Number), |
| 383 | focus: true, |
| 384 | }) |
| 385 | |
| 386 | expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({ |
| 387 | text: 'Diff dialog dismissed', |
| 388 | }) |
| 389 | }) |
| 390 | |
| 391 | test('/diff again closes the pane and says so', async ($, on) => { |
| 392 | const world = Fixtures.inRepository(on) |
| 393 | |
| 394 | await $.session.start(Fixtures.SESSION) |
| 395 | await $.command.run(Fixtures.DIFF) |
| 396 | |
| 397 | expect(await $.command.run(Fixtures.DIFF)).toEqual({ |
| 398 | text: 'Diff panel hidden', |
| 399 | }) |
| 400 | |
| 401 | expect(world.closed.map(pane => pane.id)).toEqual(['diff']) |
| 402 | }) |
| 403 | |
| 404 | test('a wide terminal opens the pane at the first edit', async ($, on) => { |
| 405 | const world = Fixtures.inRepository(on) |
| 406 | |
| 407 | on('tool.call', () => ({ result: 'edited' })) |
| 408 | |
| 409 | await $.session.start(Fixtures.SESSION) |
| 410 | await $.ui.render(Fixtures.HINT) |
| 411 | |
| 412 | await $.tool.call({ |
| 413 | tool: 'Edit', |
| 414 | file_path: '/work/app.ts', |
| 415 | old_string: '1', |
| 416 | new_string: '2', |
| 417 | }) |
| 418 | |
| 419 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 420 | |
| 421 | expect(world.opened.map(pane => pane.id)).toEqual(['diff']) |
| 422 | }) |
| 423 | |
| 424 | test('on the main screen only /diff opens, as a dialog', async ($, on) => { |
| 425 | const world = Fixtures.inRepository(on) |
| 426 | |
| 427 | on('tool.call', () => ({ result: 'edited' })) |
| 428 | |
| 429 | await $.session.start(Fixtures.SESSION) |
| 430 | await $.ui.render(Fixtures.MAIN_SCREEN_HINT) |
| 431 | |
| 432 | await $.tool.call({ |
| 433 | tool: 'Edit', |
| 434 | file_path: '/work/app.ts', |
| 435 | old_string: '1', |
| 436 | new_string: '2', |
| 437 | }) |
| 438 | |
| 439 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 440 | |
| 441 | expect(world.opened, 'a pane there would be an unasked dialog').toEqual([]) |
| 442 | expect(world.runs, 'so nothing was asked of the repository').toEqual([]) |
| 443 | |
| 444 | expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({}) |
| 445 | |
| 446 | expect( |
| 447 | world.opened[0], |
| 448 | "the person's /diff opens the dialog, not dismisses one", |
| 449 | ).toMatchObject({ id: 'diff', focus: true }) |
| 450 | }) |
| 451 | |
| 452 | test('a surface silent on docking opens nothing by itself', async ($, on) => { |
| 453 | const world = Fixtures.inRepository(on) |
| 454 | |
| 455 | on('tool.call', () => ({ result: 'edited' })) |
| 456 | |
| 457 | await $.session.start(Fixtures.SESSION) |
| 458 | await $.ui.render(Fixtures.UNSAID_HINT) |
| 459 | |
| 460 | await $.tool.call({ |
| 461 | tool: 'Edit', |
| 462 | file_path: '/work/app.ts', |
| 463 | old_string: '1', |
| 464 | new_string: '2', |
| 465 | }) |
| 466 | |
| 467 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 468 | |
| 469 | expect(world.opened, 'unknown is not a dock').toEqual([]) |
| 470 | |
| 471 | await $.ui.render(Fixtures.HINT) |
| 472 | |
| 473 | await $.tool.call({ |
| 474 | tool: 'Edit', |
| 475 | file_path: '/work/app.ts', |
| 476 | old_string: '2', |
| 477 | new_string: '3', |
| 478 | }) |
| 479 | |
| 480 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 481 | |
| 482 | expect( |
| 483 | world.opened.map(pane => pane.id), |
| 484 | 'once a drawing says the layout docks, the next edit opens it', |
| 485 | ).toEqual(['diff']) |
| 486 | }) |
| 487 | |
| 488 | test('an edit that failed or was refused opens nothing', async ($, on) => { |
| 489 | const world = Fixtures.inRepository(on) |
| 490 | |
| 491 | const answers: ResultOf['tool.call'][] = [ |
| 492 | { isError: true, result: 'no such text' }, |
| 493 | { deny: 'not allowed' }, |
| 494 | ] |
| 495 | |
| 496 | const edit = () => |
| 497 | $.tool.call({ |
| 498 | tool: 'Edit', |
| 499 | file_path: '/work/app.ts', |
| 500 | old_string: '1', |
| 501 | new_string: '2', |
| 502 | }) |
| 503 | |
| 504 | on('tool.call', () => answers.shift() ?? { result: 'edited' }) |
| 505 | |
| 506 | await $.session.start(Fixtures.SESSION) |
| 507 | await $.ui.render(Fixtures.HINT) |
| 508 | await edit() |
| 509 | await edit() |
| 510 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 511 | |
| 512 | expect(world.opened, 'neither edit landed').toEqual([]) |
| 513 | expect(world.runs, 'so nothing asked after the repository').toEqual([]) |
| 514 | |
| 515 | await edit() |
| 516 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 517 | |
| 518 | expect( |
| 519 | world.opened.map(pane => pane.id), |
| 520 | 'the third did', |
| 521 | ).toEqual(['diff']) |
| 522 | }) |
| 523 | |
| 524 | test("only the main loop's edit opens the pane", async ($, on) => { |
| 525 | const world = Fixtures.inRepository(on) |
| 526 | |
| 527 | const edit = (agentId?: string) => |
| 528 | $.tool.call({ |
| 529 | tool: 'Write', |
| 530 | file_path: '/work/app.ts', |
| 531 | content: '2', |
| 532 | ...(agentId !== undefined && { agentId }), |
| 533 | }) |
| 534 | |
| 535 | on('tool.call', () => ({ result: 'written' })) |
| 536 | |
| 537 | await $.session.start(Fixtures.SESSION) |
| 538 | await $.ui.render(Fixtures.HINT) |
| 539 | await edit('a-explorer') |
| 540 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 541 | |
| 542 | expect(world.opened, 'the built-in checkpoints no subagent edit').toEqual( |
| 543 | [], |
| 544 | ) |
| 545 | |
| 546 | expect(world.runs, 'so nothing was asked of the repository').toEqual([]) |
| 547 | |
| 548 | await edit() |
| 549 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 550 | |
| 551 | expect( |
| 552 | world.opened.map(pane => pane.id), |
| 553 | "the main loop's", |
| 554 | ).toEqual(['diff']) |
| 555 | }) |
| 556 | |
| 557 | test('checkpointing off in settings: edits open nothing', async ($, on) => { |
| 558 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 559 | settings: { fileCheckpointingEnabled: false }, |
| 560 | }) |
| 561 | |
| 562 | on('tool.call', () => ({ result: 'edited' })) |
| 563 | |
| 564 | await $.session.start(Fixtures.SESSION) |
| 565 | await $.ui.render(Fixtures.HINT) |
| 566 | |
| 567 | await $.tool.call({ |
| 568 | tool: 'Edit', |
| 569 | file_path: '/work/app.ts', |
| 570 | old_string: '1', |
| 571 | new_string: '2', |
| 572 | }) |
| 573 | |
| 574 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 575 | |
| 576 | expect(world.opened, 'the built-in opens on a checkpoint alone').toEqual([]) |
| 577 | |
| 578 | expect(world.runs, 'so nothing was asked of the repository').toEqual([]) |
| 579 | |
| 580 | expect(await $.command.run(Fixtures.DIFF)).toEqual({ |
| 581 | text: 'Diff panel shown', |
| 582 | }) |
| 583 | }) |
| 584 | |
| 585 | test('checkpointing off by variable: edits open nothing', async ($, on) => { |
| 586 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 587 | env: { CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: 'true' }, |
| 588 | }) |
| 589 | |
| 590 | on('tool.call', () => ({ result: 'edited' })) |
| 591 | |
| 592 | await $.session.start(Fixtures.SESSION) |
| 593 | await $.ui.render(Fixtures.HINT) |
| 594 | |
| 595 | await $.tool.call({ |
| 596 | tool: 'Edit', |
| 597 | file_path: '/work/app.ts', |
| 598 | old_string: '1', |
| 599 | new_string: '2', |
| 600 | }) |
| 601 | |
| 602 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 603 | |
| 604 | expect(world.opened).toEqual([]) |
| 605 | }) |
| 606 | |
| 607 | test('an open left waiting is withdrawn; /diff opens it', async ($, on) => { |
| 608 | let isNarrow = true |
| 609 | |
| 610 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 611 | stored: { [Names.STORE_OPEN_KEY]: true }, |
| 612 | isLeftWaiting: () => isNarrow, |
| 613 | }) |
| 614 | |
| 615 | const edit = () => |
| 616 | $.tool.call({ |
| 617 | tool: 'Edit', |
| 618 | file_path: '/work/app.ts', |
| 619 | old_string: '1', |
| 620 | new_string: '2', |
| 621 | }) |
| 622 | |
| 623 | on('tool.call', () => ({ result: 'edited' })) |
| 624 | |
| 625 | await $.session.start(Fixtures.SESSION) |
| 626 | await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 627 | await edit() |
| 628 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 629 | |
| 630 | expect( |
| 631 | world.waiting.map(pane => pane.id), |
| 632 | 'the edit opened', |
| 633 | ).toEqual(['diff']) |
| 634 | |
| 635 | expect( |
| 636 | world.closed.map(pane => pane.id), |
| 637 | 'and withdrew the pane the engine left waiting, so no resize seats it', |
| 638 | ).toEqual(['diff']) |
| 639 | |
| 640 | expect( |
| 641 | world.runs.map(run => Fixtures.gitWordOf(run.argv)).slice(0, 2), |
| 642 | 'the repository was found and read once before the open, as the ' + |
| 643 | 'built-in primes; nothing polls for a pane no one sees', |
| 644 | ).toEqual(['rev-parse --show-toplevel', 'status']) |
| 645 | |
| 646 | expect( |
| 647 | world.runs.map(run => Fixtures.gitWordOf(run.argv)), |
| 648 | 'no HEAD poll for a withdrawn pane', |
| 649 | ).not.toContain(Fixtures.POLL_WORD) |
| 650 | |
| 651 | isNarrow = false |
| 652 | |
| 653 | expect( |
| 654 | await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)), |
| 655 | '/diff opens it, where a pane believed open would have been hidden', |
| 656 | ).toEqual({ text: 'Diff panel shown' }) |
| 657 | |
| 658 | await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 659 | await edit() |
| 660 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 661 | |
| 662 | expect( |
| 663 | world.opened.map(pane => pane.id), |
| 664 | 'closed by hand, no edit opens it again', |
| 665 | ).toEqual(['diff']) |
| 666 | }) |
| 667 | |
| 668 | test('an unplaced open is tried again at the next edit', async ($, on) => { |
| 669 | let isNarrow = true |
| 670 | |
| 671 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 672 | stored: { [Names.STORE_OPEN_KEY]: true }, |
| 673 | isLeftWaiting: () => isNarrow, |
| 674 | }) |
| 675 | |
| 676 | const edit = () => |
| 677 | $.tool.call({ |
| 678 | tool: 'Edit', |
| 679 | file_path: '/work/app.ts', |
| 680 | old_string: '1', |
| 681 | new_string: '2', |
| 682 | }) |
| 683 | |
| 684 | on('tool.call', () => ({ result: 'edited' })) |
| 685 | |
| 686 | await $.session.start(Fixtures.SESSION) |
| 687 | await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 688 | await edit() |
| 689 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 690 | |
| 691 | expect(world.opened, 'the first try waited and was withdrawn').toEqual([]) |
| 692 | |
| 693 | isNarrow = false |
| 694 | |
| 695 | await edit() |
| 696 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 697 | |
| 698 | expect( |
| 699 | world.opened.map(pane => pane.id), |
| 700 | 'the next edit opened it, as the built-in reads the width again then', |
| 701 | ).toEqual(['diff']) |
| 702 | }) |
| 703 | |
| 704 | test('an open nothing draws yet is kept until drawn', async ($, on) => { |
| 705 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 706 | isLeftUnseen: () => true, |
| 707 | }) |
| 708 | |
| 709 | const logged = Fixtures.keeping<Args<'telemetry.log'>>() |
| 710 | |
| 711 | on('telemetry.log', logged.hook) |
| 712 | on('session.id', () => ({ value: 'first' })) |
| 713 | |
| 714 | await $.session.start(Fixtures.SESSION) |
| 715 | |
| 716 | expect( |
| 717 | await $.command.run(Fixtures.DIALOG_DIFF), |
| 718 | 'said as it is, with no word of a terminal to resize', |
| 719 | ).toEqual({ text: Names.AWAITS_SURFACE_TEXT }) |
| 720 | |
| 721 | await world.clock.advance(Limits.HEAD_POLL_MS + Fixtures.SETTLE_MS) |
| 722 | |
| 723 | expect( |
| 724 | world.unseen.map(pane => pane.id), |
| 725 | '/diff opened, then fitted the rows to what it read', |
| 726 | ).toEqual(['diff', 'diff']) |
| 727 | |
| 728 | expect(world.closed, 'and kept what it asked for').toEqual([]) |
| 729 | |
| 730 | expect( |
| 731 | world.runs |
| 732 | .map(run => Fixtures.gitWordOf(run.argv)) |
| 733 | .filter(word => word === Fixtures.POLL_WORD), |
| 734 | 'HEAD was read once at the open, as ever; no poll for a pane no one sees', |
| 735 | ).toEqual([Fixtures.POLL_WORD]) |
| 736 | |
| 737 | expect(logged.kept, 'nor is it counted shown').toEqual([]) |
| 738 | |
| 739 | const read = world.runs.length |
| 740 | |
| 741 | expect( |
| 742 | Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)), |
| 743 | 'the first drawing has what the open read', |
| 744 | ).toContain('app.ts') |
| 745 | |
| 746 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 747 | |
| 748 | expect( |
| 749 | world.runs.length, |
| 750 | 'and reads again, for what changed while no one saw', |
| 751 | ).toBeGreaterThan(read) |
| 752 | |
| 753 | expect( |
| 754 | logged.kept.map(shown => shown.props?.trigger), |
| 755 | 'and counted it, as the person asked for it', |
| 756 | ).toEqual([{ value: 'manual', of: [...Record.SHOWN_TRIGGERS] }]) |
| 757 | |
| 758 | expect( |
| 759 | await $.command.run(Fixtures.DIALOG_DIFF), |
| 760 | 'believed open all along, so /diff closes it', |
| 761 | ).toEqual({ text: 'Diff dialog dismissed' }) |
| 762 | }) |
| 763 | |
| 764 | test('while it waits unseen an edit reads nothing', async ($, on) => { |
| 765 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 766 | stored: { [Names.STORE_OPEN_KEY]: true }, |
| 767 | isLeftUnseen: () => true, |
| 768 | }) |
| 769 | |
| 770 | const edit = () => |
| 771 | $.tool.call({ |
| 772 | tool: 'Edit', |
| 773 | file_path: '/work/app.ts', |
| 774 | old_string: '1', |
| 775 | new_string: '2', |
| 776 | }) |
| 777 | |
| 778 | on('tool.call', () => ({ result: 'edited' })) |
| 779 | |
| 780 | await $.session.start(Fixtures.SESSION) |
| 781 | await $.ui.render(Fixtures.HINT) |
| 782 | await edit() |
| 783 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 784 | |
| 785 | const read = world.runs.length |
| 786 | |
| 787 | await edit() |
| 788 | await world.clock.advance(Limits.HEAD_POLL_MS + Fixtures.SETTLE_MS) |
| 789 | |
| 790 | expect( |
| 791 | world.unseen.map(pane => pane.id), |
| 792 | 'the first edit opened, and the second found it open', |
| 793 | ).toEqual(['diff']) |
| 794 | |
| 795 | expect(world.closed, 'kept for whatever attaches').toEqual([]) |
| 796 | expect(world.runs.length, 'git is left alone meanwhile').toBe(read) |
| 797 | }) |
| 798 | |
| 799 | test('a docked pane opens once its first fetch settled', async ($, on) => { |
| 800 | const opened: string[] = [] |
| 801 | const clock = Fixtures.startsSession(on) |
| 802 | |
| 803 | on('process.run', async ($, e) => { |
| 804 | if (e.argv.includes('--shortstat')) { |
| 805 | await clock.sleep(Fixtures.SLOW_DIFF_MS) |
| 806 | } |
| 807 | |
| 808 | return { value: Fixtures.gitIn(e.argv) } |
| 809 | }) |
| 810 | |
| 811 | on('ui.open', ($, e) => { |
| 812 | opened.push(e.id) |
| 813 | |
| 814 | return { value: undefined } |
| 815 | }) |
| 816 | |
| 817 | on('ui.close', () => ({ value: undefined })) |
| 818 | on('ui.invalidate', () => ({ value: undefined })) |
| 819 | on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN) |
| 820 | on('session.messages', () => ({ value: [] })) |
| 821 | on('settings.read', () => ({ value: {} })) |
| 822 | on('tool.call', () => ({ result: 'edited' })) |
| 823 | mock.store(on) |
| 824 | mock.env(on, {}) |
| 825 | |
| 826 | await $.session.start(Fixtures.SESSION) |
| 827 | await $.ui.render(Fixtures.HINT) |
| 828 | |
| 829 | await $.tool.call({ |
| 830 | tool: 'Edit', |
| 831 | file_path: '/work/app.ts', |
| 832 | old_string: '1', |
| 833 | new_string: '2', |
| 834 | }) |
| 835 | |
| 836 | await clock.advance(Fixtures.SLOW_DIFF_MS - 1) |
| 837 | |
| 838 | expect(opened, 'git has not answered: no pane, no Loading frame').toEqual( |
| 839 | [], |
| 840 | ) |
| 841 | |
| 842 | await clock.advance(Fixtures.SETTLE_MS) |
| 843 | |
| 844 | expect(opened, 'the fetch settled: the pane opens filled').toEqual(['diff']) |
| 845 | |
| 846 | const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) |
| 847 | |
| 848 | expect(drawn).toContain('1 file changed') |
| 849 | expect(drawn).not.toContain('Loading diff') |
| 850 | }) |
| 851 | |
| 852 | test('/clear leaves an open pane up and reads afresh', async ($, on) => { |
| 853 | const world = Fixtures.inRepository(on) |
| 854 | |
| 855 | on('command.run', { command: 'clear' }, () => ({})) |
| 856 | |
| 857 | await $.session.start(Fixtures.SESSION) |
| 858 | await $.command.run(Fixtures.DIFF) |
| 859 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 860 | |
| 861 | const read = world.runs.length |
| 862 | |
| 863 | await $.command.run(Fixtures.CLEAR) |
| 864 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 865 | |
| 866 | expect(world.closed, 'as the built-in panel stays across /clear').toEqual( |
| 867 | [], |
| 868 | ) |
| 869 | |
| 870 | expect( |
| 871 | world.runs.slice(read).map(run => Fixtures.gitWordOf(run.argv))[0], |
| 872 | 'the repository found again for the conversation that starts over', |
| 873 | ).toBe('rev-parse --show-toplevel') |
| 874 | |
| 875 | expect( |
| 876 | Fixtures.textOf(await $.ui.render(Fixtures.PANE)), |
| 877 | 'drawn from the fresh read', |
| 878 | ).toContain('1 file changed') |
| 879 | }) |
| 880 | |
| 881 | test('/clear under an open pane counts it shown again', async ($, on) => { |
| 882 | const world = Fixtures.inRepository(on) |
| 883 | const logged = Fixtures.keeping<Args<'telemetry.log'>>() |
| 884 | |
| 885 | let sessionId = 'first' |
| 886 | |
| 887 | on('telemetry.log', logged.hook) |
| 888 | on('session.id', () => ({ value: sessionId })) |
| 889 | on('tool.call', () => ({ result: 'edited' })) |
| 890 | |
| 891 | on('command.run', { command: 'clear' }, () => { |
| 892 | sessionId = 'second' |
| 893 | |
| 894 | return {} |
| 895 | }) |
| 896 | |
| 897 | await $.session.start(Fixtures.SESSION) |
| 898 | await $.ui.render(Fixtures.HINT) |
| 899 | |
| 900 | await $.tool.call({ |
| 901 | tool: 'Edit', |
| 902 | file_path: '/work/app.ts', |
| 903 | old_string: '1', |
| 904 | new_string: '2', |
| 905 | }) |
| 906 | |
| 907 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 908 | await $.command.run(Fixtures.CLEAR) |
| 909 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 910 | |
| 911 | expect(world.closed, 'the pane stayed up across /clear').toEqual([]) |
| 912 | |
| 913 | expect( |
| 914 | logged.kept.map(shown => shown.props?.trigger), |
| 915 | "once a conversation: Claude's edit opened the first, and the second, " + |
| 916 | "which nobody opened, reads as the person's, as the built-in's does", |
| 917 | ).toEqual([ |
| 918 | { value: 'auto_open', of: [...Record.SHOWN_TRIGGERS] }, |
| 919 | { value: 'manual', of: [...Record.SHOWN_TRIGGERS] }, |
| 920 | ]) |
| 921 | }) |
| 922 | |
| 923 | test('a merge finished 3 s after the open is noticed', async ($, on) => { |
| 924 | expect( |
| 925 | await drawnRoundMerge($, on, 3000), |
| 926 | 'finished in another terminal: read with no edit or command', |
| 927 | ).toEqual(NOTICED) |
| 928 | }) |
| 929 | |
| 930 | test('a merge finished 1.2 s after the open is noticed', async ($, on) => { |
| 931 | expect( |
| 932 | await drawnRoundMerge($, on, 1200), |
| 933 | 'before the first look at HEAD on the clock: the open read it', |
| 934 | ).toEqual(NOTICED) |
| 935 | }) |
| 936 | |
| 937 | test('a merge finished 0.3 s after the open is noticed', async ($, on) => { |
| 938 | expect( |
| 939 | await drawnRoundMerge($, on, 300), |
| 940 | 'before the first look at HEAD on the clock: the open read it', |
| 941 | ).toEqual(NOTICED) |
| 942 | }) |
| 943 | |
| 944 | test('a resumed session that edited opens before an edit', async ($, on) => { |
| 945 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 946 | messages: () => Fixtures.EDITED_TRANSCRIPT, |
| 947 | }) |
| 948 | |
| 949 | await $.session.start(Fixtures.SESSION) |
| 950 | |
| 951 | expect(world.runs, 'the start itself still runs no git').toEqual([]) |
| 952 | |
| 953 | await $.ui.render(Fixtures.HINT) |
| 954 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 955 | |
| 956 | expect( |
| 957 | world.opened.map(pane => pane.id), |
| 958 | 'open once the width is known, as the built-in opens on the restore', |
| 959 | ).toEqual(['diff']) |
| 960 | }) |
| 961 | |
| 962 | test('a resumed session opens only where an edit would', async ($, on) => { |
| 963 | const narrow = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 964 | messages: () => Fixtures.EDITED_TRANSCRIPT, |
| 965 | }) |
| 966 | |
| 967 | await $.session.start(Fixtures.SESSION) |
| 968 | await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) |
| 969 | await narrow.clock.advance(Fixtures.SETTLE_MS) |
| 970 | |
| 971 | expect(narrow.opened, 'under the unasked floor, never kept open').toEqual( |
| 972 | [], |
| 973 | ) |
| 974 | |
| 975 | expect(narrow.runs, 'and no git for it').toEqual([]) |
| 976 | }) |
| 977 | |
| 978 | test('a resumed session kept open opens at the low floor', async ($, on) => { |
| 979 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 980 | messages: () => Fixtures.EDITED_TRANSCRIPT, |
| 981 | stored: { [Names.STORE_OPEN_KEY]: true }, |
| 982 | }) |
| 983 | |
| 984 | await $.session.start(Fixtures.SESSION) |
| 985 | await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS)) |
| 986 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 987 | |
| 988 | expect(world.opened.map(pane => pane.id)).toEqual(['diff']) |
| 989 | }) |
| 990 | |
| 991 | test('a resumed session with no edits opens nothing', async ($, on) => { |
| 992 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 993 | messages: () => [Fixtures.promptOf('just talk')], |
| 994 | }) |
| 995 | |
| 996 | await $.session.start(Fixtures.SESSION) |
| 997 | await $.ui.render(Fixtures.HINT) |
| 998 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 999 | |
| 1000 | expect(world.opened).toEqual([]) |
| 1001 | expect(world.runs).toEqual([]) |
| 1002 | }) |
| 1003 | |
| 1004 | test('a resumed session the person closed opens nothing', async ($, on) => { |
| 1005 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 1006 | messages: () => Fixtures.EDITED_TRANSCRIPT, |
| 1007 | stored: { [Names.STORE_OPEN_KEY]: false }, |
| 1008 | }) |
| 1009 | |
| 1010 | await $.session.start(Fixtures.SESSION) |
| 1011 | await $.ui.render(Fixtures.HINT) |
| 1012 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1013 | |
| 1014 | expect(world.opened).toEqual([]) |
| 1015 | expect(world.runs).toEqual([]) |
| 1016 | }) |
| 1017 | |
| 1018 | test('/resume closes the pane, reopens it for its turns', async ($, on) => { |
| 1019 | let transcript: readonly SessionMessage[] = [] |
| 1020 | |
| 1021 | const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { |
| 1022 | messages: () => transcript, |
| 1023 | }) |
| 1024 | |
| 1025 | on('command.run', { command: 'resume' }, () => { |
| 1026 | transcript = Fixtures.EDITED_TRANSCRIPT |
| 1027 | |
| 1028 | return {} |
| 1029 | }) |
| 1030 | |
| 1031 | await $.session.start(Fixtures.SESSION) |
| 1032 | await $.ui.render(Fixtures.HINT) |
| 1033 | await $.command.run(Fixtures.DIFF) |
| 1034 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1035 | await $.command.run(Fixtures.RESUME) |
| 1036 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1037 | |
| 1038 | expect( |
| 1039 | world.closed.map(pane => pane.id), |
| 1040 | 'closed at /resume', |
| 1041 | ).toEqual(['diff']) |
| 1042 | |
| 1043 | expect( |
| 1044 | world.opened.map(pane => pane.id), |
| 1045 | '/diff opened it, /resume opened it again for the restored turns', |
| 1046 | ).toEqual(['diff', 'diff']) |
| 1047 | }) |
| 1048 | |
| 1049 | test('ask attaches the hunks on screen and calls no tool', async ($, on) => { |
| 1050 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 1051 | const called: Args<'tool.call'>[] = [] |
| 1052 | |
| 1053 | on('tool.call', ($, e) => { |
| 1054 | called.push(e) |
| 1055 | |
| 1056 | return { result: 'called' } |
| 1057 | }) |
| 1058 | |
| 1059 | on('prompt.submit', ($, e) => ({ text: e.text, context: e.context })) |
| 1060 | |
| 1061 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1062 | await $.command.run(Fixtures.DIFF) |
| 1063 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1064 | await $.ui.render(Fixtures.PANE) |
| 1065 | await $.ui.press({ plugin: 'diff', key: 'ask:.env' }) |
| 1066 | await world.clock.settle() |
| 1067 | |
| 1068 | const armed = Fixtures.jsonOf(await $.ui.render(Fixtures.PANE)) |
| 1069 | |
| 1070 | const submitted = await $.prompt.submit(Fixtures.typedPromptOf('why?')) |
| 1071 | |
| 1072 | expect(armed).toContain('asked ✓') |
| 1073 | expect(called).toEqual([]) |
| 1074 | |
| 1075 | expect(world.statuses.at(-2)).toBe( |
| 1076 | '.env rides your next prompt (press asked ✓ to drop it)', |
| 1077 | ) |
| 1078 | |
| 1079 | expect(submitted).toMatchObject({ |
| 1080 | context: [ |
| 1081 | 'The user attached the diff of .env from the diff pane to this ' + |
| 1082 | 'prompt:\n@@ -1 +1 @@\n-KEY=old\n+KEY=new', |
| 1083 | ], |
| 1084 | }) |
| 1085 | }) |
| 1086 | |
| 1087 | test('an ask with no room in the context is dropped', async ($, on) => { |
| 1088 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 1089 | const full = 'x'.repeat(Limits.PROMPT_CONTEXT_MAX_CHARS) |
| 1090 | const reached: (readonly string[] | undefined)[] = [] |
| 1091 | |
| 1092 | on('prompt.submit', ($, e) => { |
| 1093 | reached.push(e.context) |
| 1094 | |
| 1095 | return { text: e.text } |
| 1096 | }) |
| 1097 | |
| 1098 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1099 | await $.command.run(Fixtures.DIFF) |
| 1100 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1101 | await $.ui.render(Fixtures.PANE) |
| 1102 | await $.ui.press({ plugin: 'diff', key: 'ask:.env' }) |
| 1103 | await world.clock.settle() |
| 1104 | |
| 1105 | await $.prompt.submit(Fixtures.typedPromptOf('why?', [full])) |
| 1106 | await $.prompt.submit(Fixtures.typedPromptOf('and now?')) |
| 1107 | |
| 1108 | expect(reached).toEqual([[full], undefined]) |
| 1109 | |
| 1110 | expect(world.statuses.at(-1)).toBe( |
| 1111 | ".env's diff did not fit in the prompt and was dropped", |
| 1112 | ) |
| 1113 | }) |
| 1114 | |
| 1115 | test('a worktree opens on the base it kept', async ($, on) => { |
| 1116 | const world = Fixtures.inRepository(on, Fixtures.oneSecret(), { |
| 1117 | stored: { 'base:/main/wt': 'uncommitted' }, |
| 1118 | }) |
| 1119 | |
| 1120 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1121 | await $.command.run(Fixtures.DIFF) |
| 1122 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1123 | |
| 1124 | expect( |
| 1125 | Fixtures.stringsOf(await $.ui.render(Fixtures.PANE)), |
| 1126 | "the built-in's base line under the header", |
| 1127 | ).toContain('uncommitted (vs HEAD)') |
| 1128 | }) |
| 1129 | |
| 1130 | test('no repository is an answer kept until /clear', async ($, on) => { |
| 1131 | const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } = |
| 1132 | Fixtures.oneSecret() |
| 1133 | |
| 1134 | const script = { ...notYet } |
| 1135 | const world = Fixtures.inRepository(on, script) |
| 1136 | |
| 1137 | const probesOf = () => |
| 1138 | world.runs.filter(run => run.argv.includes('--show-toplevel')).length |
| 1139 | |
| 1140 | on('command.run', { command: 'clear' }, () => ({})) |
| 1141 | |
| 1142 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1143 | |
| 1144 | const before = await $.command.run(Fixtures.DIFF) |
| 1145 | |
| 1146 | script['rev-parse --path-format=absolute'] = worktree |
| 1147 | |
| 1148 | const again = await $.command.run(Fixtures.DIFF) |
| 1149 | const probesKept = probesOf() |
| 1150 | |
| 1151 | await $.command.run(Fixtures.CLEAR) |
| 1152 | |
| 1153 | const after = await $.command.run(Fixtures.DIFF) |
| 1154 | |
| 1155 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1156 | await $.ui.render(Fixtures.PANE) |
| 1157 | |
| 1158 | script['rev-parse --path-format=absolute'] = Fixtures.ELSEWHERE_LINES |
| 1159 | |
| 1160 | const third = $.command.run(Fixtures.DIFF) |
| 1161 | |
| 1162 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1163 | await third |
| 1164 | |
| 1165 | expect(before.text).toContain("isn't in a git repository") |
| 1166 | |
| 1167 | expect(again.text, "kept, as the built-in's answer is").toContain( |
| 1168 | "isn't in a git repository", |
| 1169 | ) |
| 1170 | |
| 1171 | expect(probesKept, 'one probe answered both').toBe(1) |
| 1172 | expect(after.text, '/clear forgot it').toBe('Diff panel shown') |
| 1173 | expect(world.opened[0]?.id).toBe('diff') |
| 1174 | expect(probesOf(), 'then one more, pinned from there on').toBe(2) |
| 1175 | |
| 1176 | expect( |
| 1177 | world.runs.some(run => run.argv.includes('--git-dir=/else/.git')), |
| 1178 | ).toBe(false) |
| 1179 | |
| 1180 | expect( |
| 1181 | world.runs |
| 1182 | .filter(run => run.argv.includes('--numstat')) |
| 1183 | .every(run => run.argv[1] === Fixtures.PINNED_LEAD[1]), |
| 1184 | ).toBe(true) |
| 1185 | }) |
| 1186 | |
| 1187 | test('a repository that appears later opens on first edit', async ($, on) => { |
| 1188 | const { 'rev-parse --path-format=absolute': worktree = '', ...notYet } = |
| 1189 | Fixtures.oneSecret() |
| 1190 | |
| 1191 | const script = { ...notYet } |
| 1192 | const world = Fixtures.inRepository(on, script) |
| 1193 | |
| 1194 | on('tool.call', () => ({ result: 'edited' })) |
| 1195 | |
| 1196 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1197 | await $.ui.render(Fixtures.HINT) |
| 1198 | |
| 1199 | script['rev-parse --path-format=absolute'] = worktree |
| 1200 | |
| 1201 | await $.tool.call({ |
| 1202 | tool: 'Edit', |
| 1203 | file_path: '/main/wt/.env', |
| 1204 | old_string: 'old', |
| 1205 | new_string: 'new', |
| 1206 | }) |
| 1207 | |
| 1208 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 1209 | |
| 1210 | expect(world.opened.map(pane => pane.id)).toEqual(['diff']) |
| 1211 | }) |
| 1212 | |
| 1213 | test('a git that cannot start is an answer, not a timeout', async ($, on) => { |
| 1214 | const runs: Args<'process.run'>[] = [] |
| 1215 | |
| 1216 | Fixtures.startsSession(on) |
| 1217 | |
| 1218 | on('process.run', ($, e) => { |
| 1219 | runs.push(e) |
| 1220 | |
| 1221 | return e.argv.includes('--show-toplevel') |
| 1222 | ? { deny: Fixtures.FAILED_START } |
| 1223 | : { value: Fixtures.gitIn(e.argv, Fixtures.oneSecret()) } |
| 1224 | }) |
| 1225 | |
| 1226 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1227 | |
| 1228 | const { text } = await $.command.run(Fixtures.DIFF) |
| 1229 | const probes = runs.filter(run => run.argv.includes('--show-toplevel')) |
| 1230 | |
| 1231 | expect(text).toContain("isn't in a git repository") |
| 1232 | expect(probes, "this /diff's alone; no retry").toHaveLength(1) |
| 1233 | }) |
| 1234 | |
| 1235 | test('/diff on a narrow terminal: the resize line', async ($, on) => { |
| 1236 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 1237 | |
| 1238 | await $.session.start(Fixtures.WORKTREE_SESSION) |
| 1239 | |
| 1240 | const narrow = await $.command.run( |
| 1241 | Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS - 1), |
| 1242 | ) |
| 1243 | |
| 1244 | const openedNarrow = [...world.opened] |
| 1245 | |
| 1246 | await $.command.run(Fixtures.diffAt(Limits.OPEN_MIN_COLUMNS)) |
| 1247 | |
| 1248 | expect(narrow.text).toContain( |
| 1249 | 'Resize your terminal to at least 110 columns to show the diff panel', |
| 1250 | ) |
| 1251 | |
| 1252 | expect(openedNarrow).toEqual([]) |
| 1253 | expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID]) |
| 1254 | }) |
| 1255 | }) |