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
| @@ -36,14 +36,14 @@ | ||
| 36 | 36 | what several tests share sits under `tests/fixtures/`, one export a file. |
| 37 | 37 | |
| 38 | 38 | ```ts |
| 39 | -import { describe, expect, memoryClock, test, tier } from 'claude-code/testing' | |
| 39 | +import { describe, expect, mock, test, tier } from 'claude-code/testing' | |
| 40 | 40 | |
| 41 | 41 | tier('builtin') |
| 42 | 42 | |
| 43 | 43 | describe('register', () => { |
| 44 | 44 | test('outside a git repository /diff says so, opens nothing', async ($, on) => { |
| 45 | 45 | const opened: string[] = [] |
| 46 | - memoryClock(on) | |
| 46 | + mock.clock(on) | |
| 47 | 47 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 48 | 48 | on('command.register', ($, e) => ({ value: { command: e.name } })) |
| 49 | 49 | on('process.run', () => ({ |
| @@ -67,13 +67,13 @@ | ||
| 67 | 67 | }) |
| 68 | 68 | ``` |
| 69 | 69 | |
| 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. | |
| 70 | +The kit's `mock` answers the world beneath the mod from memory, noun by noun, | |
| 71 | +each member a plain function over `on` registering hooks where the test calls | |
| 72 | +it: `mock.env(on, variables)` for `$.env`, `mock.store(on, entries)` for | |
| 73 | +`$.store`, and `mock.clock(on)` for `$.clock`, whose `advance(ms)` resolves | |
| 74 | +every wait the mod asked for (`$.clock.sleep`, `after`, `every`) as the clock | |
| 75 | +crosses it. `$.ui.press({ plugin, key })` presses a `Button` the test | |
| 76 | +rendered, as a click in the terminal does. | |
| 77 | 77 | |
| 78 | 78 | `tsc -p mods/tsconfig.json` typechecks every mod's hooks and tests against |
| 79 | 79 | `types/`. |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import type { Args, On } from 'claude-code' |
| 2 | -import { memoryStore } from 'claude-code/testing' | |
| 2 | +import { mock } from 'claude-code/testing' | |
| 3 | 3 | |
| 4 | 4 | import { gitIn } from './git-in.js' |
| 5 | 5 | import { HINT_DRAWN } from './hint-drawn.js' |
| @@ -30,7 +30,7 @@ | ||
| 30 | 30 | on('ui.invalidate', () => ({ value: undefined })) |
| 31 | 31 | on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN) |
| 32 | 32 | on('session.messages', () => ({ value: [] })) |
| 33 | - memoryStore(on) | |
| 33 | + mock.store(on) | |
| 34 | 34 | |
| 35 | 35 | return { runs, opened: opened.kept, closed: closed.kept, clock } |
| 36 | 36 | } |
| @@ -13,5 +13,6 @@ | ||
| 13 | 13 | export * from './session.js' |
| 14 | 14 | export * from './settle-ms.js' |
| 15 | 15 | export * from './starts-session.js' |
| 16 | +export * from './text-of.js' | |
| 16 | 17 | |
| 17 | 18 | export * as default from '.' |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import type { On } from 'claude-code' |
| 2 | -import { memoryClock } from 'claude-code/testing' | |
| 3 | -import type { MemoryClock } from 'claude-code/testing' | |
| 2 | +import { mock } from 'claude-code/testing' | |
| 3 | +import type { MockClock } from 'claude-code/testing' | |
| 4 | 4 | |
| 5 | 5 | /** |
| 6 | 6 | * Answers what every /diff session asks first: its start, with its own |
| @@ -9,9 +9,9 @@ | ||
| 9 | 9 | * @param on the test's `on` |
| 10 | 10 | * @returns the clock the session reads, at 0 until the test moves it |
| 11 | 11 | */ |
| 12 | -export function startsSession(on: On): MemoryClock { | |
| 12 | +export function startsSession(on: On): MockClock { | |
| 13 | 13 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 14 | 14 | on('command.register', ($, e) => ({ value: { command: e.name } })) |
| 15 | 15 | |
| 16 | - return memoryClock(on) | |
| 16 | + return mock.clock(on) | |
| 17 | 17 | } |
| @@ -1,0 +1,26 @@ | ||
| 1 | +/** | |
| 2 | + * A rendered tree's text as it reads: its strings and its labels, in order. | |
| 3 | + * | |
| 4 | + * @param tree what `$.ui.render` resolved to, or a part of it | |
| 5 | + * @returns the text | |
| 6 | + */ | |
| 7 | +export function textOf(tree: unknown): string { | |
| 8 | + if (typeof tree === 'string' || typeof tree === 'number') { | |
| 9 | + return String(tree) | |
| 10 | + } | |
| 11 | + | |
| 12 | + if (Array.isArray(tree)) { | |
| 13 | + return tree.map(textOf).join('') | |
| 14 | + } | |
| 15 | + | |
| 16 | + if (typeof tree !== 'object' || !tree) { | |
| 17 | + return '' | |
| 18 | + } | |
| 19 | + | |
| 20 | + const props: unknown = Reflect.get(tree, 'props') | |
| 21 | + const label: unknown = | |
| 22 | + typeof props === 'object' && props ? Reflect.get(props, 'label') : undefined | |
| 23 | + const lead = typeof label === 'string' ? label : '' | |
| 24 | + | |
| 25 | + return `${lead}${textOf(Reflect.get(tree, 'children') ?? [])}` | |
| 26 | +} |
| @@ -1,11 +1,4 @@ | ||
| 1 | -import { | |
| 2 | - describe, | |
| 3 | - expect, | |
| 4 | - memoryClock, | |
| 5 | - test, | |
| 6 | - textOf, | |
| 7 | - tier, | |
| 8 | -} from 'claude-code/testing' | |
| 1 | +import { describe, expect, mock, test, tier } from 'claude-code/testing' | |
| 9 | 2 | |
| 10 | 3 | import Limits from '../hooks/limits' |
| 11 | 4 | import Fixtures from './fixtures' |
| @@ -73,7 +66,7 @@ | ||
| 73 | 66 | |
| 74 | 67 | test('when the built-in holds /diff, the mod stands down', async ($, on) => { |
| 75 | 68 | const logged: string[] = [] |
| 76 | - memoryClock(on) | |
| 69 | + mock.clock(on) | |
| 77 | 70 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 78 | 71 | on('command.register', () => ({ deny: Fixtures.BUILTIN_HOLDS })) |
| 79 | 72 | on('command.run', () => ({ text: 'the built-in /diff ran' })) |
| @@ -93,7 +86,7 @@ | ||
| 93 | 86 | |
| 94 | 87 | test('a refusal the built-in did not cause is said aloud', async ($, on) => { |
| 95 | 88 | const logged: string[] = [] |
| 96 | - memoryClock(on) | |
| 89 | + mock.clock(on) | |
| 97 | 90 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 98 | 91 | on('command.register', () => ({ |
| 99 | 92 | deny: '32 commands are registered already', |
| @@ -121,7 +114,7 @@ | ||
| 121 | 114 | expect(world.opened.map(pane => pane.id)).toEqual(['diff']) |
| 122 | 115 | |
| 123 | 116 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 124 | - const drawn = textOf(await $.ui.render(Fixtures.PANE)) | |
| 117 | + const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) | |
| 125 | 118 | |
| 126 | 119 | expect(drawn).toContain('1 file changed') |
| 127 | 120 | expect(drawn).toContain('app.ts') |
| @@ -1,4 +1,4 @@ | ||
| 1 | -import { describe, expect, memoryEnv, test, tier } from 'claude-code/testing' | |
| 1 | +import { describe, expect, mock, test, tier } from 'claude-code/testing' | |
| 2 | 2 | |
| 3 | 3 | import Fixtures from './fixtures' |
| 4 | 4 | |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | 'a $.telemetry.log call from a plugin posts one first-party row', |
| 10 | 10 | { plugins: [Fixtures.recording] }, |
| 11 | 11 | async ($, on) => { |
| 12 | - memoryEnv(on, { USER_TYPE: 'ant' }) | |
| 12 | + mock.env(on, { USER_TYPE: 'ant' }) | |
| 13 | 13 | const posts = Fixtures.firstPartySession(on) |
| 14 | 14 | |
| 15 | 15 | expect( |
| @@ -39,7 +39,7 @@ | ||
| 39 | 39 | 'a row already named tengu_ is sent under its own name', |
| 40 | 40 | { plugins: [Fixtures.recording] }, |
| 41 | 41 | async ($, on) => { |
| 42 | - memoryEnv(on, {}) | |
| 42 | + mock.env(on, {}) | |
| 43 | 43 | const posts = Fixtures.firstPartySession(on) |
| 44 | 44 | |
| 45 | 45 | await $.command.run( |
| @@ -68,7 +68,7 @@ | ||
| 68 | 68 | 'nothing is sent for a person who asked not to be tracked', |
| 69 | 69 | { plugins: [Fixtures.recording] }, |
| 70 | 70 | async ($, on) => { |
| 71 | - memoryEnv(on, { DO_NOT_TRACK: '1' }) | |
| 71 | + mock.env(on, { DO_NOT_TRACK: '1' }) | |
| 72 | 72 | const posts = Fixtures.firstPartySession(on) |
| 73 | 73 | const { text } = await $.command.run( |
| 74 | 74 | Fixtures.record(Fixtures.surveyAnswer()), |
| @@ -82,7 +82,7 @@ | ||
| 82 | 82 | 'nothing is sent on a third-party provider', |
| 83 | 83 | { plugins: [Fixtures.recording] }, |
| 84 | 84 | async ($, on) => { |
| 85 | - memoryEnv(on, { CLAUDE_CODE_USE_BEDROCK: '1' }) | |
| 85 | + mock.env(on, { CLAUDE_CODE_USE_BEDROCK: '1' }) | |
| 86 | 86 | const posts = Fixtures.firstPartySession(on) |
| 87 | 87 | |
| 88 | 88 | await $.command.run(Fixtures.record(Fixtures.surveyAnswer())) |
| @@ -95,7 +95,7 @@ | ||
| 95 | 95 | 'a session with no first-party credential is refused, nothing sent', |
| 96 | 96 | { plugins: [Fixtures.recording] }, |
| 97 | 97 | async ($, on) => { |
| 98 | - memoryEnv(on, {}) | |
| 98 | + mock.env(on, {}) | |
| 99 | 99 | const posts = Fixtures.firstPartySession(on, null) |
| 100 | 100 | const { text } = await $.command.run( |
| 101 | 101 | Fixtures.record(Fixtures.surveyAnswer()), |
| @@ -113,7 +113,7 @@ | ||
| 113 | 113 | 'free text in a row is refused, nothing sent', |
| 114 | 114 | { plugins: [Fixtures.recording] }, |
| 115 | 115 | async ($, on) => { |
| 116 | - memoryEnv(on, {}) | |
| 116 | + mock.env(on, {}) | |
| 117 | 117 | const posts = Fixtures.firstPartySession(on) |
| 118 | 118 | const { text } = await $.command.run({ |
| 119 | 119 | ...Fixtures.record(Fixtures.surveyAnswer()), |
Large file, diff not shown.