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

mods: tests sit in one describe each, diff's shared inputs in a fixtures folder, every decision and duplicate namedBrowse files
poteat authored on Sep 11, 2026 parent 97f86b9 · commit 936b92907ce1
10 changed files+417 -370compared with 97f86b9
Mmods/diff/tests/boot-probe.test.ts
Mmods/diff/tests/command.test.ts
Amods/diff/tests/fixtures/diff.ts
Amods/diff/tests/fixtures/index.ts
Amods/diff/tests/fixtures/not-a-repository.ts
Amods/diff/tests/fixtures/session.ts
Amods/diff/tests/fixtures/starts-session.ts
Mmods/diff/tests/pane.test.ts
Mmods/sec-default/tests/sec-default.test.ts
Mmods/telemetry/tests/telemetry.test.ts
@@ -1,58 +1,41 @@
1-import type {
2- CommandRunInput,
3- ProcessRunResult,
4- SessionStartInput,
5-} from 'claude-code'
6-import { clock, expect, test, tier } from 'claude-code/testing'
1+import { clock, describe, expect, test, tier } from 'claude-code/testing'
72
8-tier('builtin')
3+import Limits from '../hooks/limits'
4+import Fixtures from './fixtures'
95
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`
6+const GIT_HUNG = `git aborted: still running after ${Limits.GIT_TIMEOUT_MS}ms`
277
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)
8+tier('builtin')
349
35- if (probes.length === 1) {
36- await clock.sleep(GIT_TIMEOUT_MS)
10+describe('boot-probe', () => {
11+ test('/diff at boot joins the boot probe, then asks again', async ($, on) => {
12+ const probes: (readonly string[])[] = []
13+ Fixtures.startsSession(on)
14+ on('process.run', async ($, e) => {
15+ probes.push(e.argv)
3716
38- return { deny: GIT_HUNG }
39- }
17+ if (probes.length === 1) {
18+ await clock.sleep(Limits.GIT_TIMEOUT_MS)
4019
41- return { value: NOT_A_REPOSITORY }
42- })
20+ return { deny: GIT_HUNG }
21+ }
4322
44- const booting = $.session.start(SESSION)
45- await clock.advance(0)
46- const ran = $.command.run(DIFF)
47- await clock.advance(0)
23+ return { value: Fixtures.NOT_A_REPOSITORY }
24+ })
4825
49- expect(probes, 'the boot probe, which /diff joined').toHaveLength(1)
26+ const booting = $.session.start(Fixtures.SESSION)
27+ await clock.advance(0)
28+ const ran = $.command.run(Fixtures.DIFF)
29+ await clock.advance(0)
5030
51- await clock.advance(GIT_TIMEOUT_MS)
52- await booting
31+ expect(probes, 'the boot probe, which /diff joined').toHaveLength(1)
5332
54- expect(await ran).toEqual({
55- text: expect.stringContaining("isn't in a git repository"),
33+ await clock.advance(Limits.GIT_TIMEOUT_MS)
34+ await booting
35+
36+ expect(await ran).toEqual({
37+ text: expect.stringContaining("isn't in a git repository"),
38+ })
39+ expect(probes, 'then one more of its own').toHaveLength(2)
5640 })
57- expect(probes, 'then one more of its own').toHaveLength(2)
5841 })
@@ -1,88 +1,77 @@
1-import type {
2- CommandRunInput,
3- ProcessRunResult,
4- SessionStartInput,
5-} from 'claude-code'
6-import { expect, test, tier } from 'claude-code/testing'
1+import { describe, expect, test, tier } from 'claude-code/testing'
72
8-tier('builtin')
3+import Fixtures from './fixtures'
94
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-}
255 const BUILTIN_HOLDS = '"/diff" refused: it is the built-in /diff'
266
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- })
7+tier('builtin')
368
37- await $.session.start(SESSION)
38- const { text } = await $.command.run(DIFF)
9+describe('command', () => {
10+ test('outside a repository /diff says so, opens nothing', async ($, on) => {
11+ const opened: string[] = []
12+ Fixtures.startsSession(on)
13+ on('process.run', () => ({ value: Fixtures.NOT_A_REPOSITORY }))
14+ on('ui.open', ($, e, next) => {
15+ opened.push(e.id)
3916
40- expect(text).toContain("isn't in a git repository")
41- expect(opened).toEqual([])
42-})
17+ return next(e)
18+ })
4319
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- }))
20+ await $.session.start(Fixtures.SESSION)
21+ const { text } = await $.command.run(Fixtures.DIFF)
5022
51- await $.session.start(SESSION)
52- const { text } = await $.command.run(DIFF)
23+ expect(text).toContain("isn't in a git repository")
24+ expect(opened).toEqual([])
25+ })
5326
54- expect(text).toContain("git didn't answer")
55-})
27+ test('a git that never answers is not "no repository"', async ($, on) => {
28+ Fixtures.startsSession(on)
29+ on('process.run', () => ({
30+ deny: 'git aborted: still running after 5000ms',
31+ }))
5632
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 }
33+ await $.session.start(Fixtures.SESSION)
34+ const { text } = await $.command.run(Fixtures.DIFF)
35+
36+ expect(text).toContain("git didn't answer")
6537 })
6638
67- await $.session.start(SESSION)
39+ test('when the built-in holds /diff, the mod stands down', async ($, on) => {
40+ const logged: string[] = []
41+ on('session.start', ($, e) => ({ cwd: e.cwd }))
42+ on('command.register', () => ({ deny: BUILTIN_HOLDS }))
43+ on('command.run', () => ({ text: 'the built-in /diff ran' }))
44+ on('ui.log', ($, e) => {
45+ logged.push(e.text)
6846
69- expect(await $.command.run(DIFF)).toEqual({ text: 'the built-in /diff ran' })
70- expect(logged).toEqual([])
71-})
47+ return { value: undefined }
48+ })
7249
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 }
50+ await $.session.start(Fixtures.SESSION)
51+
52+ expect(await $.command.run(Fixtures.DIFF)).toEqual({
53+ text: 'the built-in /diff ran',
54+ })
55+ expect(logged).toEqual([])
8056 })
8157
82- await $.session.start(SESSION)
58+ test('a refusal the built-in did not cause is said aloud', async ($, on) => {
59+ const logged: string[] = []
60+ on('session.start', ($, e) => ({ cwd: e.cwd }))
61+ on('command.register', () => ({
62+ deny: '32 commands are registered already',
63+ }))
64+ on('ui.log', ($, e) => {
65+ logged.push(e.text)
8366
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- ])
67+ return { value: undefined }
68+ })
69+
70+ await $.session.start(Fixtures.SESSION)
71+
72+ expect(logged).toEqual([
73+ 'could not register /diff: diff: $.command.register: 32 commands are ' +
74+ 'registered already; the diff panel is unavailable this session',
75+ ])
76+ })
8877 })
@@ -1,0 +1,10 @@
1+import type { CommandRunInput } from 'claude-code'
2+
3+/**
4+ * The command as the person types it, with no arguments.
5+ */
6+export const DIFF: CommandRunInput = {
7+ command: 'diff',
8+ args: '',
9+ origin: { kind: 'composer' },
10+}
@@ -1,0 +1,6 @@
1+export * from './diff.js'
2+export * from './not-a-repository.js'
3+export * from './session.js'
4+export * from './starts-session.js'
5+
6+export * as default from '.'
@@ -1,0 +1,10 @@
1+import type { ProcessRunResult } from 'claude-code'
2+
3+/**
4+ * How git fails in a directory outside any repository.
5+ */
6+export const NOT_A_REPOSITORY: ProcessRunResult = {
7+ exitCode: 128,
8+ stdout: '',
9+ stderr: 'fatal: not a git repository',
10+}
@@ -1,0 +1,10 @@
1+import type { SessionStartInput } from 'claude-code'
2+
3+/**
4+ * An interactive terminal session in /work.
5+ */
6+export const SESSION: SessionStartInput = {
7+ surface: 'terminal',
8+ isInteractive: true,
9+ cwd: '/work',
10+}
@@ -1,0 +1,12 @@
1+import type { On } from 'claude-code'
2+
3+/**
4+ * Answers what every /diff session asks first: its start, with its own
5+ * directory, and each command it registers, under the name asked for.
6+ *
7+ * @param on the test's `on`
8+ */
9+export function startsSession(on: On) {
10+ on('session.start', ($, e) => ({ cwd: e.cwd }))
11+ on('command.register', ($, e) => ({ value: { command: e.name } }))
12+}
@@ -5,10 +5,10 @@
55 ProcessRunResult,
66 RenderElement,
77 RenderInput,
8- SessionStartInput,
98 } from 'claude-code'
109 import {
1110 clock,
11+ describe,
1212 expect,
1313 memoryStore,
1414 test,
@@ -16,18 +16,8 @@
1616 tier,
1717 } from 'claude-code/testing'
1818
19-tier('builtin')
19+import Fixtures from './fixtures'
2020
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-}
3121 const CLEAR: CommandRunInput = {
3222 command: 'clear',
3323 args: '',
@@ -64,10 +54,9 @@
6454 type: 'Text',
6555 children: ['? for shortcuts'],
6656 }
67-
6857 /**
69- * A repository at /work with one changed file, as git's output for each
70- * invocation whose command line holds the key.
58+ * Git's output in /work, where one file changed, for each invocation whose
59+ * command line holds the key.
7160 */
7261 const REPOSITORY: Readonly<Record<string, string>> = {
7362 'rev-parse --path-format=absolute': '/work\n/work/.git\n/work/.git\n',
@@ -76,7 +65,14 @@
7665 'ls-files': '',
7766 '-- app.ts': '@@ -1 +1 @@\n-const a = 1\n+const a = 2\n',
7867 }
68+/**
69+ * Longer than every wait the pane schedules before it fetches and draws
70+ * (its open probe, refresh debounce and redraw), short of its HEAD poll.
71+ */
72+const SETTLE_MS = 1000
7973
74+tier('builtin')
75+
8076 /**
8177 * What git answers in that repository; an invocation it does not know
8278 * fails as git does outside a repository.
@@ -87,101 +83,114 @@
8783
8884 return found
8985 ? { exitCode: 0, stdout: found[1], stderr: '' }
90- : { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository' }
86+ : Fixtures.NOT_A_REPOSITORY
9187 }
9288
9389 /**
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.
90+ * A hook that keeps each input it is asked with, and answers, beside what
91+ * it kept.
92+ *
93+ * @returns the hook, and each input in the order it came
9794 */
95+function keeping<E>() {
96+ const kept: E[] = []
97+
98+ function hook(_engine: unknown, e: E) {
99+ kept.push(e)
100+
101+ return { value: undefined }
102+ }
103+
104+ return { hook, kept }
105+}
106+
107+/**
108+ * A session in that repository, keeping each git run and each pane opened
109+ * or closed; the store starts empty and the engine draws the hint.
110+ */
98111 function inRepository(on: On) {
99112 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 } }))
113+ const opened = keeping<Args<'ui.open'>>()
114+ const closed = keeping<Args<'ui.close'>>()
115+ Fixtures.startsSession(on)
104116 on('process.run', ($, e) => {
105117 runs.push(e)
118+
106119 return { value: gitIn(e.argv) }
107120 })
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- })
121+ on('ui.open', opened.hook)
122+ on('ui.close', closed.hook)
116123 on('ui.invalidate', () => ({ value: undefined }))
117124 on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN)
118125 on('session.messages', () => ({ value: [] }))
119126 memoryStore(on)
120127
121- return { runs, opened, closed }
128+ return { runs, opened: opened.kept, closed: closed.kept }
122129 }
123130
124-test('/diff opens the pane over the changes in the session', async ($, on) => {
125- const world = inRepository(on)
131+describe('pane', () => {
132+ test('/diff opens the pane over the session changes', async ($, on) => {
133+ const world = inRepository(on)
126134
127- await $.session.start(SESSION)
135+ await $.session.start(Fixtures.SESSION)
128136
129- expect(await $.command.run(DIFF)).toEqual({})
130- expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
137+ expect(await $.command.run(Fixtures.DIFF)).toEqual({})
138+ expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
131139
132- await clock.advance(1000)
133- const drawn = textOf(await $.ui.render(PANE))
140+ await clock.advance(SETTLE_MS)
141+ const drawn = textOf(await $.ui.render(PANE))
134142
135- expect(drawn).toContain('1 file changed')
136- expect(drawn).toContain('app.ts')
137-})
143+ expect(drawn).toContain('1 file changed')
144+ expect(drawn).toContain('app.ts')
145+ })
138146
139-test('every git child is pinned to the repository and reads the C locale', async ($, on) => {
140- const world = inRepository(on)
147+ test('each git child runs pinned to /work, in C locale', async ($, on) => {
148+ const world = inRepository(on)
141149
142- await $.session.start(SESSION)
143- await $.command.run(DIFF)
144- await clock.advance(1000)
145- const [discovery, ...pinned] = world.runs
150+ await $.session.start(Fixtures.SESSION)
151+ await $.command.run(Fixtures.DIFF)
152+ await clock.advance(SETTLE_MS)
153+ const [discovery, ...pinned] = world.runs
146154
147- expect(discovery.argv).toContain('--show-toplevel')
148- expect(discovery.init?.cwd).toBeUndefined()
149- expect(pinned.length).toBeGreaterThan(0)
155+ expect(discovery?.argv).toContain('--show-toplevel')
156+ expect(discovery?.init?.cwd).toBeUndefined()
157+ expect(pinned.length).toBeGreaterThan(0)
150158
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-})
159+ for (const run of pinned) {
160+ expect(run.argv.slice(0, 3)).toEqual([
161+ 'git',
162+ '--git-dir=/work/.git',
163+ '--work-tree=/work',
164+ ])
165+ expect(run.init).toMatchObject({ cwd: '/work', env: { LC_ALL: 'C' } })
166+ }
167+ })
160168
161-test('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' }))
169+ test('a wide terminal opens the pane at the first edit', async ($, on) => {
170+ const world = inRepository(on)
171+ on('tool.call', () => ({ result: 'edited' }))
164172
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',
173+ await $.session.start(Fixtures.SESSION)
174+ await $.ui.render(HINT)
175+ await $.tool.call({
176+ tool: 'Edit',
177+ file_path: '/work/app.ts',
178+ old_string: '1',
179+ new_string: '2',
180+ })
181+ await clock.advance(SETTLE_MS)
182+
183+ expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
172184 })
173- await clock.advance(1000)
174185
175- expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
176-})
186+ test('/clear closes the pane it finds open', async ($, on) => {
187+ const world = inRepository(on)
188+ on('command.run', { command: 'clear' }, () => ({}))
177189
178-test('/clear closes the pane it finds open', async ($, on) => {
179- const world = inRepository(on)
180- on('command.run', { command: 'clear' }, () => ({}))
190+ await $.session.start(Fixtures.SESSION)
191+ await $.command.run(Fixtures.DIFF)
192+ await $.command.run(CLEAR)
181193
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'])
194+ expect(world.closed.map(pane => pane.id)).toEqual(['diff'])
195+ })
187196 })
@@ -6,8 +6,8 @@
66 Settings,
77 ToolInfo,
88 } from 'claude-code'
9+import { describe, expect, test, tier } from 'claude-code/testing'
910 import type { Plugin } from 'claude-code/testing'
10-import { expect, test, tier } from 'claude-code/testing'
1111
1212 tier('prepend')
1313
@@ -24,7 +24,7 @@
2424 const ALLOWLIST: Settings = { allowedMcpServers: [{ serverName: 'corp' }] }
2525 const NO_ALLOWLIST: Settings = { permissions: { allow: [] } }
2626 const MEMORY: PromptSectionInput = { name: 'memory', text: 'the org says hi' }
27-const TOOLS: ToolInfo[] = [
27+const TOOLS: readonly ToolInfo[] = [
2828 {
2929 name: 'mcp__corp__search',
3030 description: 'Searches the corp wiki.',
@@ -54,6 +54,13 @@
5454 })
5555
5656 /**
57+ * What registering a plugin's tool answers: the name it is called by.
58+ */
59+const registeredToolOf = (plugin: string, name: string) => ({
60+ value: { tool: `mcp__plugin_${plugin}__${name}` },
61+})
62+
63+/**
5764 * A session starting, where each tool a plugin asks for is registered and
5865 * kept by the name of the plugin that asked.
5966 */
@@ -63,7 +70,7 @@
6370 on('tool.register', ($, e, next) => {
6471 registered.push(next.origin.plugin)
6572
66- return { value: { tool: `mcp__plugin_${next.origin.plugin}__${e.name}` } }
73+ return registeredToolOf(next.origin.plugin, e.name)
6774 })
6875
6976 return registered
@@ -78,14 +85,16 @@
7885 register(on) {
7986 on('tool.list', async ($, e, next) => {
8087 const listed = await next(e)
88+ const { value } = listed
89+ const isListed = value !== undefined
8190
82- return listed.value === undefined
83- ? listed
84- : {
85- value: listed.value
91+ return isListed
92+ ? {
93+ value: value
8694 .filter(tool => !tool.name.startsWith('mcp__corp__'))
8795 .map(tool => ({ ...tool, description: 'relabeled' })),
8896 }
97+ : listed
8998 })
9099 },
91100 }
@@ -128,69 +137,71 @@
128137 },
129138 }
130139
131-test(
132- 'under an MCP allowlist a plugin the person installed may not add a tool',
133- { plugins: [registering('mine'), registering('bundled', 'builtin')] },
134- async ($, on) => {
135- on('settings.read', () => ({ value: ALLOWLIST }))
136- const registered = toolsRegistered(on)
140+describe('sec-default', () => {
141+ test(
142+ 'under an MCP allowlist a plugin the person installed may not add a tool',
143+ { plugins: [registering('mine'), registering('bundled', 'builtin')] },
144+ async ($, on) => {
145+ on('settings.read', () => ({ value: ALLOWLIST }))
146+ const registered = toolsRegistered(on)
137147
138- await $.session.start(SESSION)
148+ await $.session.start(SESSION)
139149
140- expect(registered).toEqual(['bundled'])
141- },
142-)
150+ expect(registered).toEqual(['bundled'])
151+ },
152+ )
143153
144-test(
145- 'with no allowlist, a plugin the person installed adds its tool',
146- { plugins: [registering('mine')] },
147- async ($, on) => {
148- on('settings.read', () => ({ value: NO_ALLOWLIST }))
149- const registered = toolsRegistered(on)
154+ test(
155+ 'with no allowlist, a plugin the person installed adds its tool',
156+ { plugins: [registering('mine')] },
157+ async ($, on) => {
158+ on('settings.read', () => ({ value: NO_ALLOWLIST }))
159+ const registered = toolsRegistered(on)
150160
151- await $.session.start(SESSION)
161+ await $.session.start(SESSION)
152162
153- expect(registered).toEqual(['mine'])
154- },
155-)
163+ expect(registered).toEqual(['mine'])
164+ },
165+ )
156166
157-test(
158- 'a policy that cannot be read counts as one in force',
159- { plugins: [registering('mine')] },
160- async ($, on) => {
161- on('settings.read', () => ({ deny: 'managed settings unreadable' }))
162- const registered = toolsRegistered(on)
167+ test(
168+ 'a policy that cannot be read counts as one in force',
169+ { plugins: [registering('mine')] },
170+ async ($, on) => {
171+ on('settings.read', () => ({ deny: 'managed settings unreadable' }))
172+ const registered = toolsRegistered(on)
163173
164- await $.session.start(SESSION)
174+ await $.session.start(SESSION)
165175
166- expect(registered).toEqual([])
167- },
168-)
176+ expect(registered).toEqual([])
177+ },
178+ )
169179
170-test(
171- 'the organization tools are listed as its tiers listed them',
172- { plugins: [relabeling, listing] },
173- async ($, on) => {
174- on('settings.read', () => ({ value: ALLOWLIST }))
175- on('tool.list', () => ({ value: TOOLS }))
180+ test(
181+ 'the organization tools are listed as its tiers listed them',
182+ { plugins: [relabeling, listing] },
183+ async ($, on) => {
184+ on('settings.read', () => ({ value: ALLOWLIST }))
185+ on('tool.list', () => ({ value: [...TOOLS] }))
176186
177- const { text } = await $.command.run(TOOLS_COMMAND)
187+ const { text } = await $.command.run(TOOLS_COMMAND)
178188
179- expect(text?.split('\n')).toEqual([
180- 'mcp__corp__search: Searches the corp wiki.',
181- 'Bash: relabeled',
182- ])
183- },
184-)
189+ expect(text?.split('\n')).toEqual([
190+ 'mcp__corp__search: Searches the corp wiki.',
191+ 'Bash: relabeled',
192+ ])
193+ },
194+ )
185195
186-test(
187- 'a prompt section skips the plugins the person installed, never the organization ones',
188- { plugins: [dropping, signing] },
189- async ($, on) => {
190- on('prompt.section', ($, e) => ({ text: e.text }))
196+ test(
197+ 'a prompt section passes over the plugins the person installed',
198+ { plugins: [dropping, signing] },
199+ async ($, on) => {
200+ on('prompt.section', ($, e) => ({ text: e.text }))
191201
192- expect(await $.prompt.section(MEMORY)).toEqual({
193- text: 'the org says hi (signed)',
194- })
195- },
196-)
202+ expect(await $.prompt.section(MEMORY)).toEqual({
203+ text: 'the org says hi (signed)',
204+ })
205+ },
206+ )
207+})
@@ -5,23 +5,25 @@
55 On,
66 SessionAuthorization,
77 } from 'claude-code'
8+import { describe, expect, memoryEnv, test, tier } from 'claude-code/testing'
89 import type { Plugin } from 'claude-code/testing'
9-import { expect, memoryEnv, test, tier } from 'claude-code/testing'
1010
1111 import type { LogEntry } from '../hooks/telemetry-types'
1212
13-tier('builtin')
14-
1513 const BEARER: SessionAuthorization = { handle: 'the-handle', kind: 'bearer' }
1614 const ACCEPTED: HttpResponse = { status: 200, ok: true, headers: {}, text: '' }
1715
18-const SURVEY: LogEntry = {
16+/**
17+ * A survey answered, as a plugin logs it.
18+ */
19+const surveyAnswer = (): LogEntry => ({
1920 event: 'survey_answered',
2021 props: { answer: 2, seen: true },
21-}
22+})
2223
2324 /**
24- * `/record <entry>`, which the recording plugin answers.
25+ * The command that has the recording plugin log an entry, typed as the
26+ * person would type it.
2527 */
2628 const record = (entry: LogEntry): CommandRunInput => ({
2729 command: 'record',
@@ -46,9 +48,21 @@
4648 }
4749
4850 /**
49- * A first-party session: its id and model, the credential it holds, and
50- * the ingest accepting each row, each post kept as it was made.
51+ * The batch a post to the ingest carries, as it was sent.
5152 */
53+const batchOf = (post: Args<'http.fetch'>): unknown =>
54+ JSON.parse(String(post.init?.body))
55+
56+tier('builtin')
57+
58+/**
59+ * Answers what the telemetry plugin reads of a session signed in first
60+ * party, and keeps each post the ingest accepts.
61+ *
62+ * @param on the test's `on`
63+ * @param authorization the credential the session holds
64+ * @returns each post, as it was made
65+ */
5266 function firstPartySession(
5367 on: On,
5468 authorization: SessionAuthorization = BEARER,
@@ -66,124 +80,117 @@
6680 return posts
6781 }
6882
69-/**
70- * The batch a post to the ingest carries, as it was sent.
71- */
72-function batchOf(post: Args<'http.fetch'>): unknown {
73- return JSON.parse(String(post.init?.body))
74-}
83+describe('telemetry', () => {
84+ test(
85+ 'a $.telemetry.log call from a plugin posts one first-party row',
86+ { plugins: [recording] },
87+ async ($, on) => {
88+ memoryEnv(on, { USER_TYPE: 'ant' })
89+ const posts = firstPartySession(on)
7590
76-test(
77- 'a $.telemetry.log call from a plugin posts one first-party row',
78- { plugins: [recording] },
79- async ($, on) => {
80- memoryEnv(on, { USER_TYPE: 'ant' })
81- const posts = firstPartySession(on)
82-
83- expect(await $.command.run(record(SURVEY))).toEqual({
84- text: 'sent',
85- })
86- expect(posts).toHaveLength(1)
87-
88- const [post] = posts
89-
90- expect(post.init).toMatchObject({ method: 'POST', auth: 'the-handle' })
91- expect(batchOf(post)).toMatchObject({
92- events: [
91+ expect(await $.command.run(record(surveyAnswer()))).toEqual({
92+ text: 'sent',
93+ })
94+ expect(posts.map(post => post.init)).toMatchObject([
95+ { method: 'POST', auth: 'the-handle' },
96+ ])
97+ expect(posts.map(batchOf)).toMatchObject([
9398 {
94- event_data: {
95- event_name: 'tengu_plugin_survey_answered',
96- session_id: 'the-session',
97- model: 'the-model',
98- user_type: 'ant',
99- },
99+ events: [
100+ {
101+ event_data: {
102+ event_name: 'tengu_plugin_survey_answered',
103+ session_id: 'the-session',
104+ model: 'the-model',
105+ user_type: 'ant',
106+ },
107+ },
108+ ],
100109 },
101- ],
102- })
103- },
104-)
110+ ])
111+ },
112+ )
105113
106-test(
107- 'a row already named tengu_ is sent under its own name',
108- { plugins: [recording] },
109- async ($, on) => {
110- memoryEnv(on, {})
111- const posts = firstPartySession(on)
114+ test(
115+ 'a row already named tengu_ is sent under its own name',
116+ { plugins: [recording] },
117+ async ($, on) => {
118+ memoryEnv(on, {})
119+ const posts = firstPartySession(on)
112120
113- await $.command.run(
114- record({ ...SURVEY, event: 'tengu_repl_diff_panel_shown' }),
115- )
121+ await $.command.run(
122+ record({ ...surveyAnswer(), event: 'tengu_repl_diff_panel_shown' }),
123+ )
116124
117- expect(posts.map(batchOf)).toMatchObject([
118- {
119- events: [
120- {
121- event_data: {
122- event_name: 'tengu_repl_diff_panel_shown',
123- user_type: 'external',
125+ expect(posts.map(batchOf)).toMatchObject([
126+ {
127+ events: [
128+ {
129+ event_data: {
130+ event_name: 'tengu_repl_diff_panel_shown',
131+ user_type: 'external',
132+ },
124133 },
125- },
126- ],
127- },
128- ])
129- },
130-)
134+ ],
135+ },
136+ ])
137+ },
138+ )
131139
132-test(
133- 'nothing is sent for a person who asked not to be tracked',
134- { plugins: [recording] },
135- async ($, on) => {
136- memoryEnv(on, { DO_NOT_TRACK: '1' })
137- const posts = firstPartySession(on)
140+ test(
141+ 'nothing is sent for a person who asked not to be tracked',
142+ { plugins: [recording] },
143+ async ($, on) => {
144+ memoryEnv(on, { DO_NOT_TRACK: '1' })
145+ const posts = firstPartySession(on)
146+ const { text } = await $.command.run(record(surveyAnswer()))
138147
139- expect(await $.command.run(record(SURVEY))).toEqual({
140- text: 'sent',
141- })
142- expect(posts).toEqual([])
143- },
144-)
148+ expect({ text, posts }).toEqual({ text: 'sent', posts: [] })
149+ },
150+ )
145151
146-test(
147- 'nothing is sent on a third-party provider',
148- { plugins: [recording] },
149- async ($, on) => {
150- memoryEnv(on, { CLAUDE_CODE_USE_BEDROCK: '1' })
151- const posts = firstPartySession(on)
152+ test(
153+ 'nothing is sent on a third-party provider',
154+ { plugins: [recording] },
155+ async ($, on) => {
156+ memoryEnv(on, { CLAUDE_CODE_USE_BEDROCK: '1' })
157+ const posts = firstPartySession(on)
152158
153- await $.command.run(record(SURVEY))
159+ await $.command.run(record(surveyAnswer()))
154160
155- expect(posts).toEqual([])
156- },
157-)
161+ expect(posts).toEqual([])
162+ },
163+ )
158164
159-test(
160- 'a session with no first-party credential is refused, nothing sent',
161- { plugins: [recording] },
162- async ($, on) => {
163- memoryEnv(on, {})
164- const posts = firstPartySession(on, null)
165- const { text } = await $.command.run(record(SURVEY))
165+ test(
166+ 'a session with no first-party credential is refused, nothing sent',
167+ { plugins: [recording] },
168+ async ($, on) => {
169+ memoryEnv(on, {})
170+ const posts = firstPartySession(on, null)
171+ const { text } = await $.command.run(record(surveyAnswer()))
166172
167- expect(text).toEndWith(
168- '$.telemetry.log: this session has no first-party credential to ' +
169- 'authorize',
170- )
171- expect(posts).toEqual([])
172- },
173-)
173+ expect(text).toEndWith(
174+ '$.telemetry.log: this session has no first-party credential to ' +
175+ 'authorize',
176+ )
177+ expect(posts).toEqual([])
178+ },
179+ )
174180
175-test(
176- 'free text in a row is refused, nothing sent',
177- { plugins: [recording] },
178- async ($, on) => {
179- memoryEnv(on, {})
180- const posts = firstPartySession(on)
181- const { text } = await $.command.run({
182- ...record(SURVEY),
183- args: '{"event":"survey_answered","props":{"note":"hello world"}}',
184- })
181+ test(
182+ 'free text in a row is refused, nothing sent',
183+ { plugins: [recording] },
184+ async ($, on) => {
185+ memoryEnv(on, {})
186+ const posts = firstPartySession(on)
187+ const { text } = await $.command.run({
188+ ...record(surveyAnswer()),
189+ args: '{"event":"survey_answered","props":{"note":"hello world"}}',
190+ })
185191
186- expect(text).toContain('props.note: free text is refused')
187- expect(posts).toEqual([])
188- },
189-)
192+ expect(text).toContain('props.note: free text is refused')
193+ expect(posts).toEqual([])
194+ },
195+ )
196+})