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

claude-code / mods / telemetry / tests / register.test.ts
740 lines · 21.1 KBRaw
1import { describe, expect, mock, test, tier } from 'claude-code/testing'
2
3import Hooks from '../hooks'
4import Fixtures from './fixtures'
5
6tier('builtin')
7
8const tiers = ['builtin', 'core', 'user', 'prepend', 'append'] as const
9
10describe('register', () => {
11 test(
12 'rows queued through $.telemetry.log go out as one complete batch',
13 { plugins: [Fixtures.recording] },
14 async ($, on) => {
15 mock.env(on, Fixtures.SENDING_ENV)
16
17 const session = Fixtures.firstPartySession(on)
18
19 await $.session.start(Fixtures.STARTED)
20
21 const first = await $.command.run(
22 Fixtures.record(Fixtures.surveyAnswer()),
23 )
24
25 await $.command.run(Fixtures.record({ event: 'again', props: { n: 1 } }))
26
27 expect(first).toEqual({ text: 'queued' })
28 expect(session.posts, 'nothing before the window passes').toEqual([])
29
30 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
31
32 expect(
33 session.posts.map(post => [
34 post.url,
35 post.init?.method,
36 post.init?.auth,
37 post.init?.headers,
38 ]),
39 ).toEqual([
40 [
41 'https://api.anthropic.com/api/event_logging/v2/batch',
42 'POST',
43 'the-handle',
44 {
45 'Content-Type': 'application/json',
46 'x-service-name': 'claude-code',
47 },
48 ],
49 ])
50
51 expect(Fixtures.rowsOf(session)).toHaveLength(2)
52 expect(Fixtures.rowsOf(session)[0]).toEqual(Fixtures.EXPECTED_ROW)
53
54 expect(Fixtures.rowsOf(session)[1]).toMatchObject({
55 event_name: 'tengu_plugin_again',
56 metadata: { rh: Fixtures.REMOTE_HASH, n: 1 },
57 })
58
59 expect(session.lines).toEqual(['telemetry: sent 2 row(s)'])
60 },
61 )
62
63 test(
64 'an engine without the version read leaves those three columns empty',
65 { plugins: [Fixtures.recording] },
66 async ($, on) => {
67 mock.env(on, Fixtures.SENDING_ENV)
68
69 const session = Fixtures.firstPartySession(on, {
70 engineVersion: 'unanswered',
71 })
72
73 await $.session.start(Fixtures.STARTED)
74 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
75 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
76
77 const { version, version_base, build_time, ...rest } =
78 Fixtures.EXPECTED_ROW.env
79
80 expect([version, version_base, build_time]).toEqual([
81 Fixtures.ENGINE_VERSION.version,
82 Fixtures.ENGINE_VERSION.base,
83 Fixtures.ENGINE_VERSION.builtAt,
84 ])
85
86 expect(Fixtures.rowsOf(session)).toEqual([
87 { ...Fixtures.EXPECTED_ROW, env: rest },
88 ])
89
90 expect(session.lines).toEqual([
91 expect.stringContaining(
92 "telemetry: the engine's version is not readable here, so the " +
93 'version columns stay empty (',
94 ),
95 'telemetry: sent 1 row(s)',
96 ])
97
98 expect(session.lines[0]).toContain('session.version')
99 },
100 )
101
102 test(
103 'a full queue goes out at once, before the window',
104 { plugins: [Fixtures.recording] },
105 async ($, on) => {
106 mock.env(on, Fixtures.SENDING_ENV)
107
108 const session = Fixtures.firstPartySession(on)
109
110 for (let row = 0; row < Hooks.BATCH_ROWS; row++) {
111 await $.command.run(Fixtures.record({ event: 'x', props: { row } }))
112 }
113
114 await session.clock.settle()
115
116 expect(session.posts).toHaveLength(1)
117 expect(Fixtures.rowsOf(session)).toHaveLength(Hooks.BATCH_ROWS)
118 },
119 )
120
121 test(
122 'the end of the session sends what still waits',
123 { plugins: [Fixtures.recording] },
124 async ($, on) => {
125 mock.env(on, Fixtures.SENDING_ENV)
126
127 const session = Fixtures.firstPartySession(on)
128
129 await $.session.start(Fixtures.STARTED)
130 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
131 await $.session.end(Fixtures.ENDED)
132
133 expect(Fixtures.rowsOf(session)).toEqual([Fixtures.EXPECTED_ROW])
134 },
135 )
136
137 test(
138 'a tengu_ name is kept; an external build sends no address',
139 { plugins: [Fixtures.recording] },
140 async ($, on) => {
141 mock.env(on, { ...Fixtures.SENDING_ENV, USER_TYPE: 'external' })
142
143 const session = Fixtures.firstPartySession(on)
144
145 await $.command.run(
146 Fixtures.record({
147 ...Fixtures.surveyAnswer(),
148 event: 'tengu_repl_diff_panel_shown',
149 }),
150 )
151
152 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
153
154 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
155 event_name: 'tengu_repl_diff_panel_shown',
156 user_type: 'external',
157 device_id: Fixtures.DEVICE_ID,
158 })
159
160 expect(Fixtures.rowsOf(session)[0]).not.toHaveProperty('email')
161 },
162 )
163
164 test(
165 'nothing is sent where any switch has analytics off',
166 { plugins: [Fixtures.recording] },
167 async ($, on) => {
168 let environment: Readonly<Record<string, string>> = {}
169
170 on('env.get', ($, e) => ({ value: environment[e.name] }))
171
172 const session = Fixtures.firstPartySession(on)
173 const answers: (string | undefined)[] = []
174
175 for (const off of Fixtures.ANALYTICS_OFF_ENVIRONMENTS) {
176 environment = { ...Fixtures.SENDING_ENV, ...off }
177
178 answers.push(
179 (await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))).text,
180 )
181
182 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
183 }
184
185 const withheld = session.posts.length
186
187 environment = Fixtures.SENDING_ENV
188
189 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
190 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
191
192 expect(answers).toEqual(
193 Fixtures.ANALYTICS_OFF_ENVIRONMENTS.map(() => 'queued'),
194 )
195
196 expect(withheld, 'nothing posted while any switch was off').toBe(0)
197
198 expect(
199 session.posts,
200 'one batch once every switch is clear',
201 ).toHaveLength(1)
202 },
203 )
204
205 test(
206 'a gateway the managed policy pins sends nothing',
207 { plugins: [Fixtures.recording] },
208 async ($, on) => {
209 mock.env(on, Fixtures.SENDING_ENV)
210
211 const session = Fixtures.firstPartySession(on, {
212 policy: { kind: 'settings', settings: { forceLoginMethod: 'gateway' } },
213 })
214
215 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
216 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
217
218 expect(session.posts).toEqual([])
219 },
220 )
221
222 test(
223 'unreadable switches send nothing, and the debug log says why',
224 { plugins: [Fixtures.recording] },
225 async ($, on) => {
226 mock.env(on, Fixtures.SENDING_ENV)
227
228 const session = Fixtures.firstPartySession(on, {
229 policy: { kind: 'unreadable' },
230 })
231
232 const { text } = await $.command.run(
233 Fixtures.record(Fixtures.surveyAnswer()),
234 )
235
236 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
237
238 expect({ text, posts: session.posts }).toEqual({
239 text: 'queued',
240 posts: [],
241 })
242
243 expect(session.reads, 'nothing of the machine is read either').toEqual([])
244
245 expect(session.lines.at(-1)).toStartWith(
246 'telemetry: the analytics switches could not be read, so nothing is ' +
247 'sent (',
248 )
249 },
250 )
251
252 test(
253 'a host-managed third-party provider follows DISABLE_TELEMETRY',
254 { plugins: [Fixtures.recording] },
255 async ($, on) => {
256 let environment = Fixtures.HOST_MANAGED_BEDROCK_ENV
257
258 on('env.get', ($, e) => ({ value: environment[e.name] }))
259
260 const session = Fixtures.firstPartySession(on)
261
262 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
263 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
264
265 const managed = session.posts.length
266
267 environment = { ...environment, DISABLE_TELEMETRY: '1' }
268
269 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
270 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
271
272 expect({ managed, afterOptOut: session.posts.length }).toEqual({
273 managed: 1,
274 afterOptOut: 1,
275 })
276 },
277 )
278
279 test(
280 'marks go out as the feature events, by their own names',
281 { plugins: [Fixtures.marking] },
282 async ($, on) => {
283 mock.env(on, Fixtures.SENDING_ENV)
284
285 const session = Fixtures.firstPartySession(on)
286 const answers: (string | undefined)[] = []
287
288 for (const entry of [
289 { feature: 'learn_page', kind: 'ok' },
290 { feature: 'learn_page', kind: 'sad', reason: 'blocked' },
291 { feature: 'suggest_learning', kind: 'bad', reason: 'api_error' },
292 ]) {
293 answers.push((await $.command.run(Fixtures.mark(entry))).text)
294 }
295
296 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
297
298 expect(answers).toEqual(['queued', 'queued', 'queued'])
299
300 expect(
301 Fixtures.rowsOf(session),
302 'one batch, the feature events by their own names, no plugin prefix',
303 ).toMatchObject([
304 {
305 event_name: 'tengu_feature_ok',
306 metadata: { feature_name: 'learn_page' },
307 },
308 {
309 event_name: 'tengu_feature_sad',
310 metadata: { feature_name: 'learn_page', error_code: 'blocked' },
311 },
312 {
313 event_name: 'tengu_feature_bad',
314 metadata: {
315 feature_name: 'suggest_learning',
316 error_code: 'api_error',
317 },
318 },
319 ])
320 },
321 )
322
323 test(
324 'a mark with a bad kind or a wrong reason is refused, nothing queued',
325 { plugins: [Fixtures.marking] },
326 async ($, on) => {
327 mock.env(on, Fixtures.SENDING_ENV)
328
329 const session = Fixtures.firstPartySession(on)
330
331 const refusalFor = async (entry: unknown) =>
332 (await $.command.run(Fixtures.mark(entry))).text
333
334 expect(
335 await refusalFor({ feature: 'learn_page', kind: 'meh' }),
336 ).toEndWith("$.telemetry.mark: kind: 'ok', 'sad' or 'bad'")
337
338 expect(
339 await refusalFor({ feature: 'learn_page', kind: 'bad' }),
340 ).toEndWith(
341 '$.telemetry.mark: reason: a bad mark names why, a snake_case token',
342 )
343
344 expect(
345 await refusalFor({ feature: 'learn_page', kind: 'ok', reason: 'x' }),
346 ).toEndWith('$.telemetry.mark: reason: an ok mark carries none')
347
348 expect(await refusalFor({ feature: 'Learn Page', kind: 'ok' })).toEndWith(
349 '$.telemetry.mark: takes a feature name, a snake_case token',
350 )
351
352 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
353
354 expect(session.posts).toEqual([])
355 },
356 )
357
358 test(
359 'with no first-party credential nothing is sent, and the debug log says so',
360 { plugins: [Fixtures.recording] },
361 async ($, on) => {
362 mock.env(on, Fixtures.SENDING_ENV)
363
364 const session = Fixtures.firstPartySession(on, {
365 authorizations: [null],
366 })
367
368 const { text } = await $.command.run(
369 Fixtures.record(Fixtures.surveyAnswer()),
370 )
371
372 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
373
374 expect({ text, posts: session.posts }).toEqual({
375 text: 'queued',
376 posts: [],
377 })
378
379 expect(session.lines.at(-1)).toBe(
380 'telemetry: 1 row(s) not sent: this session has no first-party ' +
381 'credential to authorize',
382 )
383 },
384 )
385
386 test(
387 'a server error is tried once more; a second one drops the batch',
388 { plugins: [Fixtures.recording] },
389 async ($, on) => {
390 mock.env(on, Fixtures.SENDING_ENV)
391
392 const session = Fixtures.firstPartySession(on, {
393 answers: [Fixtures.REFUSED, Fixtures.REFUSED],
394 })
395
396 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
397 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
398 await session.clock.advance(Hooks.RETRY_DELAY_MS)
399
400 expect(session.posts).toHaveLength(2)
401
402 expect(session.lines.at(-1)).toBe(
403 'telemetry: 1 row(s) not sent: the ingest answered 500',
404 )
405 },
406 )
407
408 test(
409 'a server error then an acceptance sends the batch',
410 { plugins: [Fixtures.recording] },
411 async ($, on) => {
412 mock.env(on, Fixtures.SENDING_ENV)
413
414 const session = Fixtures.firstPartySession(on, {
415 answers: [Fixtures.REFUSED, Fixtures.ACCEPTED],
416 })
417
418 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
419 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
420 await session.clock.advance(Hooks.RETRY_DELAY_MS)
421
422 expect(session.posts.map(post => post.init?.body)).toEqual([
423 session.posts[0]?.init?.body,
424 session.posts[0]?.init?.body,
425 ])
426
427 expect(session.lines.at(-1)).toBe('telemetry: sent 1 row(s)')
428 },
429 )
430
431 test(
432 'a client error is not retried',
433 { plugins: [Fixtures.recording] },
434 async ($, on) => {
435 mock.env(on, Fixtures.SENDING_ENV)
436
437 const session = Fixtures.firstPartySession(on, {
438 answers: [Fixtures.REJECTED],
439 })
440
441 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
442 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
443 await session.clock.advance(Hooks.RETRY_DELAY_MS)
444
445 expect(session.posts).toHaveLength(1)
446
447 expect(session.lines.at(-1)).toBe(
448 'telemetry: 1 row(s) not sent: the ingest answered 400',
449 )
450 },
451 )
452
453 test(
454 'free text and a malformed entry are refused, nothing queued',
455 { plugins: [Fixtures.recording] },
456 async ($, on) => {
457 mock.env(on, Fixtures.SENDING_ENV)
458
459 const session = Fixtures.firstPartySession(on)
460
461 const refusalFor = async (entry: unknown) =>
462 (
463 await $.command.run({
464 ...Fixtures.record(Fixtures.surveyAnswer()),
465 args: JSON.stringify(entry),
466 })
467 ).text
468
469 expect(
470 await refusalFor({ event: 'x', props: { note: 'hello world' } }),
471 ).toEndWith(
472 '$.telemetry.log: props.note: free text is refused; a string is a ' +
473 'Choice, { value, of: [...] }',
474 )
475
476 expect(
477 await refusalFor({
478 event: 'x',
479 props: { page: { value: 'elsewhere', of: ['ready', 'later'] } },
480 }),
481 ).toEndWith(
482 '$.telemetry.log: props.page.value: one of the members of `of`',
483 )
484
485 expect(await refusalFor({ event: 'Survey' })).toEndWith(
486 '$.telemetry.log: takes an event name, a snake_case token',
487 )
488
489 expect(
490 await refusalFor({ event: 'x', props: { 'a path': 1 } }),
491 ).toEndWith('$.telemetry.log: props: every key is a snake_case token')
492
493 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
494
495 expect(session.posts).toEqual([])
496 },
497 )
498
499 test(
500 'a number that is not finite is refused, nothing queued',
501 {
502 plugins: [
503 {
504 name: 'counting',
505 tier: 'builtin',
506 register(on) {
507 on('command.run', { command: 'count' }, $ =>
508 $.telemetry.log({ event: 'x', props: { n: Number.NaN } }).then(
509 () => ({ text: 'queued' }),
510 (error: unknown) => ({ text: String(error) }),
511 ),
512 )
513 },
514 },
515 ],
516 },
517 async ($, on) => {
518 mock.env(on, Fixtures.SENDING_ENV)
519
520 const session = Fixtures.firstPartySession(on)
521
522 const { text } = await $.command.run({
523 command: 'count',
524 args: '',
525 origin: { kind: 'composer' },
526 presentation: Fixtures.FULLSCREEN,
527 })
528
529 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
530
531 expect(text).toEndWith('$.telemetry.log: props.n: a number is finite')
532 expect(session.posts).toEqual([])
533 },
534 )
535
536 test(
537 'the config, the machine and the variables are read once a session',
538 { plugins: [Fixtures.recording] },
539 async ($, on) => {
540 mock.env(on, Fixtures.SENDING_ENV)
541
542 const session = Fixtures.firstPartySession(on)
543
544 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
545 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
546 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
547 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
548
549 expect({
550 posts: session.posts.length,
551 configReads: session.reads.filter(path => path === Fixtures.CONFIG_PATH)
552 .length,
553 runs: session.runs.map(run => run.argv.slice(0, 2)),
554 }).toEqual({ posts: 2, configReads: 1, runs: [['sh', '-c']] })
555 },
556 )
557
558 test(
559 'on Windows the platform is read off the environment and no program runs',
560 { plugins: [Fixtures.recording] },
561 async ($, on) => {
562 mock.env(on, Fixtures.WINDOWS_ENV)
563
564 const session = Fixtures.firstPartySession(on)
565
566 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
567 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
568
569 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
570 env: {
571 platform: 'win32',
572 platform_raw: 'win32',
573 arch: 'x64',
574 shell: 'cmd',
575 terminal: 'windows-terminal',
576 deployment_environment: 'unknown-win32',
577 },
578 })
579
580 expect(session.runs).toEqual([])
581 },
582 )
583
584 test(
585 'on Linux the distribution, kernel, WSL and EC2 come from their files',
586 { plugins: [Fixtures.recording] },
587 async ($, on) => {
588 mock.env(on, { ...Fixtures.SENDING_ENV, WSL_DISTRO_NAME: 'Ubuntu' })
589
590 const session = Fixtures.firstPartySession(on, {
591 probeOutput: Fixtures.LINUX_PROBED,
592 files: Fixtures.LINUX_FILES,
593 existing: ['/.dockerenv'],
594 })
595
596 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
597 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
598
599 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
600 env: {
601 platform: 'linux',
602 platform_raw: 'linux',
603 arch: 'x64',
604 terminal: 'ghostty',
605 package_managers: 'npm',
606 runtimes: 'node',
607 linux_distro_id: 'ubuntu',
608 linux_distro_version: '24.04',
609 linux_kernel: '6.6.87.2-microsoft-standard-WSL2',
610 wsl_version: '2',
611 deployment_environment: 'aws-ec2',
612 },
613 })
614 },
615 )
616
617 test(
618 'in a GitHub Actions run the client and the workflow ids ride along',
619 { plugins: [Fixtures.recording] },
620 async ($, on) => {
621 mock.env(on, Fixtures.GITHUB_ACTION_ENV)
622
623 const session = Fixtures.firstPartySession(on)
624
625 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
626 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
627
628 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
629 client_type: 'github-action',
630 entrypoint: 'claude-code-github-action',
631 env: {
632 is_ci: true,
633 is_github_action: true,
634 is_claude_code_action: true,
635 deployment_environment: 'github-actions',
636 github_event_name: 'pull_request',
637 github_actions_runner_os: 'Linux',
638 github_actions_runner_environment: 'github-hosted',
639 github_action_ref: 'v1',
640 github_actions_metadata: {
641 actor_id: '11',
642 repository_id: '22',
643 repository_owner_id: '33',
644 },
645 },
646 })
647 },
648 )
649
650 test(
651 'a failed authorize is not held against the next batch',
652 { plugins: [Fixtures.recording] },
653 async ($, on) => {
654 mock.env(on, Fixtures.SENDING_ENV)
655
656 const session = Fixtures.firstPartySession(on, {
657 authorizations: ['refused', Fixtures.BEARER],
658 })
659
660 await $.command.run(Fixtures.record({ event: 'x' }))
661 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
662
663 const postsAfterFirst = session.posts.length
664
665 await $.command.run(Fixtures.record({ event: 'x' }))
666 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
667
668 expect({ postsAfterFirst, posts: session.posts.length }).toEqual({
669 postsAfterFirst: 0,
670 posts: 1,
671 })
672
673 expect(session.lines[0]).toContain('the credential store is locked')
674 },
675 )
676
677 test(
678 'a plugin a person installed is refused, and its own hook carries on',
679 { plugins: [Fixtures.visiting] },
680 async ($, on) => {
681 mock.env(on, Fixtures.SENDING_ENV)
682
683 const session = Fixtures.firstPartySession(on)
684
685 const { text } = await $.command.run(
686 Fixtures.typed('visit', Fixtures.surveyAnswer()),
687 )
688
689 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
690
691 expect({ text, posts: session.posts }).toEqual({
692 text: `HooksError: visiting: $.telemetry.log: ${Hooks.REFUSED.deny}`,
693 posts: [],
694 })
695 },
696 )
697
698 test(
699 'a plugin an administrator prepended is refused too',
700 { plugins: [Fixtures.managing] },
701 async ($, on) => {
702 mock.env(on, Fixtures.SENDING_ENV)
703
704 const session = Fixtures.firstPartySession(on)
705
706 const { text } = await $.command.run(
707 Fixtures.typed('manage', { feature: 'learn_page', kind: 'ok' }),
708 )
709
710 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
711
712 expect({ text, posts: session.posts }).toEqual({
713 text: `HooksError: managing: $.telemetry.mark: ${Hooks.REFUSED.deny}`,
714 posts: [],
715 })
716 },
717 )
718
719 test('the gate serves built-ins and core, throws on no origin', () => {
720 expect(tiers.map(tier => Hooks.served({}, Fixtures.nextOf(tier)))).toEqual([
721 'served',
722 'served',
723 Hooks.REFUSED,
724 Hooks.REFUSED,
725 Hooks.REFUSED,
726 ])
727
728 expect(() => Hooks.served({}, Fixtures.nextOf(undefined))).toThrow(
729 '$.telemetry: the call names no origin',
730 )
731 })
732
733 test('a gate that threw refuses; a refusal from beneath passes up', () => {
734 expect(Hooks.caught({}, Fixtures.failingNext(false))).toEqual(Hooks.REFUSED)
735
736 expect(() => Hooks.caught({}, Fixtures.failingNext(true))).toThrow(
737 'beneath refused the entry',
738 )
739 })
740})