Skip to content

Commit 59bb885

Browse files
committed
mods: each test file is named for the hooks/ subject it covers and holds one describe, shared inputs under tests/fixtures; memoryClock answers $.clock beneath the mod, and the diff pane's close button is pressed through $.ui.press
1 parent f43a16b commit 59bb885

45 files changed

Lines changed: 921 additions & 737 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎mods/README.md‎

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,33 +30,51 @@ the test registers with `on` sit beneath the mod, where the rest of the world
3030
would be, and nothing is beneath them: a call they leave unanswered throws,
3131
naming its event.
3232

33+
A test file is named for what it covers under `hooks/` (`register.test.ts`
34+
beside `hooks/register.ts`, `git.test.ts` beside `hooks/git/`), and holds its
35+
imports, the tier the mod loads in, and one `describe` titled with that name;
36+
what several tests share sits under `tests/fixtures/`, one export a file.
37+
3338
```ts
34-
import { expect, test, tier } from 'claude-code/testing'
39+
import { describe, expect, memoryClock, test, tier } from 'claude-code/testing'
3540

3641
tier('builtin')
3742

38-
test('outside a git repository /diff says so and opens nothing', async ($, on) => {
39-
const opened: string[] = []
40-
on('session.start', ($, e) => ({ cwd: e.cwd }))
41-
on('command.register', ($, e) => ({ value: { command: e.name } }))
42-
on('process.run', () => ({ value: { exitCode: 128, stdout: '', stderr: '' } }))
43-
on('ui.open', ($, e, next) => {
44-
opened.push(e.id)
45-
return next(e)
46-
})
43+
describe('register', () => {
44+
test('outside a git repository /diff says so, opens nothing', async ($, on) => {
45+
const opened: string[] = []
46+
memoryClock(on)
47+
on('session.start', ($, e) => ({ cwd: e.cwd }))
48+
on('command.register', ($, e) => ({ value: { command: e.name } }))
49+
on('process.run', () => ({
50+
value: { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository' },
51+
}))
52+
on('ui.open', ($, e, next) => {
53+
opened.push(e.id)
54+
return next(e)
55+
})
4756

48-
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })
49-
const { text } = await $.command.run({
50-
command: 'diff',
51-
args: '',
52-
origin: { kind: 'composer' },
53-
})
57+
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })
58+
const { text } = await $.command.run({
59+
command: 'diff',
60+
args: '',
61+
origin: { kind: 'composer' },
62+
})
5463

55-
expect(text).toContain("isn't in a git repository")
56-
expect(opened).toEqual([])
64+
expect(text).toContain("isn't in a git repository")
65+
expect(opened).toEqual([])
66+
})
5767
})
5868
```
5969

70+
The kit's helpers are plain functions over `on`, each answering one noun
71+
beneath the mod where the test calls it: `memoryEnv(on, variables)` for
72+
`$.env`, `memoryStore(on, entries)` for `$.store`, and `memoryClock(on)` for
73+
`$.clock`, whose `advance(ms)` resolves every wait the mod asked for
74+
(`$.clock.sleep`, `after`, `every`) as the clock crosses it. `textOf(tree)`
75+
reads a rendered tree's text, and `$.ui.press({ plugin, key })` presses a
76+
`Button` the test rendered, as a click in the terminal does.
77+
6078
`tsc -p mods/tsconfig.json` typechecks every mod's hooks and tests against
6179
`types/`.
6280

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

Lines changed: 0 additions & 41 deletions
This file was deleted.

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

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* How `$.command.register` refuses `/diff` while the built-in one is listed.
3+
*/
4+
export const BUILTIN_HOLDS = '"/diff" refused: it is the built-in /diff'

‎mods/diff/tests/fixtures/clear.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { CommandRunInput } from 'claude-code'
2+
3+
/**
4+
* The command that starts the conversation over, as the person types it.
5+
*/
6+
export const CLEAR: CommandRunInput = {
7+
command: 'clear',
8+
args: '',
9+
origin: { kind: 'composer' },
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Limits from '../../hooks/limits'
2+
3+
/**
4+
* How `$.process.run` refuses a git that outran its timeout.
5+
*/
6+
export const GIT_HUNG =
7+
'git aborted: still running after ' + `${Limits.GIT_TIMEOUT_MS}ms`

‎mods/diff/tests/fixtures/git-in.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { ProcessRunResult } from 'claude-code'
2+
3+
import { NOT_A_REPOSITORY } from './not-a-repository.js'
4+
import { REPOSITORY } from './repository.js'
5+
6+
/**
7+
* What git answers in /work (REPOSITORY); an invocation it does not know
8+
* fails as git does outside a repository.
9+
*
10+
* @param argv the invocation, program first
11+
* @returns git's exit code and output
12+
*/
13+
export function gitIn(argv: readonly string[]): ProcessRunResult {
14+
const line = argv.join(' ')
15+
const found = Object.entries(REPOSITORY).find(([key]) => line.includes(key))
16+
17+
return found
18+
? { exitCode: 0, stdout: found[1], stderr: '' }
19+
: NOT_A_REPOSITORY
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { RenderElement } from 'claude-code'
2+
3+
/**
4+
* What the engine draws for the hint, standing in beneath the plugin.
5+
*/
6+
export const HINT_DRAWN: RenderElement = {
7+
type: 'Text',
8+
children: ['? for shortcuts'],
9+
}

‎mods/diff/tests/fixtures/hint.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { RenderInput } from 'claude-code'
2+
3+
/**
4+
* The prompt's hint on a 160-column terminal: drawing it is how the plugin
5+
* learns how wide the terminal is.
6+
*/
7+
export const HINT: RenderInput<'PromptHint'> = {
8+
component: 'PromptHint',
9+
surface: 'terminal',
10+
requestId: 'hint',
11+
viewport: { columns: 160, rows: 40 },
12+
props: { isDraft: false, isWorking: false, hint: '' },
13+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Args, On } from 'claude-code'
2+
import { memoryStore } from 'claude-code/testing'
3+
4+
import { gitIn } from './git-in.js'
5+
import { HINT_DRAWN } from './hint-drawn.js'
6+
import { keeping } from './keeping.js'
7+
import { startsSession } from './starts-session.js'
8+
9+
/**
10+
* A session in /work (REPOSITORY), keeping each git run and each pane
11+
* opened or closed.
12+
*
13+
* The store starts empty, the clock at 0, and the engine draws the hint.
14+
*
15+
* @param on the test's `on`
16+
* @returns the runs, the panes opened and closed, and the session's clock
17+
*/
18+
export function inRepository(on: On) {
19+
const runs: Args<'process.run'>[] = []
20+
const opened = keeping<Args<'ui.open'>>()
21+
const closed = keeping<Args<'ui.close'>>()
22+
const clock = startsSession(on)
23+
on('process.run', ($, e) => {
24+
runs.push(e)
25+
26+
return { value: gitIn(e.argv) }
27+
})
28+
on('ui.open', opened.hook)
29+
on('ui.close', closed.hook)
30+
on('ui.invalidate', () => ({ value: undefined }))
31+
on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN)
32+
on('session.messages', () => ({ value: [] }))
33+
memoryStore(on)
34+
35+
return { runs, opened: opened.kept, closed: closed.kept, clock }
36+
}

0 commit comments

Comments
 (0)