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 / pane.test.ts
187 lines · 4.9 KBRaw
1import type {
2 Args,
3 CommandRunInput,
4 On,
5 ProcessRunResult,
6 RenderElement,
7 RenderInput,
8 SessionStartInput,
9} from 'claude-code'
10import {
11 clock,
12 expect,
13 memoryStore,
14 test,
15 textOf,
16 tier,
17} from 'claude-code/testing'
18
19tier('builtin')
20
21const SESSION: SessionStartInput = {
22 surface: 'terminal',
23 isInteractive: true,
24 cwd: '/work',
25}
26const DIFF: CommandRunInput = {
27 command: 'diff',
28 args: '',
29 origin: { kind: 'composer' },
30}
31const CLEAR: CommandRunInput = {
32 command: 'clear',
33 args: '',
34 origin: { kind: 'composer' },
35}
36const PANE: RenderInput<'Pane'> = {
37 component: 'Pane',
38 surface: 'terminal',
39 requestId: 'diff',
40 viewport: { columns: 160, rows: 40 },
41 props: {
42 title: 'Diff',
43 isFocused: false,
44 bodyColumns: 80,
45 placement: 'dock',
46 scroll: { offset: 0, bodyRows: 30 },
47 },
48}
49/**
50 * The prompt's hint on a 160-column terminal: drawing it is how the plugin
51 * learns how wide the terminal is.
52 */
53const HINT: RenderInput<'PromptHint'> = {
54 component: 'PromptHint',
55 surface: 'terminal',
56 requestId: 'hint',
57 viewport: { columns: 160, rows: 40 },
58 props: { isDraft: false, isWorking: false, hint: '' },
59}
60/**
61 * What the engine draws for the hint, standing in beneath the plugin.
62 */
63const HINT_DRAWN: RenderElement = {
64 type: 'Text',
65 children: ['? for shortcuts'],
66}
67
68/**
69 * A repository at /work with one changed file, as git's output for each
70 * invocation whose command line holds the key.
71 */
72const REPOSITORY: Readonly<Record<string, string>> = {
73 'rev-parse --path-format=absolute': '/work\n/work/.git\n/work/.git\n',
74 'HEAD --shortstat': ' 1 file changed, 1 insertion(+)',
75 'HEAD --numstat': '1\t0\tapp.ts\0',
76 'ls-files': '',
77 '-- app.ts': '@@ -1 +1 @@\n-const a = 1\n+const a = 2\n',
78}
79
80/**
81 * What git answers in that repository; an invocation it does not know
82 * fails as git does outside a repository.
83 */
84function gitIn(argv: readonly string[]): ProcessRunResult {
85 const line = argv.join(' ')
86 const found = Object.entries(REPOSITORY).find(([key]) => line.includes(key))
87
88 return found
89 ? { exitCode: 0, stdout: found[1], stderr: '' }
90 : { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository' }
91}
92
93/**
94 * A session in that repository: /diff registers, git answers, the store
95 * starts empty, the engine draws the prompt's hint, and what the plugin
96 * asks of the world (each git run, each pane opened or closed) is kept.
97 */
98function inRepository(on: On) {
99 const runs: Args<'process.run'>[] = []
100 const opened: Args<'ui.open'>[] = []
101 const closed: Args<'ui.close'>[] = []
102 on('session.start', ($, e) => ({ cwd: e.cwd }))
103 on('command.register', ($, e) => ({ value: { command: e.name } }))
104 on('process.run', ($, e) => {
105 runs.push(e)
106 return { value: gitIn(e.argv) }
107 })
108 on('ui.open', ($, e) => {
109 opened.push(e)
110 return { value: undefined }
111 })
112 on('ui.close', ($, e) => {
113 closed.push(e)
114 return { value: undefined }
115 })
116 on('ui.invalidate', () => ({ value: undefined }))
117 on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN)
118 on('session.messages', () => ({ value: [] }))
119 memoryStore(on)
120
121 return { runs, opened, closed }
122}
123
124test('/diff opens the pane over the changes in the session', async ($, on) => {
125 const world = inRepository(on)
126
127 await $.session.start(SESSION)
128
129 expect(await $.command.run(DIFF)).toEqual({})
130 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
131
132 await clock.advance(1000)
133 const drawn = textOf(await $.ui.render(PANE))
134
135 expect(drawn).toContain('1 file changed')
136 expect(drawn).toContain('app.ts')
137})
138
139test('every git child is pinned to the repository and reads the C locale', async ($, on) => {
140 const world = inRepository(on)
141
142 await $.session.start(SESSION)
143 await $.command.run(DIFF)
144 await clock.advance(1000)
145 const [discovery, ...pinned] = world.runs
146
147 expect(discovery.argv).toContain('--show-toplevel')
148 expect(discovery.init?.cwd).toBeUndefined()
149 expect(pinned.length).toBeGreaterThan(0)
150
151 for (const run of pinned) {
152 expect(run.argv.slice(0, 3)).toEqual([
153 'git',
154 '--git-dir=/work/.git',
155 '--work-tree=/work',
156 ])
157 expect(run.init).toMatchObject({ cwd: '/work', env: { LC_ALL: 'C' } })
158 }
159})
160
161test('the first edit Claude makes opens the pane on a wide terminal', async ($, on) => {
162 const world = inRepository(on)
163 on('tool.call', () => ({ result: 'edited' }))
164
165 await $.session.start(SESSION)
166 await $.ui.render(HINT)
167 await $.tool.call({
168 tool: 'Edit',
169 file_path: '/work/app.ts',
170 old_string: '1',
171 new_string: '2',
172 })
173 await clock.advance(1000)
174
175 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
176})
177
178test('/clear closes the pane it finds open', async ($, on) => {
179 const world = inRepository(on)
180 on('command.run', { command: 'clear' }, () => ({}))
181
182 await $.session.start(SESSION)
183 await $.command.run(DIFF)
184 await $.command.run(CLEAR)
185
186 expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
187})