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
771 lines · 21.9 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 refused entry is denied by the hook, naming the caller and the reason',
501 { plugins: [Fixtures.recording, Fixtures.marking] },
502 async ($, on) => {
503 mock.env(on, Fixtures.SENDING_ENV)
504
505 const session = Fixtures.firstPartySession(on)
506
507 const logged = (
508 await $.command.run(Fixtures.typed('record', { event: 'Survey' }))
509 ).text
510
511 const marked = (
512 await $.command.run(
513 Fixtures.typed('mark', { feature: 'learn_page', kind: 'meh' }),
514 )
515 ).text
516
517 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
518
519 expect({ logged, marked, posts: session.posts }).toEqual({
520 logged:
521 'HooksError: recording: $.telemetry.log: takes an event name, a ' +
522 'snake_case token',
523 marked:
524 "HooksError: marking: $.telemetry.mark: kind: 'ok', 'sad' or 'bad'",
525 posts: [],
526 })
527 },
528 )
529
530 test(
531 'a number that is not finite is refused, nothing queued',
532 {
533 plugins: [
534 {
535 name: 'counting',
536 tier: 'builtin',
537 register(on) {
538 on('command.run', { command: 'count' }, $ =>
539 $.telemetry.log({ event: 'x', props: { n: Number.NaN } }).then(
540 () => ({ text: 'queued' }),
541 (error: unknown) => ({ text: String(error) }),
542 ),
543 )
544 },
545 },
546 ],
547 },
548 async ($, on) => {
549 mock.env(on, Fixtures.SENDING_ENV)
550
551 const session = Fixtures.firstPartySession(on)
552
553 const { text } = await $.command.run({
554 command: 'count',
555 args: '',
556 origin: { kind: 'composer' },
557 presentation: Fixtures.FULLSCREEN,
558 })
559
560 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
561
562 expect(text).toEndWith('$.telemetry.log: props.n: a number is finite')
563 expect(session.posts).toEqual([])
564 },
565 )
566
567 test(
568 'the config, the machine and the variables are read once a session',
569 { plugins: [Fixtures.recording] },
570 async ($, on) => {
571 mock.env(on, Fixtures.SENDING_ENV)
572
573 const session = Fixtures.firstPartySession(on)
574
575 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
576 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
577 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
578 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
579
580 expect({
581 posts: session.posts.length,
582 configReads: session.reads.filter(path => path === Fixtures.CONFIG_PATH)
583 .length,
584 runs: session.runs.map(run => run.argv.slice(0, 2)),
585 }).toEqual({ posts: 2, configReads: 1, runs: [['sh', '-c']] })
586 },
587 )
588
589 test(
590 'on Windows the platform is read off the environment and no program runs',
591 { plugins: [Fixtures.recording] },
592 async ($, on) => {
593 mock.env(on, Fixtures.WINDOWS_ENV)
594
595 const session = Fixtures.firstPartySession(on)
596
597 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
598 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
599
600 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
601 env: {
602 platform: 'win32',
603 platform_raw: 'win32',
604 arch: 'x64',
605 shell: 'cmd',
606 terminal: 'windows-terminal',
607 deployment_environment: 'unknown-win32',
608 },
609 })
610
611 expect(session.runs).toEqual([])
612 },
613 )
614
615 test(
616 'on Linux the distribution, kernel, WSL and EC2 come from their files',
617 { plugins: [Fixtures.recording] },
618 async ($, on) => {
619 mock.env(on, { ...Fixtures.SENDING_ENV, WSL_DISTRO_NAME: 'Ubuntu' })
620
621 const session = Fixtures.firstPartySession(on, {
622 probeOutput: Fixtures.LINUX_PROBED,
623 files: Fixtures.LINUX_FILES,
624 existing: ['/.dockerenv'],
625 })
626
627 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
628 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
629
630 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
631 env: {
632 platform: 'linux',
633 platform_raw: 'linux',
634 arch: 'x64',
635 terminal: 'ghostty',
636 package_managers: 'npm',
637 runtimes: 'node',
638 linux_distro_id: 'ubuntu',
639 linux_distro_version: '24.04',
640 linux_kernel: '6.6.87.2-microsoft-standard-WSL2',
641 wsl_version: '2',
642 deployment_environment: 'aws-ec2',
643 },
644 })
645 },
646 )
647
648 test(
649 'in a GitHub Actions run the client and the workflow ids ride along',
650 { plugins: [Fixtures.recording] },
651 async ($, on) => {
652 mock.env(on, Fixtures.GITHUB_ACTION_ENV)
653
654 const session = Fixtures.firstPartySession(on)
655
656 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
657 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
658
659 expect(Fixtures.rowsOf(session)[0]).toMatchObject({
660 client_type: 'github-action',
661 entrypoint: 'claude-code-github-action',
662 env: {
663 is_ci: true,
664 is_github_action: true,
665 is_claude_code_action: true,
666 deployment_environment: 'github-actions',
667 github_event_name: 'pull_request',
668 github_actions_runner_os: 'Linux',
669 github_actions_runner_environment: 'github-hosted',
670 github_action_ref: 'v1',
671 github_actions_metadata: {
672 actor_id: '11',
673 repository_id: '22',
674 repository_owner_id: '33',
675 },
676 },
677 })
678 },
679 )
680
681 test(
682 'a failed authorize is not held against the next batch',
683 { plugins: [Fixtures.recording] },
684 async ($, on) => {
685 mock.env(on, Fixtures.SENDING_ENV)
686
687 const session = Fixtures.firstPartySession(on, {
688 authorizations: ['refused', Fixtures.BEARER],
689 })
690
691 await $.command.run(Fixtures.record({ event: 'x' }))
692 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
693
694 const postsAfterFirst = session.posts.length
695
696 await $.command.run(Fixtures.record({ event: 'x' }))
697 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
698
699 expect({ postsAfterFirst, posts: session.posts.length }).toEqual({
700 postsAfterFirst: 0,
701 posts: 1,
702 })
703
704 expect(session.lines[0]).toContain('the credential store is locked')
705 },
706 )
707
708 test(
709 'a plugin a person installed is refused, and its own hook carries on',
710 { plugins: [Fixtures.visiting] },
711 async ($, on) => {
712 mock.env(on, Fixtures.SENDING_ENV)
713
714 const session = Fixtures.firstPartySession(on)
715
716 const { text } = await $.command.run(
717 Fixtures.typed('visit', Fixtures.surveyAnswer()),
718 )
719
720 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
721
722 expect({ text, posts: session.posts }).toEqual({
723 text: `HooksError: visiting: $.telemetry.log: ${Hooks.REFUSED.deny}`,
724 posts: [],
725 })
726 },
727 )
728
729 test(
730 'a plugin an administrator prepended is refused too',
731 { plugins: [Fixtures.managing] },
732 async ($, on) => {
733 mock.env(on, Fixtures.SENDING_ENV)
734
735 const session = Fixtures.firstPartySession(on)
736
737 const { text } = await $.command.run(
738 Fixtures.typed('manage', { feature: 'learn_page', kind: 'ok' }),
739 )
740
741 await session.clock.advance(Hooks.BATCH_WINDOW_MS)
742
743 expect({ text, posts: session.posts }).toEqual({
744 text: `HooksError: managing: $.telemetry.mark: ${Hooks.REFUSED.deny}`,
745 posts: [],
746 })
747 },
748 )
749
750 test('the gate serves built-ins and core, throws on no origin', () => {
751 expect(tiers.map(tier => Hooks.served({}, Fixtures.nextOf(tier)))).toEqual([
752 'served',
753 'served',
754 Hooks.REFUSED,
755 Hooks.REFUSED,
756 Hooks.REFUSED,
757 ])
758
759 expect(() => Hooks.served({}, Fixtures.nextOf(undefined))).toThrow(
760 '$.telemetry: the call names no origin',
761 )
762 })
763
764 test('a gate that threw refuses; a refusal from beneath passes up', () => {
765 expect(Hooks.caught({}, Fixtures.failingNext(false))).toEqual(Hooks.REFUSED)
766
767 expect(() => Hooks.caught({}, Fixtures.failingNext(true))).toThrow(
768 'beneath refused the entry',
769 )
770 })
771})