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
| 1 | import type { Args } from 'claude-code' |
| 2 | import { describe, expect, mock, test, tier } from 'claude-code/testing' |
| 3 | |
| 4 | import Fixtures from './fixtures' |
| 5 | |
| 6 | tier('builtin') |
| 7 | |
| 8 | describe('register', () => { |
| 9 | test( |
| 10 | 'a $.telemetry.log call from a plugin posts one first-party row', |
| 11 | { plugins: [Fixtures.recording] }, |
| 12 | async ($, on) => { |
| 13 | mock.env(on, { USER_TYPE: 'ant' }) |
| 14 | |
| 15 | const posts = Fixtures.firstPartySession(on) |
| 16 | |
| 17 | expect( |
| 18 | await $.command.run(Fixtures.record(Fixtures.surveyAnswer())), |
| 19 | ).toEqual({ text: 'sent' }) |
| 20 | |
| 21 | expect( |
| 22 | posts.map(post => [post.url, post.init?.method, post.init?.auth]), |
| 23 | ).toEqual([ |
| 24 | [ |
| 25 | 'https://api.anthropic.com/api/event_logging/v2/batch', |
| 26 | 'POST', |
| 27 | 'the-handle', |
| 28 | ], |
| 29 | ]) |
| 30 | |
| 31 | expect(posts.map(Fixtures.rowOf)).toEqual([ |
| 32 | { |
| 33 | event_type: 'ClaudeCodeInternalEvent', |
| 34 | hasTimestamp: true, |
| 35 | event_name: 'tengu_plugin_survey_answered', |
| 36 | session_id: 'the-session', |
| 37 | model: 'the-model', |
| 38 | user_type: 'ant', |
| 39 | metadata: { answer: 2, page: 'ready', seen: true }, |
| 40 | }, |
| 41 | ]) |
| 42 | }, |
| 43 | ) |
| 44 | |
| 45 | test( |
| 46 | 'a row already named tengu_ is sent under its own name', |
| 47 | { plugins: [Fixtures.recording] }, |
| 48 | async ($, on) => { |
| 49 | mock.env(on, {}) |
| 50 | |
| 51 | const posts = Fixtures.firstPartySession(on) |
| 52 | |
| 53 | await $.command.run( |
| 54 | Fixtures.record({ |
| 55 | ...Fixtures.surveyAnswer(), |
| 56 | event: 'tengu_repl_diff_panel_shown', |
| 57 | }), |
| 58 | ) |
| 59 | |
| 60 | expect(posts.map(Fixtures.batchOf)).toMatchObject([ |
| 61 | { |
| 62 | events: [ |
| 63 | { |
| 64 | event_data: { |
| 65 | event_name: 'tengu_repl_diff_panel_shown', |
| 66 | user_type: 'external', |
| 67 | }, |
| 68 | }, |
| 69 | ], |
| 70 | }, |
| 71 | ]) |
| 72 | }, |
| 73 | ) |
| 74 | |
| 75 | test( |
| 76 | 'nothing is sent where any switch has analytics off', |
| 77 | { plugins: [Fixtures.recording] }, |
| 78 | async ($, on) => { |
| 79 | let environment: Readonly<Record<string, string>> = {} |
| 80 | |
| 81 | on('env.get', ($, e) => ({ value: environment[e.name] })) |
| 82 | |
| 83 | const posts = Fixtures.firstPartySession(on) |
| 84 | const answers: (string | undefined)[] = [] |
| 85 | |
| 86 | for (const off of Fixtures.ANALYTICS_OFF_ENVIRONMENTS) { |
| 87 | environment = { USER_TYPE: 'ant', ...off } |
| 88 | |
| 89 | answers.push( |
| 90 | (await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))).text, |
| 91 | ) |
| 92 | } |
| 93 | |
| 94 | const withheld = posts.length |
| 95 | |
| 96 | environment = { USER_TYPE: 'ant' } |
| 97 | |
| 98 | await $.command.run(Fixtures.record(Fixtures.surveyAnswer())) |
| 99 | |
| 100 | expect(answers).toEqual( |
| 101 | Fixtures.ANALYTICS_OFF_ENVIRONMENTS.map(() => 'sent'), |
| 102 | ) |
| 103 | |
| 104 | expect(withheld, 'nothing posted while any switch was off').toBe(0) |
| 105 | expect(posts, 'then one row once every switch is clear').toHaveLength(1) |
| 106 | }, |
| 107 | ) |
| 108 | |
| 109 | test( |
| 110 | 'a third-party provider the host manages sends nothing too', |
| 111 | { plugins: [Fixtures.recording] }, |
| 112 | async ($, on) => { |
| 113 | mock.env(on, { |
| 114 | CLAUDE_CODE_USE_BEDROCK: '1', |
| 115 | CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1', |
| 116 | }) |
| 117 | |
| 118 | const posts = Fixtures.firstPartySession(on) |
| 119 | |
| 120 | const { text } = await $.command.run( |
| 121 | Fixtures.record(Fixtures.surveyAnswer()), |
| 122 | ) |
| 123 | |
| 124 | expect({ text, posts }).toEqual({ text: 'sent', posts: [] }) |
| 125 | }, |
| 126 | ) |
| 127 | |
| 128 | test( |
| 129 | 'a $.telemetry.mark call posts the feature row by its own name', |
| 130 | { plugins: [Fixtures.marking] }, |
| 131 | async ($, on) => { |
| 132 | mock.env(on, { USER_TYPE: 'ant' }) |
| 133 | |
| 134 | const posts = Fixtures.firstPartySession(on) |
| 135 | const answers: (string | undefined)[] = [] |
| 136 | |
| 137 | for (const entry of [ |
| 138 | { feature: 'learn_page', kind: 'ok' }, |
| 139 | { feature: 'learn_page', kind: 'sad', reason: 'blocked' }, |
| 140 | { feature: 'suggest_learning', kind: 'bad', reason: 'api_error' }, |
| 141 | ]) { |
| 142 | answers.push((await $.command.run(Fixtures.mark(entry))).text) |
| 143 | } |
| 144 | |
| 145 | expect(answers).toEqual(['sent', 'sent', 'sent']) |
| 146 | |
| 147 | expect( |
| 148 | posts.map(Fixtures.rowOf), |
| 149 | 'the feature events by their own names, no plugin prefix', |
| 150 | ).toMatchObject([ |
| 151 | { |
| 152 | event_name: 'tengu_feature_ok', |
| 153 | metadata: { feature_name: 'learn_page' }, |
| 154 | }, |
| 155 | { |
| 156 | event_name: 'tengu_feature_sad', |
| 157 | metadata: { feature_name: 'learn_page', error_code: 'blocked' }, |
| 158 | }, |
| 159 | { |
| 160 | event_name: 'tengu_feature_bad', |
| 161 | metadata: { |
| 162 | feature_name: 'suggest_learning', |
| 163 | error_code: 'api_error', |
| 164 | }, |
| 165 | }, |
| 166 | ]) |
| 167 | }, |
| 168 | ) |
| 169 | |
| 170 | test( |
| 171 | 'a mark with a bad kind or a wrong reason is refused, nothing sent', |
| 172 | { plugins: [Fixtures.marking] }, |
| 173 | async ($, on) => { |
| 174 | mock.env(on, { USER_TYPE: 'ant' }) |
| 175 | |
| 176 | const posts = Fixtures.firstPartySession(on) |
| 177 | |
| 178 | const refusalFor = async (entry: unknown) => |
| 179 | (await $.command.run(Fixtures.mark(entry))).text |
| 180 | |
| 181 | expect( |
| 182 | await refusalFor({ feature: 'learn_page', kind: 'meh' }), |
| 183 | ).toEndWith("$.telemetry.mark: kind: 'ok', 'sad' or 'bad'") |
| 184 | |
| 185 | expect( |
| 186 | await refusalFor({ feature: 'learn_page', kind: 'bad' }), |
| 187 | ).toEndWith( |
| 188 | '$.telemetry.mark: reason: a bad mark names why, a snake_case token', |
| 189 | ) |
| 190 | |
| 191 | expect( |
| 192 | await refusalFor({ feature: 'learn_page', kind: 'ok', reason: 'x' }), |
| 193 | ).toEndWith('$.telemetry.mark: reason: an ok mark carries none') |
| 194 | |
| 195 | expect(await refusalFor({ feature: 'Learn Page', kind: 'ok' })).toEndWith( |
| 196 | '$.telemetry.mark: takes a feature name, a snake_case token', |
| 197 | ) |
| 198 | |
| 199 | expect(await refusalFor('x')).toEndWith( |
| 200 | '$.telemetry.mark: takes one entry, { feature, kind, reason?, props? }', |
| 201 | ) |
| 202 | |
| 203 | expect(posts).toEqual([]) |
| 204 | }, |
| 205 | ) |
| 206 | |
| 207 | test( |
| 208 | 'a session with no first-party credential is refused, nothing sent', |
| 209 | { plugins: [Fixtures.recording] }, |
| 210 | async ($, on) => { |
| 211 | mock.env(on, {}) |
| 212 | |
| 213 | const posts = Fixtures.firstPartySession(on, null) |
| 214 | |
| 215 | const { text } = await $.command.run( |
| 216 | Fixtures.record(Fixtures.surveyAnswer()), |
| 217 | ) |
| 218 | |
| 219 | expect(text).toEndWith( |
| 220 | '$.telemetry.log: this session has no first-party credential to ' + |
| 221 | 'authorize', |
| 222 | ) |
| 223 | |
| 224 | expect(posts).toEqual([]) |
| 225 | }, |
| 226 | ) |
| 227 | |
| 228 | test( |
| 229 | "the ingest's refusal rejects the caller's promise", |
| 230 | { plugins: [Fixtures.recording] }, |
| 231 | async ($, on) => { |
| 232 | mock.env(on, {}) |
| 233 | |
| 234 | const posts = Fixtures.firstPartySession( |
| 235 | on, |
| 236 | Fixtures.BEARER, |
| 237 | Fixtures.REFUSED, |
| 238 | ) |
| 239 | |
| 240 | const { text } = await $.command.run( |
| 241 | Fixtures.record(Fixtures.surveyAnswer()), |
| 242 | ) |
| 243 | |
| 244 | expect(text).toEndWith('$.telemetry.log: the ingest answered 500') |
| 245 | expect(posts).toHaveLength(1) |
| 246 | }, |
| 247 | ) |
| 248 | |
| 249 | test( |
| 250 | 'free text and a malformed entry are refused, nothing sent', |
| 251 | { plugins: [Fixtures.recording] }, |
| 252 | async ($, on) => { |
| 253 | mock.env(on, {}) |
| 254 | |
| 255 | const posts = Fixtures.firstPartySession(on) |
| 256 | |
| 257 | const refusalFor = async (entry: unknown) => |
| 258 | ( |
| 259 | await $.command.run({ |
| 260 | ...Fixtures.record(Fixtures.surveyAnswer()), |
| 261 | args: JSON.stringify(entry), |
| 262 | }) |
| 263 | ).text |
| 264 | |
| 265 | expect( |
| 266 | await refusalFor({ event: 'x', props: { note: 'hello world' } }), |
| 267 | ).toEndWith( |
| 268 | '$.telemetry.log: props.note: free text is refused; a string is a ' + |
| 269 | 'Choice, { value, of: [...] }', |
| 270 | ) |
| 271 | |
| 272 | expect( |
| 273 | await refusalFor({ |
| 274 | event: 'x', |
| 275 | props: { page: { value: 'elsewhere', of: ['ready', 'later'] } }, |
| 276 | }), |
| 277 | ).toEndWith( |
| 278 | '$.telemetry.log: props.page.value: one of the members of `of`', |
| 279 | ) |
| 280 | |
| 281 | expect(await refusalFor({ event: 'Survey' })).toEndWith( |
| 282 | '$.telemetry.log: takes an event name, a snake_case token', |
| 283 | ) |
| 284 | |
| 285 | expect( |
| 286 | await refusalFor({ event: 'x', props: { 'a path': 1 } }), |
| 287 | ).toEndWith('$.telemetry.log: props: every key is a snake_case token') |
| 288 | |
| 289 | expect(await refusalFor('x')).toEndWith( |
| 290 | '$.telemetry.log: takes one entry, { event, props? }', |
| 291 | ) |
| 292 | |
| 293 | expect(posts).toEqual([]) |
| 294 | }, |
| 295 | ) |
| 296 | |
| 297 | test( |
| 298 | 'a number that is not finite is refused, nothing sent', |
| 299 | { |
| 300 | plugins: [ |
| 301 | { |
| 302 | name: 'counting', |
| 303 | register(on) { |
| 304 | on('command.run', { command: 'count' }, $ => |
| 305 | $.telemetry.log({ event: 'x', props: { n: Number.NaN } }).then( |
| 306 | () => ({ text: 'sent' }), |
| 307 | (error: unknown) => ({ text: String(error) }), |
| 308 | ), |
| 309 | ) |
| 310 | }, |
| 311 | }, |
| 312 | ], |
| 313 | }, |
| 314 | async ($, on) => { |
| 315 | mock.env(on, {}) |
| 316 | |
| 317 | const posts = Fixtures.firstPartySession(on) |
| 318 | |
| 319 | const { text } = await $.command.run({ |
| 320 | command: 'count', |
| 321 | args: '', |
| 322 | origin: { kind: 'composer' }, |
| 323 | presentation: Fixtures.FULLSCREEN, |
| 324 | }) |
| 325 | |
| 326 | expect(text).toEndWith('$.telemetry.log: props.n: a number is finite') |
| 327 | expect(posts).toEqual([]) |
| 328 | }, |
| 329 | ) |
| 330 | |
| 331 | test( |
| 332 | 'a failed authorize is not memoized; a retry sends', |
| 333 | { plugins: [Fixtures.recording] }, |
| 334 | async ($, on) => { |
| 335 | const posts: Args<'http.fetch'>[] = [] |
| 336 | |
| 337 | let authorizations = 0 |
| 338 | |
| 339 | mock.env(on, {}) |
| 340 | on('session.id', () => ({ value: 'the-session' })) |
| 341 | on('session.model', () => ({ value: 'the-model' })) |
| 342 | |
| 343 | on('session.authorize', () => { |
| 344 | authorizations += 1 |
| 345 | |
| 346 | const isFirstAuthorization = authorizations === 1 |
| 347 | |
| 348 | return isFirstAuthorization |
| 349 | ? { deny: 'transient' } |
| 350 | : { value: Fixtures.BEARER } |
| 351 | }) |
| 352 | |
| 353 | on('http.fetch', ($, e) => { |
| 354 | posts.push(e) |
| 355 | |
| 356 | return { value: Fixtures.ACCEPTED } |
| 357 | }) |
| 358 | |
| 359 | const first = await $.command.run(Fixtures.record({ event: 'x' })) |
| 360 | const postsAfterFirst = posts.length |
| 361 | const second = await $.command.run(Fixtures.record({ event: 'x' })) |
| 362 | |
| 363 | expect(first.text).toContain('transient') |
| 364 | expect(postsAfterFirst).toBe(0) |
| 365 | expect(second.text).toBe('sent') |
| 366 | expect(posts).toHaveLength(1) |
| 367 | }, |
| 368 | ) |
| 369 | }) |