@alumbwe/anvil 1.0.41 → 1.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/bin/anvil.js +18 -0
  2. package/package.json +13 -6
  3. package/src/agents/bundled-agents.generated.ts +1 -1
  4. package/src/entry.ts +5 -0
  5. package/src/index.tsx +27 -4
  6. package/src/login/plain-login.ts +60 -0
  7. package/src/login/supabase-auth.ts +346 -0
  8. package/src/pre-init/client-env.ts +17 -0
  9. package/src/utils/anvil-client.ts +4 -0
  10. package/src/utils/auth.ts +16 -4
  11. package/src/utils/client-env-defaults.ts +28 -0
  12. package/src/utils/config-dir.ts +1 -1
  13. package/vendor/@anvil/common/src/constants/deepseek-direct.ts +30 -1
  14. package/vendor/@anvil/common/src/util/project-ignore.ts +4 -3
  15. package/vendor/@anvil/sdk/dist/index.cjs +46 -10
  16. package/vendor/@anvil/sdk/dist/index.mjs +46 -10
  17. package/src/commands/__tests__/anvil-command-aliases.test.ts +0 -56
  18. package/src/commands/__tests__/bash-command.test.ts +0 -445
  19. package/src/commands/__tests__/command-args.test.ts +0 -349
  20. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +0 -108
  21. package/src/commands/__tests__/copy-conversation.test.ts +0 -226
  22. package/src/commands/__tests__/diagnostics-command.test.ts +0 -35
  23. package/src/commands/__tests__/image.test.ts +0 -99
  24. package/src/commands/__tests__/init.test.ts +0 -456
  25. package/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -91
  26. package/src/commands/__tests__/process-diagnostics.test.ts +0 -54
  27. package/src/commands/__tests__/prompt-builders.test.ts +0 -28
  28. package/src/commands/__tests__/router-input.test.ts +0 -298
  29. package/src/components/__tests__/ad-banner.test.tsx +0 -83
  30. package/src/components/__tests__/anvil-landing-heading-row.test.tsx +0 -91
  31. package/src/components/__tests__/anvil-model-selector.test.tsx +0 -519
  32. package/src/components/__tests__/anvil-offer-invariants.test.ts +0 -54
  33. package/src/components/__tests__/anvil-takeover-prompt.test.tsx +0 -212
  34. package/src/components/__tests__/chat-input-bar.test.tsx +0 -130
  35. package/src/components/__tests__/grid-layout.integration.test.tsx +0 -304
  36. package/src/components/__tests__/grid-layout.test.tsx +0 -1051
  37. package/src/components/__tests__/message-block.completion.test.tsx +0 -104
  38. package/src/components/__tests__/message-block.streaming.test.tsx +0 -78
  39. package/src/components/__tests__/message-with-agents.test.tsx +0 -579
  40. package/src/components/__tests__/multiline-input.test.tsx +0 -1132
  41. package/src/components/__tests__/queue-panel.test.tsx +0 -398
  42. package/src/components/__tests__/selectable-list.test.ts +0 -232
  43. package/src/components/__tests__/status-bar.test.tsx +0 -44
  44. package/src/components/__tests__/status-indicator.test.tsx +0 -204
  45. package/src/components/__tests__/terminal-thinking-orb.test.tsx +0 -111
  46. package/src/components/__tests__/user-error-banner.test.tsx +0 -102
  47. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
  48. package/src/components/ask-user/__tests__/validation.test.ts +0 -213
  49. package/src/components/tools/__tests__/apply-patch.test.tsx +0 -84
  50. package/src/components/tools/__tests__/code-search.test.tsx +0 -45
  51. package/src/components/tools/__tests__/gravity-index.test.ts +0 -76
  52. package/src/components/tools/__tests__/render-ui.test.tsx +0 -92
  53. package/src/components/tools/__tests__/run-terminal-command.test.ts +0 -233
  54. package/src/contexts/__tests__/chat-runtime-context.test.tsx +0 -149
  55. package/src/data/slash-commands.test.ts +0 -24
  56. package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +0 -49
  57. package/src/hooks/__tests__/session-fetch-signal.test.ts +0 -63
  58. package/src/hooks/__tests__/use-activity-query.test.ts +0 -1000
  59. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -176
  60. package/src/hooks/__tests__/use-auth-query.test.ts +0 -21
  61. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +0 -66
  62. package/src/hooks/__tests__/use-clipboard.test.tsx +0 -73
  63. package/src/hooks/__tests__/use-connection-status.test.ts +0 -112
  64. package/src/hooks/__tests__/use-directory-browser.test.ts +0 -203
  65. package/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
  66. package/src/hooks/__tests__/use-grid-layout.test.ts +0 -349
  67. package/src/hooks/__tests__/use-input-history.test.ts +0 -699
  68. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +0 -209
  69. package/src/hooks/__tests__/use-message-queue.test.tsx +0 -71
  70. package/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -315
  71. package/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
  72. package/src/hooks/__tests__/use-queue-ui.test.tsx +0 -83
  73. package/src/hooks/__tests__/use-searchable-list.test.ts +0 -359
  74. package/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
  75. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -361
  76. package/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
  77. package/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
  78. package/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
  79. package/src/hooks/__tests__/use-timeout.test.ts +0 -330
  80. package/src/hooks/__tests__/use-usage-query.test.ts +0 -502
  81. package/src/hooks/__tests__/use-user-details-query.test.ts +0 -195
  82. package/src/hooks/helpers/__tests__/send-message.test.ts +0 -1918
  83. package/src/init/__tests__/init-direnv.test.ts +0 -526
  84. package/src/state/__tests__/chat-store-focus.test.ts +0 -22
  85. package/src/state/__tests__/feedback-store.test.ts +0 -257
  86. package/src/utils/__tests__/active-run.test.ts +0 -140
  87. package/src/utils/__tests__/activity-tracker.test.ts +0 -440
  88. package/src/utils/__tests__/agent-display.test.ts +0 -139
  89. package/src/utils/__tests__/analytics-client.test.ts +0 -262
  90. package/src/utils/__tests__/anonymous-id.test.ts +0 -74
  91. package/src/utils/__tests__/anvil-api.test.ts +0 -578
  92. package/src/utils/__tests__/anvil-instance-owner.test.ts +0 -69
  93. package/src/utils/__tests__/anvil-model-navigation.test.ts +0 -102
  94. package/src/utils/__tests__/anvil-premium-reset.test.ts +0 -79
  95. package/src/utils/__tests__/anvil-referral-cache.test.ts +0 -116
  96. package/src/utils/__tests__/anvil-session-api.test.ts +0 -215
  97. package/src/utils/__tests__/anvil-session-display.test.ts +0 -23
  98. package/src/utils/__tests__/anvil-streak-line.test.ts +0 -217
  99. package/src/utils/__tests__/arrays.test.ts +0 -264
  100. package/src/utils/__tests__/bash-context-processor.test.ts +0 -38
  101. package/src/utils/__tests__/block-processor.test.ts +0 -810
  102. package/src/utils/__tests__/chat-history.test.ts +0 -203
  103. package/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
  104. package/src/utils/__tests__/chat-meta.test.ts +0 -130
  105. package/src/utils/__tests__/clipboard-linux.test.ts +0 -359
  106. package/src/utils/__tests__/clipboard.test.ts +0 -910
  107. package/src/utils/__tests__/code-search-summary.test.ts +0 -84
  108. package/src/utils/__tests__/collapse-helpers.test.ts +0 -1109
  109. package/src/utils/__tests__/error-handling.test.ts +0 -658
  110. package/src/utils/__tests__/exit-cleanly.test.ts +0 -110
  111. package/src/utils/__tests__/feedback-helpers.test.ts +0 -444
  112. package/src/utils/__tests__/feedback-submission.test.ts +0 -26
  113. package/src/utils/__tests__/fetch-usage.test.ts +0 -355
  114. package/src/utils/__tests__/fingerprint.test.ts +0 -144
  115. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +0 -7
  116. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +0 -3
  117. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +0 -33
  118. package/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
  119. package/src/utils/__tests__/format-timeout.test.ts +0 -87
  120. package/src/utils/__tests__/image-dimensions.test.ts +0 -249
  121. package/src/utils/__tests__/image-processor.test.ts +0 -209
  122. package/src/utils/__tests__/implementor-helpers.test.ts +0 -1458
  123. package/src/utils/__tests__/keyboard-actions.test.ts +0 -660
  124. package/src/utils/__tests__/layout-helpers.test.ts +0 -36
  125. package/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
  126. package/src/utils/__tests__/log-shipper.test.ts +0 -53
  127. package/src/utils/__tests__/markdown-renderer.test.tsx +0 -421
  128. package/src/utils/__tests__/message-block-helpers.test.ts +0 -1263
  129. package/src/utils/__tests__/message-updater.test.ts +0 -645
  130. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
  131. package/src/utils/__tests__/pending-attachments.test.ts +0 -277
  132. package/src/utils/__tests__/polling-backoff.test.ts +0 -51
  133. package/src/utils/__tests__/queue-panel-actions.test.ts +0 -148
  134. package/src/utils/__tests__/run-state-storage.test.ts +0 -919
  135. package/src/utils/__tests__/safe-json.test.ts +0 -77
  136. package/src/utils/__tests__/sdk-event-handlers.test.ts +0 -685
  137. package/src/utils/__tests__/send-message-helpers.test.ts +0 -1898
  138. package/src/utils/__tests__/send-message-timer.test.ts +0 -64
  139. package/src/utils/__tests__/settings.test.ts +0 -99
  140. package/src/utils/__tests__/strings.test.ts +0 -192
  141. package/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
  142. package/src/utils/__tests__/terminal-command-broker.test.ts +0 -528
  143. package/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
  144. package/src/utils/__tests__/terminal-io.test.ts +0 -137
  145. package/src/utils/__tests__/terminal-protocol-controller.test.ts +0 -119
  146. package/src/utils/__tests__/text-layout.test.ts +0 -82
  147. package/src/utils/__tests__/theme-platform-detection.test.ts +0 -125
  148. package/src/utils/__tests__/think-tag-parser.test.ts +0 -159
  149. package/src/utils/__tests__/trace-writer.test.ts +0 -223
  150. package/src/utils/__tests__/trim-chat-logs.test.ts +0 -78
  151. package/src/utils/__tests__/usage-banner-state.test.ts +0 -298
  152. package/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
  153. package/src/utils/__tests__/windows-terminal-health.test.ts +0 -101
  154. package/src/utils/__tests__/write-file-atomic.test.ts +0 -127
  155. package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +0 -846
  156. package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +0 -101
  157. package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +0 -1408
  158. package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +0 -258
  159. package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +0 -48
  160. package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +0 -282
  161. package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +0 -526
  162. package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +0 -68
  163. package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +0 -132
  164. package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -422
  165. package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +0 -167
  166. package/vendor/@anvil/common/src/__tests__/env-process.test.ts +0 -145
  167. package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +0 -234
  168. package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +0 -379
  169. package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +0 -807
  170. package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +0 -154
  171. package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +0 -246
  172. package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +0 -77
  173. package/vendor/@anvil/common/src/__tests__/model-config.test.ts +0 -89
  174. package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +0 -298
  175. package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +0 -116
  176. package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +0 -167
  177. package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +0 -132
  178. package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +0 -72
  179. package/vendor/@anvil/common/src/__tests__/user-state.test.ts +0 -30
  180. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +0 -63
  181. package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +0 -246
  182. package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +0 -106
  183. package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +0 -351
  184. package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +0 -169
  185. package/vendor/@anvil/common/src/testing/errors.ts +0 -33
  186. package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +0 -334
  187. package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +0 -6
  188. package/vendor/@anvil/common/src/testing/index.ts +0 -84
  189. package/vendor/@anvil/common/src/testing/mock-modules.ts +0 -53
  190. package/vendor/@anvil/common/src/testing/mock-types.ts +0 -123
  191. package/vendor/@anvil/common/src/testing/mocks/analytics.ts +0 -261
  192. package/vendor/@anvil/common/src/testing/mocks/child-process.ts +0 -93
  193. package/vendor/@anvil/common/src/testing/mocks/crypto.ts +0 -218
  194. package/vendor/@anvil/common/src/testing/mocks/database.ts +0 -337
  195. package/vendor/@anvil/common/src/testing/mocks/fetch.ts +0 -219
  196. package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +0 -166
  197. package/vendor/@anvil/common/src/testing/mocks/index.ts +0 -101
  198. package/vendor/@anvil/common/src/testing/mocks/logger.ts +0 -135
  199. package/vendor/@anvil/common/src/testing/mocks/stream.ts +0 -313
  200. package/vendor/@anvil/common/src/testing/mocks/timers.ts +0 -132
  201. package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +0 -127
  202. package/vendor/@anvil/common/src/testing/setup.ts +0 -282
  203. package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
  204. package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -213
  205. package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +0 -47
  206. package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +0 -65
  207. package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +0 -49
  208. package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
  209. package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +0 -26
  210. package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -122
  211. package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +0 -102
  212. package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +0 -143
  213. package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +0 -202
  214. package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +0 -193
  215. package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +0 -140
  216. package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +0 -148
  217. package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
  218. package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +0 -57
  219. package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +0 -774
  220. package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +0 -206
  221. package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +0 -173
  222. package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +0 -60
  223. package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +0 -31
  224. package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +0 -1252
  225. package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
  226. package/vendor/@anvil/common/src/util/__tests__/path.test.ts +0 -22
  227. package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +0 -90
  228. package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +0 -325
  229. package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +0 -28
  230. package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +0 -130
  231. package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +0 -1008
  232. package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +0 -289
  233. package/vendor/@anvil/common/src/util/__tests__/string.test.ts +0 -239
  234. package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +0 -70
  235. package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +0 -108
  236. package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +0 -180
  237. package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +0 -24
  238. package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +0 -88
  239. package/vendor/@anvil/common/src/util/skills.test.ts +0 -44
  240. package/vendor/@anvil/sdk/dist/index.cjs.map +0 -440
  241. package/vendor/@anvil/sdk/dist/index.mjs.map +0 -440
