@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,519 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, test } from 'bun:test'
2
- import { createTestRenderer } from '@opentui/core/testing'
3
- import { createRoot, flushSync } from '@opentui/react'
4
- import React from 'react'
5
-
6
- import { AnvilModelSelector } from '../anvil-model-selector'
7
- import {
8
- DEFAULT_ANVIL_MODEL_ID,
9
- FALLBACK_ANVIL_MODEL_ID,
10
- ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID,
11
- ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
12
- ANVIL_FABLE_5_MODEL_ID,
13
- ANVIL_GLM_V52_MODEL_ID,
14
- ANVIL_GPT_5_6_LUNA_MODEL_ID,
15
- ANVIL_MINIMAX_M3_MODEL_ID,
16
- ANVIL_MODELS,
17
- getAnvilModelSupersededBy,
18
- isAnvilModelId,
19
- } from '@anvil/common/constants/anvil-models'
20
-
21
- import { initializeThemeStore } from '../../hooks/use-theme'
22
- import {
23
- getSelectedAnvilModel,
24
- useAnvilModelStore,
25
- } from '../../state/anvil-model-store'
26
- import { useAnvilSessionStore } from '../../state/anvil-session-store'
27
-
28
- let cleanupRenderer: (() => void) | undefined
29
-
30
- beforeAll(() => {
31
- initializeThemeStore()
32
- })
33
-
34
- afterEach(() => {
35
- cleanupRenderer?.()
36
- cleanupRenderer = undefined
37
- useAnvilSessionStore.getState().setSession(null)
38
- useAnvilSessionStore.getState().setFailure(null)
39
- useAnvilModelStore.getState().setSelectedModel(FALLBACK_ANVIL_MODEL_ID)
40
- })
41
-
42
- const renderSelector = async (maxHeight = 40) => {
43
- // Tear down any selector this test already rendered. Only the LAST one was
44
- // reachable from afterEach, so a test that renders twice used to leave the
45
- // earlier root mounted — and a mounted selector keeps running its landing
46
- // repair effect, rewriting the shared model store out from under whichever
47
- // test ran next.
48
- cleanupRenderer?.()
49
- cleanupRenderer = undefined
50
- const setup = await createTestRenderer({ width: 100, height: 40 })
51
- const root = createRoot(setup.renderer)
52
- cleanupRenderer = () => {
53
- flushSync(() => root.unmount())
54
- setup.renderer.destroy()
55
- }
56
- flushSync(() => root.render(<AnvilModelSelector maxHeight={maxHeight} />))
57
- await setup.renderOnce()
58
- return setup
59
- }
60
-
61
- const renderSelectorWithGlmRemaining = async (remaining?: number) => {
62
- useAnvilSessionStore.getState().setSession({
63
- status: 'none',
64
- accessTier: 'full',
65
- referral: {
66
- code: 'test-referral',
67
- referrerName: null,
68
- qualifiedCount: 1,
69
- ...(remaining === undefined
70
- ? {}
71
- : { weeklySessionsRemaining: remaining }),
72
- resetAt: new Date(Date.now() + 60_000).toISOString(),
73
- githubLinked: true,
74
- },
75
- })
76
- useAnvilModelStore.getState().setSelectedModel(ANVIL_GLM_V52_MODEL_ID)
77
-
78
- const nextSetup = await renderSelector(30)
79
- await nextSetup.renderOnce()
80
- await Promise.resolve()
81
- await nextSetup.renderOnce()
82
- }
83
-
84
- describe('AnvilModelSelector referral selection', () => {
85
- test('keeps a fractional unlocked GLM session selected while its request is pending', async () => {
86
- await renderSelectorWithGlmRemaining(0.25)
87
- expect(getSelectedAnvilModel()).toBe(ANVIL_GLM_V52_MODEL_ID)
88
- })
89
-
90
- test('still repairs a locked GLM selection to a visible grid model', async () => {
91
- await renderSelectorWithGlmRemaining(0)
92
- expect(isAnvilModelId(getSelectedAnvilModel())).toBe(true)
93
- })
94
-
95
- test('treats an omitted GLM balance as locked', async () => {
96
- await renderSelectorWithGlmRemaining()
97
- expect(isAnvilModelId(getSelectedAnvilModel())).toBe(true)
98
- })
99
- })
100
-
101
- describe('AnvilModelSelector tier layout', () => {
102
- test('keeps the referral actions on one condensed row', async () => {
103
- useAnvilSessionStore.getState().setSession({
104
- status: 'none',
105
- accessTier: 'full',
106
- referral: {
107
- code: 'test-referral',
108
- referrerName: null,
109
- qualifiedCount: 0,
110
- weeklySessionsRemaining: 0,
111
- resetAt: new Date(Date.now() + 60_000).toISOString(),
112
- githubLinked: true,
113
- },
114
- })
115
- useAnvilModelStore
116
- .getState()
117
- .setSelectedModel(ANVIL_MINIMAX_M3_MODEL_ID)
118
-
119
- const frame = (await renderSelector()).captureCharFrame()
120
- const actionRow =
121
- frame.split('\n').find((line) => line.includes('Copy invite link')) ?? ''
122
-
123
- expect(actionRow).toContain('Open referral status')
124
- expect(frame).not.toContain('Or earn')
125
- expect(frame).not.toContain('for small tasks')
126
- })
127
-
128
- test('orders Luna above MiniMax while keeping the saved premium model focused', async () => {
129
- useAnvilSessionStore.getState().setSession({
130
- status: 'none',
131
- accessTier: 'full',
132
- })
133
- // The saved pick has to be something OTHER than the recommended hero, or
134
- // the landing picker opens collapsed and there are no tier headers to order.
135
- // Since 2026-08-12 the hero is itself premium (DeepSeek V4 Pro), so Luna is
136
- // the premium row that exercises "saved model stays focused" here.
137
- useAnvilModelStore
138
- .getState()
139
- .setSelectedModel(ANVIL_GPT_5_6_LUNA_MODEL_ID)
140
-
141
- const setup = await renderSelector()
142
- const frame = setup.captureCharFrame()
143
- const premiumHeaderIndex = frame.indexOf('PREMIUM')
144
- const recommendedModelIndex = frame.indexOf('DeepSeek V4 Pro')
145
- const selectedModelIndex = frame.indexOf('GPT-5.6 Luna')
146
- const minimaxModelIndex = frame.indexOf('MiniMax M3')
147
- const unlimitedHeaderIndex = frame.indexOf('UNLIMITED')
148
-
149
- expect(premiumHeaderIndex).toBeGreaterThanOrEqual(0)
150
- expect(recommendedModelIndex).toBeGreaterThan(premiumHeaderIndex)
151
- expect(selectedModelIndex).toBeGreaterThan(recommendedModelIndex)
152
- expect(minimaxModelIndex).toBeGreaterThan(selectedModelIndex)
153
- expect(unlimitedHeaderIndex).toBeGreaterThan(minimaxModelIndex)
154
- expect(frame).toContain('› GPT-5.6 Luna')
155
- expect(frame).not.toContain('› MiniMax M3')
156
- })
157
-
158
- test('shows the switch-to-Flash nudge only on the row the user is on', async () => {
159
- useAnvilSessionStore.getState().setSession({
160
- status: 'none',
161
- accessTier: 'full',
162
- })
163
- // Assert against the real copy rather than a hardcoded fragment, so
164
- // rewording the notice doesn't fail this test for the wrong reason. It must
165
- // still render on ONE line — the width math reserves exactly its length.
166
- const notice = getAnvilModelSupersededBy(
167
- ANVIL_MINIMAX_M3_MODEL_ID,
168
- ANVIL_MODELS.map((m) => m.id),
169
- )!.notice
170
- const occurrences = (frame: string) => frame.split(notice).length - 1
171
-
172
- // On a superseded model: the nudge appears, once, on that model's card.
173
- useAnvilModelStore
174
- .getState()
175
- .setSelectedModel(ANVIL_MINIMAX_M3_MODEL_ID)
176
- const onSuperseded = (await renderSelector()).captureCharFrame()
177
- expect(occurrences(onSuperseded)).toBe(1)
178
- // It names the dated build, which is what the row it steers to is labelled.
179
- expect(notice).toContain('DeepSeek V4 Flash 07/31')
180
- // The new builds are badged so a returning user notices they changed.
181
- expect(onSuperseded).toContain('NEW')
182
-
183
- // MiMo is superseded too and is on screen here, but only the selected row
184
- // nags — otherwise the list would repeat the same notice on every row it
185
- // applies to. V4 Pro is on screen and is NOT superseded at all since its
186
- // 08/13 GA build, so selecting it shows no nudge anywhere.
187
- useAnvilModelStore
188
- .getState()
189
- .setSelectedModel(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
190
- const onUnsuperseded = (await renderSelector()).captureCharFrame()
191
- expect(onUnsuperseded).toContain('DeepSeek V4 Pro 08/13')
192
- expect(occurrences(onUnsuperseded)).toBe(0)
193
-
194
- // On the replacement itself: no nudge at all. (Picking the recommended
195
- // model also collapses the picker to its hero card.)
196
- useAnvilModelStore
197
- .getState()
198
- .setSelectedModel(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
199
- const onCurrent = (await renderSelector()).captureCharFrame()
200
- expect(occurrences(onCurrent)).toBe(0)
201
- })
202
-
203
- test('places the exhausted-quota recommendation beneath UNLIMITED', async () => {
204
- const resetAt = new Date(Date.now() + 60_000).toISOString()
205
- useAnvilSessionStore.getState().setSession({
206
- status: 'none',
207
- accessTier: 'full',
208
- rateLimitsByModel: {
209
- [ANVIL_DEEPSEEK_V4_PRO_MODEL_ID]: {
210
- model: ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
211
- limit: 6,
212
- period: 'pacific_day',
213
- resetTimeZone: 'America/Los_Angeles',
214
- resetAt,
215
- windowHours: 24,
216
- recentCount: 6,
217
- },
218
- },
219
- })
220
- useAnvilModelStore
221
- .getState()
222
- .setSelectedModel(ANVIL_MINIMAX_M3_MODEL_ID)
223
-
224
- const setup = await renderSelector()
225
- const frame = setup.captureCharFrame()
226
- const premiumHeaderIndex = frame.indexOf('PREMIUM')
227
- const unlimitedHeaderIndex = frame.indexOf('UNLIMITED')
228
- const recommendedLabelIndex = frame.indexOf('RECOMMENDED')
229
- const recommendedModelIndex = frame.indexOf(
230
- 'DeepSeek V4 Flash',
231
- recommendedLabelIndex,
232
- )
233
-
234
- expect(unlimitedHeaderIndex).toBeGreaterThan(premiumHeaderIndex)
235
- expect(recommendedLabelIndex).toBeGreaterThan(unlimitedHeaderIndex)
236
- expect(recommendedModelIndex).toBeGreaterThan(recommendedLabelIndex)
237
- })
238
-
239
- test('collapses to the unlimited hero when the premium default is spent', async () => {
240
- // The default selection has been premium since 2026-08-12, so a returning
241
- // user who has spent their pool opens the picker already sitting on a row
242
- // `pick` silently refuses. Both the selection AND the cursor have to leave
243
- // it, or Enter does nothing with no explanation — and the picker has to
244
- // collapse onto the replacement, or it opens on three greyed, unusable
245
- // premium rows with the recommendation fourth.
246
- const resetAt = new Date(Date.now() + 60_000).toISOString()
247
- useAnvilSessionStore.getState().setSession({
248
- status: 'none',
249
- accessTier: 'full',
250
- rateLimitsByModel: {
251
- [ANVIL_DEEPSEEK_V4_PRO_MODEL_ID]: {
252
- model: ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
253
- limit: 6,
254
- period: 'pacific_day',
255
- resetTimeZone: 'America/Los_Angeles',
256
- resetAt,
257
- windowHours: 24,
258
- recentCount: 6,
259
- },
260
- },
261
- })
262
- useAnvilModelStore.getState().setSelectedModel(DEFAULT_ANVIL_MODEL_ID)
263
-
264
- const setup = await renderSelector()
265
- await Promise.resolve()
266
- await setup.renderOnce()
267
- await setup.renderOnce()
268
-
269
- expect(getSelectedAnvilModel()).toBe(FALLBACK_ANVIL_MODEL_ID)
270
- const frame = setup.captureCharFrame()
271
- // `›` is the cursor: it has to be on the row Enter now commits.
272
- expect(frame).toContain('› DeepSeek V4 Flash')
273
- // …and that row is the whole screen, exactly as for a user who is already
274
- // on the recommendation. The spent rows live behind the toggle.
275
- expect(frame).toContain('See all')
276
- expect(frame).not.toContain('PREMIUM')
277
- })
278
-
279
- test('repairs an invalid selection to the unlimited recommendation when premium is exhausted', async () => {
280
- const resetAt = new Date(Date.now() + 60_000).toISOString()
281
- useAnvilSessionStore.getState().setSession({
282
- status: 'none',
283
- accessTier: 'full',
284
- rateLimitsByModel: {
285
- [ANVIL_DEEPSEEK_V4_PRO_MODEL_ID]: {
286
- model: ANVIL_DEEPSEEK_V4_PRO_MODEL_ID,
287
- limit: 6,
288
- period: 'pacific_day',
289
- resetTimeZone: 'America/Los_Angeles',
290
- resetAt,
291
- windowHours: 24,
292
- recentCount: 6,
293
- },
294
- },
295
- })
296
- useAnvilModelStore.getState().setSelectedModel(ANVIL_GLM_V52_MODEL_ID)
297
-
298
- const setup = await renderSelector()
299
- await Promise.resolve()
300
- await setup.renderOnce()
301
- await setup.renderOnce()
302
-
303
- expect(getSelectedAnvilModel()).toBe(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
304
- expect(setup.captureCharFrame()).toContain('› DeepSeek V4 Flash')
305
- })
306
-
307
- test('shows every limited-tier model when the access tier arrives after mount', async () => {
308
- useAnvilSessionStore.getState().setSession({
309
- status: 'none',
310
- accessTier: 'full',
311
- })
312
- useAnvilModelStore
313
- .getState()
314
- .setSelectedModel(ANVIL_DEEPSEEK_V4_PRO_MODEL_ID)
315
- const setup = await renderSelector()
316
-
317
- flushSync(() => {
318
- useAnvilSessionStore.getState().setSession({
319
- status: 'none',
320
- accessTier: 'limited',
321
- })
322
- })
323
- await Promise.resolve()
324
- await setup.renderOnce()
325
- await setup.renderOnce()
326
-
327
- const frame = setup.captureCharFrame()
328
- expect(frame).toContain('DeepSeek V4 Flash')
329
- expect(frame).toContain('MiMo 2.5')
330
- expect(frame).not.toContain('PREMIUM')
331
- expect(frame).not.toContain('UNLIMITED')
332
- })
333
-
334
- test('badges only natively multimodal rows with Images', async () => {
335
- useAnvilSessionStore.getState().setSession({
336
- status: 'none',
337
- accessTier: 'full',
338
- })
339
- // Expanded (a saved non-recommended pick) so every row is on screen.
340
- useAnvilModelStore
341
- .getState()
342
- .setSelectedModel(ANVIL_MINIMAX_M3_MODEL_ID)
343
-
344
- const rowOf = (frame: string, name: string) =>
345
- frame.split('\n').find((line) => line.includes(name)) ?? ''
346
- const frame = (await renderSelector()).captureCharFrame()
347
-
348
- // Natively multimodal: the badge is a real capability claim.
349
- expect(rowOf(frame, 'MiniMax M3')).toContain('Images')
350
- expect(rowOf(frame, 'GPT-5.6 Luna')).toContain('Images')
351
- expect(rowOf(frame, 'MiMo 2.5')).toContain('Images')
352
- // Text-only. They still accept a pasted image (read server-side as a
353
- // description), but badging them made the label mean nothing — and the
354
- // badge is what widened the hero card.
355
- expect(rowOf(frame, 'DeepSeek V4 Flash')).not.toContain('Images')
356
- expect(rowOf(frame, 'DeepSeek V4 Pro')).not.toContain('Images')
357
- })
358
-
359
- test('says the reasoning effort on rows whose catalog entry carries one', async () => {
360
- useAnvilSessionStore.getState().setSession({
361
- status: 'none',
362
- accessTier: 'full',
363
- })
364
- useAnvilModelStore
365
- .getState()
366
- .setSelectedModel(ANVIL_MINIMAX_M3_MODEL_ID)
367
-
368
- // Anchored on taglines: model names also appear in superseded-notice lines
369
- const rowOf = (frame: string, tagline: string) =>
370
- frame.split('\n').find((line) => line.includes(tagline)) ?? ''
371
- const frame = (await renderSelector()).captureCharFrame()
372
-
373
- expect(rowOf(frame, 'Smart & Fast')).toContain('Reasoning: high')
374
- expect(rowOf(frame, 'Smartest')).toContain('Reasoning: high')
375
- const lunaRow = rowOf(frame, 'GPT-5.6 Luna')
376
- expect(lunaRow).toContain('Strong all-around')
377
- expect(lunaRow).toContain('Reasoning: high')
378
- expect(rowOf(frame, 'MiniMax M3')).not.toContain('Reasoning')
379
- })
380
-
381
- test('sizes the hero card to its content, with no Press-Enter gutter', async () => {
382
- useAnvilSessionStore.getState().setSession({
383
- status: 'none',
384
- accessTier: 'full',
385
- })
386
- useAnvilModelStore
387
- .getState()
388
- .setSelectedModel(ANVIL_DEEPSEEK_V4_FLASH_MODEL_ID)
389
-
390
- const frame = (await renderSelector()).captureCharFrame()
391
- // trimEnd drops the terminal's blank columns to the right of the card, so
392
- // what's left ends at the card's own right border.
393
- const heroRow = (
394
- frame.split('\n').find((line) => line.includes('› DeepSeek V4 Flash')) ??
395
- ''
396
- ).trimEnd()
397
-
398
- expect(frame).not.toContain('Press Enter')
399
- // The reserved cue gutter used to sit between the last badge and the right
400
- // border, padding the card out by ~17 columns of empty space. What remains
401
- // is ordinary slack from the widest row in the set.
402
- const gapToBorder =
403
- heroRow.length - 1 - (heroRow.indexOf('NEW') + 'NEW'.length)
404
- expect(heroRow.endsWith('│')).toBe(true)
405
- expect(gapToBorder).toBeLessThan(10)
406
- })
407
- })
408
-
409
- describe('AnvilModelSelector limited-model offer', () => {
410
- const offerSession = (
411
- offer: Partial<{
412
- remaining: number
413
- total: number
414
- userRemaining: number
415
- userResetAt: string
416
- }> = {},
417
- ) => ({
418
- status: 'none' as const,
419
- accessTier: 'full' as const,
420
- limitedModelOffers: [
421
- {
422
- model: ANVIL_FABLE_5_MODEL_ID,
423
- remaining: 38,
424
- total: 50,
425
- userRemaining: 1,
426
- userResetAt: new Date(Date.now() + 5 * 60 * 60_000).toISOString(),
427
- ...offer,
428
- },
429
- ],
430
- })
431
-
432
- test('renders nothing when the server sends no offer', async () => {
433
- // The regression that matters most: a user who is not in the wave must see
434
- // the picker exactly as it was before the offer existed.
435
- useAnvilSessionStore.getState().setSession({
436
- status: 'none',
437
- accessTier: 'full',
438
- })
439
- const frame = (await renderSelector()).captureCharFrame()
440
- expect(frame).not.toContain('LIMITED TRIAL')
441
- expect(frame).not.toContain('Fable')
442
- })
443
-
444
- test('renders the offered model with its scarcity and data-use label', async () => {
445
- useAnvilSessionStore.getState().setSession(offerSession())
446
- const frame = (await renderSelector()).captureCharFrame()
447
- expect(frame).toContain('LIMITED TRIAL')
448
- expect(frame).toContain('38 of 50 sessions left')
449
- expect(frame).toContain('Claude Fable 5')
450
- // The disclosure that makes collecting the traces legitimate travels on the
451
- // row itself, not in a footnote somewhere else.
452
- expect(frame).toContain('May use data for AI training')
453
- })
454
-
455
- test('stays visible while collapsed, unlike the ordinary tiers', async () => {
456
- // The picker opens collapsed for a user already on the recommended model.
457
- // A wave nobody sees is a wave nobody joins. Read off the constant so the
458
- // collapsed state survives the next flip of the recommended default.
459
- useAnvilModelStore
460
- .getState()
461
- .setSelectedModel(DEFAULT_ANVIL_MODEL_ID)
462
- useAnvilSessionStore.getState().setSession(offerSession())
463
- const frame = (await renderSelector()).captureCharFrame()
464
- expect(frame).toContain('See all')
465
- expect(frame).toContain('Claude Fable 5')
466
- expect(frame).not.toContain('PREMIUM')
467
- })
468
-
469
- test('explains a spent personal allowance instead of hiding the row', async () => {
470
- useAnvilSessionStore
471
- .getState()
472
- .setSession(offerSession({ userRemaining: 0 }))
473
- const frame = (await renderSelector()).captureCharFrame()
474
- expect(frame).toContain('Claude Fable 5')
475
- expect(frame).toContain("you've used yours")
476
- expect(frame).toContain('resets in')
477
- })
478
-
479
- test('drops an offer this build has no catalog entry for', async () => {
480
- // A server rolling out a model older clients don't know must be a no-op,
481
- // not a row with a blank name and no data-use warning.
482
- useAnvilSessionStore.getState().setSession({
483
- status: 'none',
484
- accessTier: 'full',
485
- limitedModelOffers: [
486
- {
487
- model: 'someone/unreleased-model-9',
488
- remaining: 5,
489
- total: 50,
490
- userRemaining: 1,
491
- userResetAt: new Date(Date.now() + 60_000).toISOString(),
492
- },
493
- ],
494
- })
495
- const frame = (await renderSelector()).captureCharFrame()
496
- expect(frame).not.toContain('LIMITED TRIAL')
497
- expect(frame).not.toContain('unreleased-model-9')
498
- })
499
-
500
- test('keeps an offered selection instead of repairing it away', async () => {
501
- // The offer model is not in ANVIL_MODELS, so the picker's
502
- // invalid-selection repair would otherwise bounce the user off the row they
503
- // just picked.
504
- useAnvilSessionStore.getState().setSession(offerSession())
505
- useAnvilModelStore.getState().setSelectedModel(ANVIL_FABLE_5_MODEL_ID)
506
- await renderSelector()
507
- expect(getSelectedAnvilModel()).toBe(ANVIL_FABLE_5_MODEL_ID)
508
- })
509
-
510
- test('repairs the selection once the wave ends', async () => {
511
- useAnvilSessionStore.getState().setSession({
512
- status: 'none',
513
- accessTier: 'full',
514
- })
515
- useAnvilModelStore.getState().setSelectedModel(ANVIL_FABLE_5_MODEL_ID)
516
- await renderSelector()
517
- expect(isAnvilModelId(getSelectedAnvilModel())).toBe(true)
518
- })
519
- })
@@ -1,54 +0,0 @@
1
- // The CLI can offer two kinds of anvil row: the picker grid, and the referral banner's earned
2
- // GLM 5.2 action. Both end up as a POST the server gates, and as a free-mode root agent that has
3
- // to allow the model — so a row this surface can show must survive all of it. Desktop shipped the
4
- // mirror-image of this bug (an offered GLM row its own route answered 400 for), which is what
5
- // these lock down here.
6
-
7
- import { describe, expect, test } from 'bun:test'
8
-
9
- import { getAnvilRootAgentIdForModel } from '@anvil/common/constants/free-agents'
10
- import {
11
- ANVIL_GLM_V52_MODEL_ID,
12
- resolveAnvilModelForAccessTier,
13
- } from '@anvil/common/constants/anvil-models'
14
- import { anvilOfferViolations } from '@anvil/common/testing/anvil-offer-invariants'
15
-
16
- import { anvilCliOfferedModelIds } from '../anvil-model-selector'
17
-
18
- describe('anvil rows the CLI offers', () => {
19
- for (const accessTier of ['full', 'limited'] as const) {
20
- test(`are all usable on the ${accessTier} tier`, () => {
21
- expect(
22
- anvilOfferViolations({
23
- surface: `cli picker + referral banner (${accessTier})`,
24
- accessTier,
25
- offered: anvilCliOfferedModelIds(accessTier),
26
- // the CLI's own resolver, which every session start runs the selection through: a model
27
- // it coerces away is one the user picked and never got
28
- accepts: (model) =>
29
- resolveAnvilModelForAccessTier(model, accessTier) === model,
30
- rootAgentIdFor: getAnvilRootAgentIdForModel,
31
- catalog: 'supported',
32
- }),
33
- ).toEqual([])
34
- })
35
- }
36
-
37
- test('the earned reward is offered on BOTH tiers, and the grid never shows it', () => {
38
- // Limited access included: a bounty grant is redeemable there, so the row has to be
39
- // reachable there. The banner still only renders it against a live balance.
40
- expect(anvilCliOfferedModelIds('full')).toContain(ANVIL_GLM_V52_MODEL_ID)
41
- expect(anvilCliOfferedModelIds('limited')).toContain(
42
- ANVIL_GLM_V52_MODEL_ID,
43
- )
44
- })
45
-
46
- // 'base2-free' is the fallback root, and its allowlist has never included the referral reward.
47
- // A GLM row that fell through to it would 403 with free_mode_invalid_agent_model on the first
48
- // turn instead of failing at selection, so the mapping is what keeps the reward runnable.
49
- test('the reward maps to its own root agent rather than the fallback', () => {
50
- expect(getAnvilRootAgentIdForModel(ANVIL_GLM_V52_MODEL_ID)).toBe(
51
- 'base2-free-glm',
52
- )
53
- })
54
- })