Skip to content

Commit 38b8cf3

Browse files
committed
mods: each mod's tests run from the engine's seat, $ the engine's own and the hooks on() registers beneath the mod
1 parent f6cd2ca commit 38b8cf3

5 files changed

Lines changed: 677 additions & 0 deletions

File tree

‎mods/diff/tests/boot-probe.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type {
2+
CommandRunInput,
3+
ProcessRunResult,
4+
SessionStartInput,
5+
} from 'claude-code'
6+
import { clock, expect, seat, test } from 'claude-code/testing'
7+
8+
seat('builtin')
9+
10+
const SESSION: SessionStartInput = {
11+
surface: 'terminal',
12+
isInteractive: true,
13+
cwd: '/work',
14+
}
15+
const DIFF: CommandRunInput = {
16+
command: 'diff',
17+
args: '',
18+
origin: { kind: 'composer' },
19+
}
20+
const NOT_A_REPOSITORY: ProcessRunResult = {
21+
exitCode: 128,
22+
stdout: '',
23+
stderr: 'fatal: not a git repository',
24+
}
25+
const GIT_TIMEOUT_MS = 5000
26+
const GIT_HUNG = `git aborted: still running after ${GIT_TIMEOUT_MS}ms`
27+
28+
test('/diff at boot joins the boot probe, then asks again', async ($, on) => {
29+
const probes: (readonly string[])[] = []
30+
on('session.start', ($, e) => ({ cwd: e.cwd }))
31+
on('command.register', ($, e) => ({ value: { command: e.name } }))
32+
on('process.run', async ($, e) => {
33+
probes.push(e.argv)
34+
35+
if (probes.length === 1) {
36+
await clock.sleep(GIT_TIMEOUT_MS)
37+
38+
return { deny: GIT_HUNG }
39+
}
40+
41+
return { value: NOT_A_REPOSITORY }
42+
})
43+
44+
const booting = $.session.start(SESSION)
45+
await clock.advance(0)
46+
const ran = $.command.run(DIFF)
47+
await clock.advance(0)
48+
49+
expect(probes, 'the boot’s probe, which /diff joined').toHaveLength(1)
50+
51+
await clock.advance(GIT_TIMEOUT_MS)
52+
await booting
53+
54+
expect(await ran).toEqual({
55+
text: expect.stringContaining('isn’t in a git repository'),
56+
})
57+
expect(probes, 'then one more of its own').toHaveLength(2)
58+
})

‎mods/diff/tests/command.test.ts‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type {
2+
CommandRunInput,
3+
ProcessRunResult,
4+
SessionStartInput,
5+
} from 'claude-code'
6+
import { expect, seat, test } from 'claude-code/testing'
7+
8+
seat('builtin')
9+
10+
const SESSION: SessionStartInput = {
11+
surface: 'terminal',
12+
isInteractive: true,
13+
cwd: '/work',
14+
}
15+
const DIFF: CommandRunInput = {
16+
command: 'diff',
17+
args: '',
18+
origin: { kind: 'composer' },
19+
}
20+
const NOT_A_REPOSITORY: ProcessRunResult = {
21+
exitCode: 128,
22+
stdout: '',
23+
stderr: 'fatal: not a git repository',
24+
}
25+
const BUILTIN_HOLDS = '"/diff" refused: it is the built-in /diff'
26+
27+
test('outside a git repository /diff says so and opens nothing', async ($, on) => {
28+
const opened: string[] = []
29+
on('session.start', ($, e) => ({ cwd: e.cwd }))
30+
on('command.register', ($, e) => ({ value: { command: e.name } }))
31+
on('process.run', () => ({ value: NOT_A_REPOSITORY }))
32+
on('ui.open', ($, e, next) => {
33+
opened.push(e.id)
34+
return next(e)
35+
})
36+
37+
await $.session.start(SESSION)
38+
const { text } = await $.command.run(DIFF)
39+
40+
expect(text).toContain('isn’t in a git repository')
41+
expect(opened).toEqual([])
42+
})
43+
44+
test('a git that never answers is not “no repository”', async ($, on) => {
45+
on('session.start', ($, e) => ({ cwd: e.cwd }))
46+
on('command.register', ($, e) => ({ value: { command: e.name } }))
47+
on('process.run', () => ({
48+
deny: 'git aborted: still running after 5000ms',
49+
}))
50+
51+
await $.session.start(SESSION)
52+
const { text } = await $.command.run(DIFF)
53+
54+
expect(text).toContain('git didn’t answer')
55+
})
56+
57+
test('with the built-in holding /diff, the plugin stands down', async ($, on) => {
58+
const logged: string[] = []
59+
on('session.start', ($, e) => ({ cwd: e.cwd }))
60+
on('command.register', () => ({ deny: BUILTIN_HOLDS }))
61+
on('command.run', () => ({ text: 'the built-in /diff ran' }))
62+
on('ui.log', ($, e) => {
63+
logged.push(e.text)
64+
return { value: undefined }
65+
})
66+
67+
await $.session.start(SESSION)
68+
69+
expect(await $.command.run(DIFF)).toEqual({ text: 'the built-in /diff ran' })
70+
expect(logged).toEqual([])
71+
})
72+
73+
test('a refusal the built-in did not cause is said aloud', async ($, on) => {
74+
const logged: string[] = []
75+
on('session.start', ($, e) => ({ cwd: e.cwd }))
76+
on('command.register', () => ({ deny: '32 commands are registered already' }))
77+
on('ui.log', ($, e) => {
78+
logged.push(e.text)
79+
return { value: undefined }
80+
})
81+
82+
await $.session.start(SESSION)
83+
84+
expect(logged).toEqual([
85+
'could not register /diff: diff: $.command.register: 32 commands are ' +
86+
'registered already; the diff panel is unavailable this session',
87+
])
88+
})

‎mods/diff/tests/pane.test.ts‎

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

0 commit comments

Comments
 (0)