@@ -1,132 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
-
3
- import { describe, expect, test } from 'bun:test'
4
-
5
- import {
6
- buildRedditCustomConversionBody,
7
- redditConversionId,
8
- RedditCapiDeliveryError,
9
- sendRedditCustomConversion,
10
- } from '../reddit-capi'
11
-
12
- const sha256 = (value: string) =>
13
- createHash('sha256').update(value).digest('hex')
14
-
15
- describe('Reddit CAPI', () => {
16
- test('builds a documented v3 custom-event payload', () => {
17
- const body = buildRedditCustomConversionBody({
18
- customEventName: 'FirstPrompt',
19
- conversionId: 'conversion-1',
20
- actionSource: 'WEBSITE',
21
- eventSourceUrl: 'https://anvil.com/chat?utm_source=reddit',
22
- testId: 'test-123',
23
- eventAt: 123,
24
- user: {
25
- email: 'Al.ice+Apple@Example.Com',
26
- externalId: ' user-123 ',
27
- ipAddress: '203.0.113.10',
28
- userAgent: 'Browser',
29
- clickId: 'reddit-click',
30
- uuid: 'pixel-uuid',
31
- },
32
- })
33
-
34
- expect(body).toEqual({
35
- data: {
36
- test_id: 'test-123',
37
- events: [
38
- {
39
- event_at: 123,
40
- action_source: 'WEBSITE',
41
- event_source_url:
42
- 'https://anvil.com/chat?utm_source=reddit&rdt_cid=reddit-click',
43
- type: {
44
- tracking_type: 'CUSTOM',
45
- custom_event_name: 'FirstPrompt',
46
- },
47
- click_id: 'reddit-click',
48
- metadata: { conversion_id: 'conversion-1' },
49
- user: {
50
- email: sha256('alice@example.com'),
51
- external_id: sha256('user-123'),
52
- ip_address: '203.0.113.10',
53
- user_agent: 'Browser',
54
- uuid: 'pixel-uuid',
55
- },
56
- },
57
- ],
58
- },
59
- })
60
- expect(body.data.events[0]?.user).not.toHaveProperty('click_id')
61
- expect(body.data).not.toHaveProperty('partner')
62
- })
63
-
64
- test('uses a stable opaque conversion id per user and event', () => {
65
- expect(redditConversionId('FirstPrompt', 'user-123')).toBe(
66
- sha256('FirstPrompt:user-123'),
67
- )
68
- expect(redditConversionId('Retention1d', 'user-123')).not.toBe(
69
- redditConversionId('FirstPrompt', 'user-123'),
70
- )
71
- })
72
-
73
- test('retries one transient failure', async () => {
74
- let attempts = 0
75
- const result = await sendRedditCustomConversion({
76
- enabled: true,
77
- accessToken: 'secret',
78
- customEventName: 'Retention1d',
79
- conversionId: 'conversion-1',
80
- actionSource: 'OTHER',
81
- user: { externalId: 'user-1' },
82
- sleepImpl: async () => {},
83
- fetchImpl: (async () => {
84
- attempts += 1
85
- return attempts === 1
86
- ? new Response('retry', { status: 503 })
87
- : Response.json({ data: { message: 'ok' } })
88
- }) as unknown as typeof fetch,
89
- })
90
-
91
- expect(result).toBe('sent')
92
- expect(attempts).toBe(2)
93
- })
94
-
95
- test('does not retry permanent failures', async () => {
96
- let attempts = 0
97
- await expect(
98
- sendRedditCustomConversion({
99
- enabled: true,
100
- accessToken: 'secret',
101
- customEventName: 'FirstPrompt',
102
- conversionId: 'conversion-1',
103
- actionSource: 'WEBSITE',
104
- user: { externalId: 'user-1' },
105
- sleepImpl: async () => {},
106
- fetchImpl: (async () => {
107
- attempts += 1
108
- return new Response('invalid', { status: 400 })
109
- }) as unknown as typeof fetch,
110
- }),
111
- ).rejects.toBeInstanceOf(RedditCapiDeliveryError)
112
- expect(attempts).toBe(1)
113
- })
114
-
115
- test('is disabled without production enablement or a token', async () => {
116
- let called = false
117
- const result = await sendRedditCustomConversion({
118
- enabled: false,
119
- accessToken: 'secret',
120
- customEventName: 'FirstPrompt',
121
- conversionId: 'conversion-1',
122
- actionSource: 'WEBSITE',
123
- user: {},
124
- fetchImpl: (async () => {
125
- called = true
126
- return Response.json({})
127
- }) as unknown as typeof fetch,
128
- })
129
- expect(result).toBe('disabled')
130
- expect(called).toBe(false)
131
- })
132
- })
@@ -1,72 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
-
3
- import {
4
- responseAdNodePositions,
5
- responseAdSlotCount,
6
- RESPONSE_AD_FIRST_NODE_COUNT,
7
- RESPONSE_AD_NODE_STEP,
8
- } from '../util/response-ad-positions'
9
-
10
- describe('responseAdNodePositions', () => {
11
- test('places nothing in a response too short to intersperse', () => {
12
- expect(responseAdNodePositions({ nodeCount: 0, adCount: 3 })).toEqual([])
13
- expect(responseAdNodePositions({ nodeCount: 1, adCount: 3 })).toEqual([])
14
- expect(responseAdNodePositions({ nodeCount: 2, adCount: 3 })).toEqual([])
15
- })
16
-
17
- test('keeps the first ad after two nodes, then spaces ads every three nodes', () => {
18
- expect(RESPONSE_AD_FIRST_NODE_COUNT).toBe(2)
19
- expect(RESPONSE_AD_NODE_STEP).toBe(3)
20
- expect(responseAdNodePositions({ nodeCount: 2, adCount: 4 })).toEqual([])
21
- expect(responseAdNodePositions({ nodeCount: 3, adCount: 4 })).toEqual([1])
22
- expect(responseAdNodePositions({ nodeCount: 4, adCount: 4 })).toEqual([1])
23
- expect(responseAdNodePositions({ nodeCount: 6, adCount: 4 })).toEqual([1, 4])
24
- expect(responseAdNodePositions({ nodeCount: 9, adCount: 4 })).toEqual([1, 4, 7])
25
- })
26
-
27
- test('eligible display slots are not capped at eight', () => {
28
- expect(responseAdSlotCount({ nodeCount: 2 })).toBe(0)
29
- expect(responseAdSlotCount({ nodeCount: 3 })).toBe(1)
30
- expect(responseAdSlotCount({ nodeCount: 24 })).toBe(8)
31
- expect(responseAdSlotCount({ nodeCount: 27 })).toBe(9)
32
- expect(responseAdNodePositions({ nodeCount: 27, adCount: 9 })).toEqual([
33
- 1, 4, 7, 10, 13, 16, 19, 22, 25,
34
- ])
35
- })
36
-
37
- test('never places more ads than provided', () => {
38
- expect(responseAdNodePositions({ nodeCount: 20, adCount: 2 })).toEqual([1, 4])
39
- expect(responseAdNodePositions({ nodeCount: 20, adCount: 0 })).toEqual([])
40
- })
41
-
42
- test('positions are stable as the streaming response appends nodes', () => {
43
- let previous: number[] = []
44
- for (let nodeCount = 0; nodeCount <= 12; nodeCount++) {
45
- const next = responseAdNodePositions({ nodeCount, adCount: 3 })
46
- expect(next.slice(0, previous.length)).toEqual(previous)
47
- previous = next
48
- }
49
- })
50
-
51
- test('allows the first-ad offset to differ from recurring spacing', () => {
52
- expect(
53
- responseAdNodePositions({
54
- nodeCount: 8,
55
- adCount: 3,
56
- firstAdAfterNodes: 3,
57
- step: 2,
58
- }),
59
- ).toEqual([2, 4, 6])
60
- })
61
-
62
- test('clamps non-positive placement settings to one', () => {
63
- expect(
64
- responseAdNodePositions({
65
- nodeCount: 4,
66
- adCount: 3,
67
- firstAdAfterNodes: 0,
68
- step: 0,
69
- }),
70
- ).toEqual([0, 1, 2])
71
- })
72
- })
@@ -1,30 +0,0 @@
1
- import { describe, test, expect } from 'bun:test'
2
-
3
- import { getUserState, UserState } from '../old-constants'
4
-
5
- describe('getUserState', () => {
6
- test('returns LOGGED_OUT for unauthenticated users regardless of credits', () => {
7
- expect(getUserState(false, 1000)).toBe(UserState.LOGGED_OUT)
8
- expect(getUserState(false, 0)).toBe(UserState.LOGGED_OUT)
9
- })
10
-
11
- test('returns GOOD_STANDING for users with >= 100 credits', () => {
12
- expect(getUserState(true, 100)).toBe(UserState.GOOD_STANDING)
13
- expect(getUserState(true, 500)).toBe(UserState.GOOD_STANDING)
14
- })
15
-
16
- test('returns ATTENTION_NEEDED for users with 20-99 credits', () => {
17
- expect(getUserState(true, 99)).toBe(UserState.ATTENTION_NEEDED)
18
- expect(getUserState(true, 20)).toBe(UserState.ATTENTION_NEEDED)
19
- })
20
-
21
- test('returns CRITICAL for users with 1-19 credits', () => {
22
- expect(getUserState(true, 19)).toBe(UserState.CRITICAL)
23
- expect(getUserState(true, 1)).toBe(UserState.CRITICAL)
24
- })
25
-
26
- test('returns DEPLETED for users with 0 or fewer credits', () => {
27
- expect(getUserState(true, 0)).toBe(UserState.DEPLETED)
28
- expect(getUserState(true, -100)).toBe(UserState.DEPLETED)
29
- })
30
- })
@@ -1,63 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
-
3
- import {
4
- evaluateOnboardingRequirement,
5
- parseOnboardingEnabled,
6
- } from '../anvil-onboarding-gate'
7
-
8
- describe('evaluateOnboardingRequirement', () => {
9
- it('asks nobody while the switch is off', () => {
10
- // The load-bearing polarity: absence of config disables the feature. The
11
- // opposite convention would show a form nobody decided to launch to every
12
- // user at once.
13
- const result = evaluateOnboardingRequirement({
14
- enabled: false,
15
- complete: false,
16
- })
17
- expect(result.required).toBe(false)
18
- if (result.required) throw new Error('unreachable')
19
- expect(result.reason).toBe('gate_disabled')
20
- })
21
-
22
- it('asks anyone who has not answered, regardless of account age', () => {
23
- // Deliberately no cutover: a long-standing account that never answered is
24
- // exactly who we have the least data about. Skip and the seen-cookie are
25
- // what keep this from being a burden.
26
- expect(
27
- evaluateOnboardingRequirement({ enabled: true, complete: false }).required,
28
- ).toBe(true)
29
- })
30
-
31
- it('stops asking once the answers are in', () => {
32
- const result = evaluateOnboardingRequirement({
33
- enabled: true,
34
- complete: true,
35
- })
36
- expect(result.required).toBe(false)
37
- if (result.required) throw new Error('unreachable')
38
- expect(result.reason).toBe('already_complete')
39
- })
40
- })
41
-
42
- describe('parseOnboardingEnabled', () => {
43
- it('accepts the affirmative spellings', () => {
44
- for (const raw of ['on', 'ON', ' true ', '1']) {
45
- expect(parseOnboardingEnabled(raw)).toBe(true)
46
- }
47
- })
48
-
49
- it('treats anything else as off', () => {
50
- // Unset, empty and misspelt all fail the same safe way — nothing about a
51
- // broken value should be able to switch a user-facing screen on.
52
- for (const raw of [undefined, null, '', ' ', 'off', 'yes', 'enabled']) {
53
- expect(parseOnboardingEnabled(raw)).toBe(false)
54
- }
55
- })
56
- })
57
-
58
- describe('seen cookie', () => {
59
- it('is a stable name with a months-long life', () => {
60
- // The name is read by the `/web` layout and written by the welcome page;
61
- // changing it re-asks everyone, so it is pinned here on purpose.
62
- })
63
- })
@@ -1,246 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
-
3
- import {
4
- classifyOnboardingOtherText,
5
- ANVIL_ONBOARDING_QUESTIONS,
6
- isOnboardingComplete,
7
- ONBOARDING_LEGACY_OPTION_IDS,
8
- ONBOARDING_OTHER_TEXT_MAX,
9
- OTHER_OPTION_ID,
10
- validateOnboardingSubmission,
11
- type OnboardingAnswer,
12
- } from '../anvil-onboarding'
13
-
14
- /** A complete, valid submission. */
15
- function fullAnswers(overrides: OnboardingAnswer[] = []): OnboardingAnswer[] {
16
- const base: OnboardingAnswer[] = [
17
- { questionId: 'referral_source', optionIds: ['youtube'] },
18
- { questionId: 'role', optionIds: ['professional_dev'] },
19
- { questionId: 'proficiency', optionIds: ['advanced'] },
20
- { questionId: 'intended_use', optionIds: ['work', 'side_projects'] },
21
- { questionId: 'subscriptions', optionIds: ['cursor'] },
22
- ]
23
- return base.map((a) => overrides.find((o) => o.questionId === a.questionId) ?? a)
24
- }
25
-
26
- describe('the question set itself', () => {
27
- it('gives every option a unique id within its question', () => {
28
- // A duplicate id would silently merge two distinct answers in the analytics.
29
- for (const q of ANVIL_ONBOARDING_QUESTIONS) {
30
- const ids = q.options.map((o) => o.id)
31
- expect(new Set(ids).size).toBe(ids.length)
32
- }
33
- })
34
-
35
- it('offers Other everywhere except the ordinal scale', () => {
36
- // Free text on an ordered scale is unusable for the segmentation the
37
- // question exists to support.
38
- for (const q of ANVIL_ONBOARDING_QUESTIONS) {
39
- const hasOther = q.options.some((o) => o.id === OTHER_OPTION_ID)
40
- expect(hasOther).toBe(q.id !== 'proficiency')
41
- }
42
- })
43
-
44
- it('maps every legacy option onto an option that still exists', () => {
45
- // A successor that no longer exists silently drops the answers it was
46
- // supposed to rescue — the exact failure the map exists to prevent.
47
- for (const [questionId, map] of Object.entries(ONBOARDING_LEGACY_OPTION_IDS)) {
48
- const question = ANVIL_ONBOARDING_QUESTIONS.find((q) => q.id === questionId)
49
- expect(question).toBeDefined()
50
- const live = new Set(question!.options.map((o) => o.id))
51
- for (const [retired, successor] of Object.entries(map ?? {})) {
52
- expect(live.has(retired)).toBe(false)
53
- expect(live.has(successor)).toBe(true)
54
- }
55
- }
56
- })
57
-
58
- it('only marks options exclusive on multi-select questions', () => {
59
- // On a single-choice question "exclusive" is meaningless, and reading as
60
- // though it does something is worse than not having it.
61
- for (const q of ANVIL_ONBOARDING_QUESTIONS) {
62
- if (q.multi) continue
63
- expect(q.options.some((o) => o.exclusive)).toBe(false)
64
- }
65
- })
66
- })
67
-
68
- describe('classifyOnboardingOtherText — write-ins folded into real options', () => {
69
- it('counts every Instagram spelling as the Instagram / TikTok option', () => {
70
- for (const text of ['insta', 'Instagram', 'instagram ads', 'IG', 'tik tok']) {
71
- expect(classifyOnboardingOtherText('referral_source', text)).toBe('tiktok')
72
- }
73
- })
74
-
75
- it('counts AI assistants as the Google / AI search option', () => {
76
- for (const text of ['ChatGPT', 'chat gpt', 'AI', 'gemini', 'perplexity']) {
77
- expect(classifyOnboardingOtherText('referral_source', text)).toBe('search')
78
- }
79
- })
80
-
81
- it('prefers the more specific rule when a write-in matches both', () => {
82
- expect(classifyOnboardingOtherText('referral_source', 'instagram AI page')).toBe(
83
- 'tiktok',
84
- )
85
- })
86
-
87
- it('leaves genuinely other answers alone', () => {
88
- // The AI rule is the dangerous one: a substring match would swallow
89
- // "email", "said", "chair" and quietly inflate a channel that never
90
- // referred anyone.
91
- for (const text of ['forums', 'my brother', 'email newsletter', 'a fair']) {
92
- expect(classifyOnboardingOtherText('referral_source', text)).toBeNull()
93
- }
94
- })
95
-
96
- it('does nothing on questions with no rules', () => {
97
- expect(classifyOnboardingOtherText('role', 'instagram')).toBeNull()
98
- })
99
- })
100
-
101
- describe('validateOnboardingSubmission', () => {
102
- it('accepts a complete submission', () => {
103
- const result = validateOnboardingSubmission({ answers: fullAnswers() })
104
- expect(result.ok).toBe(true)
105
- })
106
-
107
- it('requires every question', () => {
108
- // The form has no skip, so a partial submission is a stale or hand-rolled
109
- // client rather than a user choice — and half a response would sit in the
110
- // analytics looking like a real one.
111
- const result = validateOnboardingSubmission({
112
- answers: fullAnswers().slice(0, 2),
113
- })
114
- expect(result.ok).toBe(false)
115
- if (result.ok) throw new Error('unreachable')
116
- expect(result.errors.length).toBeGreaterThan(0)
117
- expect(result.errors.every((e) => e.questionId !== null)).toBe(true)
118
- })
119
-
120
- it('rejects an unknown option id', () => {
121
- // Out-of-date client or a forged post. Accepting it would put a value in
122
- // the analytics that no question can explain.
123
- const result = validateOnboardingSubmission({
124
- answers: fullAnswers([{ questionId: 'role', optionIds: ['ceo_of_mars'] }]),
125
- })
126
- expect(result.ok).toBe(false)
127
- })
128
-
129
- it('rejects multiple answers to a single-choice question', () => {
130
- const result = validateOnboardingSubmission({
131
- answers: fullAnswers([
132
- { questionId: 'role', optionIds: ['student', 'founder'] },
133
- ]),
134
- })
135
- expect(result.ok).toBe(false)
136
- })
137
-
138
- it('rejects an exclusive option combined with others', () => {
139
- // "No subscriptions" and "Cursor" cannot both be true; a stored
140
- // contradiction has no honest reading in the tally.
141
- const result = validateOnboardingSubmission({
142
- answers: fullAnswers([
143
- { questionId: 'subscriptions', optionIds: ['none', 'cursor'] },
144
- ]),
145
- })
146
- expect(result.ok).toBe(false)
147
- })
148
-
149
- it('accepts the exclusive option on its own', () => {
150
- const result = validateOnboardingSubmission({
151
- answers: fullAnswers([
152
- { questionId: 'subscriptions', optionIds: ['none'] },
153
- ]),
154
- })
155
- expect(result.ok).toBe(true)
156
- })
157
-
158
- it('accepts multiple answers where the question allows it', () => {
159
- const result = validateOnboardingSubmission({
160
- answers: fullAnswers([
161
- {
162
- questionId: 'intended_use',
163
- optionIds: ['work', 'learning', 'automation'],
164
- },
165
- ]),
166
- })
167
- expect(result.ok).toBe(true)
168
- })
169
- })
170
-
171
- describe('the Other option', () => {
172
- it('requires accompanying text', () => {
173
- const result = validateOnboardingSubmission({
174
- answers: fullAnswers([
175
- { questionId: 'role', optionIds: [OTHER_OPTION_ID] },
176
- ]),
177
- })
178
- expect(result.ok).toBe(false)
179
- })
180
-
181
- it('keeps the text when Other is chosen', () => {
182
- const result = validateOnboardingSubmission({
183
- answers: fullAnswers([
184
- {
185
- questionId: 'role',
186
- optionIds: [OTHER_OPTION_ID],
187
- otherText: ' technical writer ',
188
- },
189
- ]),
190
- })
191
- expect(result.ok).toBe(true)
192
- if (!result.ok) throw new Error('unreachable')
193
- const role = result.answers.find((a) => a.questionId === 'role')
194
- expect(role?.otherText).toBe('technical writer')
195
- })
196
-
197
- it('drops stray text when Other was NOT chosen', () => {
198
- // Otherwise text rides along on an answer that has nowhere to display it,
199
- // and the admin view shows a note against the wrong option.
200
- const result = validateOnboardingSubmission({
201
- answers: fullAnswers([
202
- { questionId: 'role', optionIds: ['student'], otherText: 'ignore me' },
203
- ]),
204
- })
205
- expect(result.ok).toBe(true)
206
- if (!result.ok) throw new Error('unreachable')
207
- expect(result.answers.find((a) => a.questionId === 'role')?.otherText).toBeUndefined()
208
- })
209
-
210
- it('truncates rather than rejecting a long note', () => {
211
- const result = validateOnboardingSubmission({
212
- answers: fullAnswers([
213
- {
214
- questionId: 'role',
215
- optionIds: [OTHER_OPTION_ID],
216
- otherText: 'x'.repeat(ONBOARDING_OTHER_TEXT_MAX + 500),
217
- },
218
- ]),
219
- })
220
- expect(result.ok).toBe(true)
221
- if (!result.ok) throw new Error('unreachable')
222
- expect(result.answers.find((a) => a.questionId === 'role')?.otherText).toHaveLength(
223
- ONBOARDING_OTHER_TEXT_MAX,
224
- )
225
- })
226
- })
227
-
228
- describe('isOnboardingComplete — the blocking gate reads this', () => {
229
- it('is true only when every question has an answer', () => {
230
- expect(isOnboardingComplete(fullAnswers())).toBe(true)
231
- expect(isOnboardingComplete(fullAnswers().slice(0, 3))).toBe(false)
232
- })
233
-
234
- it('treats null, undefined and empty as incomplete', () => {
235
- // A user with no record must be gated, not waved through — this is the
236
- // difference between the gate working and being decorative.
237
- expect(isOnboardingComplete(null)).toBe(false)
238
- expect(isOnboardingComplete(undefined)).toBe(false)
239
- expect(isOnboardingComplete([])).toBe(false)
240
- })
241
-
242
- it('does not count an answer with no options chosen', () => {
243
- const hollow = fullAnswers([{ questionId: 'role', optionIds: [] }])
244
- expect(isOnboardingComplete(hollow)).toBe(false)
245
- })
246
- })
@@ -1,106 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
-
3
- import {
4
- detectCfWorker,
5
- looksLikeProxyClientId,
6
- parseAllowedWorkerZones,
7
- } from '../cf-worker-signals'
8
-
9
- const NO_ZONES = new Set<string>()
10
-
11
- describe('detectCfWorker', () => {
12
- it('detects a Worker subrequest that came through our edge', () => {
13
- expect(
14
- detectCfWorker({
15
- cfWorkerHeader: 'anvil2api.workers.dev',
16
- cfRayHeader: 'abc123-SJC',
17
- allowedZones: NO_ZONES,
18
- }),
19
- ).toEqual({ detected: true, zone: 'anvil2api.workers.dev' })
20
- })
21
-
22
- it('ignores ordinary traffic', () => {
23
- expect(
24
- detectCfWorker({
25
- cfWorkerHeader: null,
26
- cfRayHeader: 'abc123-SJC',
27
- allowedZones: NO_ZONES,
28
- }),
29
- ).toEqual({ detected: false, reason: 'no_header' })
30
- })
31
-
32
- it('refuses to act on cf-worker without edge corroboration', () => {
33
- // This is the whole difference between this detector and the ones that were
34
- // defeated: without `cf-ray` the header is caller-authored text on a request
35
- // that never touched Cloudflare, which is exactly as forgeable as a
36
- // self-reported fingerprint.
37
- expect(
38
- detectCfWorker({
39
- cfWorkerHeader: 'anything.workers.dev',
40
- cfRayHeader: null,
41
- allowedZones: NO_ZONES,
42
- }),
43
- ).toEqual({ detected: false, reason: 'not_edge_verified' })
44
- })
45
-
46
- it('never flags our own Workers', () => {
47
- // app-preview-proxy is a Cloudflare Worker that talks to our API. Banning
48
- // our own infrastructure is the most obvious way this could go wrong.
49
- const allowed = parseAllowedWorkerZones('app-preview-proxy, vly-sh-router')
50
- expect(
51
- detectCfWorker({
52
- cfWorkerHeader: 'app-preview-proxy',
53
- cfRayHeader: 'abc-SJC',
54
- allowedZones: allowed,
55
- }),
56
- ).toEqual({ detected: false, reason: 'allowlisted' })
57
- })
58
-
59
- it('matches the allowlist case-insensitively and ignores padding', () => {
60
- const allowed = parseAllowedWorkerZones(' App-Preview-Proxy ,, ')
61
- expect(
62
- detectCfWorker({
63
- cfWorkerHeader: ' APP-PREVIEW-PROXY ',
64
- cfRayHeader: 'abc-SJC',
65
- allowedZones: allowed,
66
- }).detected,
67
- ).toBe(false)
68
- })
69
-
70
- it('treats a whitespace-only header as absent', () => {
71
- expect(
72
- detectCfWorker({
73
- cfWorkerHeader: ' ',
74
- cfRayHeader: 'abc-SJC',
75
- allowedZones: NO_ZONES,
76
- }),
77
- ).toEqual({ detected: false, reason: 'no_header' })
78
- })
79
- })
80
-
81
- describe('parseAllowedWorkerZones', () => {
82
- it('is empty for unset config rather than throwing', () => {
83
- expect(parseAllowedWorkerZones(undefined).size).toBe(0)
84
- expect(parseAllowedWorkerZones('').size).toBe(0)
85
- })
86
- })
87
-
88
- describe('looksLikeProxyClientId', () => {
89
- it('matches the published proxy generator', () => {
90
- expect(looksLikeProxyClientId('wf-a1b2c3d4')).toBe(true)
91
- })
92
-
93
- it('does not match our own clients or near-misses', () => {
94
- for (const id of [
95
- 'enhanced-abc',
96
- 'wf-TOOLONGVALUE',
97
- 'wf-a1b2c3',
98
- 'wf-A1B2C3D4',
99
- null,
100
- undefined,
101
- '',
102
- ]) {
103
- expect(looksLikeProxyClientId(id as string)).toBe(false)
104
- }
105
- })
106
- })