@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,774 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
-
3
- import {
4
- ABORT_ERROR_MESSAGE,
5
- AbortError,
6
- isAbortError,
7
- promptAborted,
8
- promptSuccess,
9
- unwrapPromptResult,
10
- type PromptResult,
11
- } from '../error'
12
-
13
- describe('AbortError class', () => {
14
- describe('constructor', () => {
15
- it('creates error without reason', () => {
16
- const error = new AbortError()
17
- expect(error.message).toBe(ABORT_ERROR_MESSAGE)
18
- expect(error.name).toBe('AbortError')
19
- })
20
-
21
- it('creates error with reason', () => {
22
- const error = new AbortError('User cancelled')
23
- expect(error.message).toBe(`${ABORT_ERROR_MESSAGE}: User cancelled`)
24
- expect(error.name).toBe('AbortError')
25
- })
26
-
27
- it('creates error with empty string reason', () => {
28
- const error = new AbortError('')
29
- // Empty string is falsy, so no reason appended
30
- expect(error.message).toBe(ABORT_ERROR_MESSAGE)
31
- })
32
-
33
- it('is instanceof Error', () => {
34
- const error = new AbortError()
35
- expect(error instanceof Error).toBe(true)
36
- expect(error instanceof AbortError).toBe(true)
37
- })
38
-
39
- it('has stack trace', () => {
40
- const error = new AbortError('test')
41
- expect(error.stack).toBeDefined()
42
- expect(error.stack).toContain('AbortError')
43
- })
44
- })
45
-
46
- describe('message format', () => {
47
- it('reason is appended after colon and space', () => {
48
- const error = new AbortError('timeout')
49
- expect(error.message).toBe('Request aborted: timeout')
50
- })
51
-
52
- it('preserves special characters in reason', () => {
53
- const error = new AbortError('User pressed Ctrl+C')
54
- expect(error.message).toBe('Request aborted: User pressed Ctrl+C')
55
- })
56
-
57
- it('handles multi-line reason', () => {
58
- const error = new AbortError('First line\nSecond line')
59
- expect(error.message).toBe('Request aborted: First line\nSecond line')
60
- })
61
- })
62
- })
63
-
64
- describe('isAbortError edge cases', () => {
65
- describe('message matching with startsWith', () => {
66
- it('returns true for exact ABORT_ERROR_MESSAGE', () => {
67
- const error = new Error(ABORT_ERROR_MESSAGE)
68
- expect(isAbortError(error)).toBe(true)
69
- })
70
-
71
- it('returns true for message with suffix after ABORT_ERROR_MESSAGE (like AbortError with reason)', () => {
72
- // This is the format AbortError uses: 'Request aborted: reason'
73
- const error = new Error(`${ABORT_ERROR_MESSAGE}: timeout`)
74
- expect(isAbortError(error)).toBe(true)
75
- })
76
-
77
- it('returns false for message with non-colon suffix after ABORT_ERROR_MESSAGE', () => {
78
- // Only 'Request aborted' or 'Request aborted: <reason>' should match
79
- // Other patterns like 'Request aborted by user' should NOT match
80
- const error = new Error(`${ABORT_ERROR_MESSAGE} due to user action`)
81
- expect(isAbortError(error)).toBe(false)
82
- })
83
-
84
- it('returns false for message containing ABORT_ERROR_MESSAGE as substring (not prefix)', () => {
85
- const error = new Error(`Error: ${ABORT_ERROR_MESSAGE} by system`)
86
- expect(isAbortError(error)).toBe(false)
87
- })
88
-
89
- it('returns false for message with prefix before ABORT_ERROR_MESSAGE', () => {
90
- const error = new Error(`Something failed: ${ABORT_ERROR_MESSAGE}`)
91
- expect(isAbortError(error)).toBe(false)
92
- })
93
- })
94
-
95
- describe('case sensitivity', () => {
96
- it('returns false for lowercase version of message', () => {
97
- const error = new Error('request aborted')
98
- expect(isAbortError(error)).toBe(false)
99
- })
100
-
101
- it('returns false for uppercase version of message', () => {
102
- const error = new Error('REQUEST ABORTED')
103
- expect(isAbortError(error)).toBe(false)
104
- })
105
-
106
- it('returns false for mixed case version of message', () => {
107
- const error = new Error('Request Aborted')
108
- expect(isAbortError(error)).toBe(false)
109
- })
110
- })
111
-
112
- describe('AbortError name detection', () => {
113
- it('returns true for Error with name set to AbortError', () => {
114
- const error = new Error('Some other message')
115
- error.name = 'AbortError'
116
- expect(isAbortError(error)).toBe(true)
117
- })
118
-
119
- it('returns false for name containing AbortError as substring', () => {
120
- const error = new Error('test')
121
- error.name = 'MyAbortErrorClass'
122
- expect(isAbortError(error)).toBe(false)
123
- })
124
-
125
- it('returns false for lowercase aborterror name', () => {
126
- const error = new Error('test')
127
- error.name = 'aborterror'
128
- expect(isAbortError(error)).toBe(false)
129
- })
130
- })
131
-
132
- describe('DOMException handling', () => {
133
- it('returns true for DOMException with name AbortError', () => {
134
- const error = new DOMException('The operation was aborted', 'AbortError')
135
- expect(isAbortError(error)).toBe(true)
136
- })
137
-
138
- it('returns true for DOMException with signal abort message', () => {
139
- const error = new DOMException(
140
- 'signal is aborted without reason',
141
- 'AbortError',
142
- )
143
- expect(isAbortError(error)).toBe(true)
144
- })
145
-
146
- it('returns false for DOMException with different name', () => {
147
- const error = new DOMException('test', 'NotFoundError')
148
- expect(isAbortError(error)).toBe(false)
149
- })
150
- })
151
-
152
- describe('Error subclasses', () => {
153
- it('returns true for AbortError instance', () => {
154
- const error = new AbortError('test reason')
155
- expect(isAbortError(error)).toBe(true)
156
- })
157
-
158
- it('returns true for TypeError with AbortError name', () => {
159
- const error = new TypeError('test')
160
- error.name = 'AbortError'
161
- expect(isAbortError(error)).toBe(true)
162
- })
163
-
164
- it('returns false for custom error class without AbortError characteristics', () => {
165
- class CustomError extends Error {
166
- constructor(message: string) {
167
- super(message)
168
- this.name = 'CustomError'
169
- }
170
- }
171
- // Note: Using a message that's similar but NOT exact match to ABORT_ERROR_MESSAGE
172
- const error = new CustomError('Request was aborted by user')
173
- expect(isAbortError(error)).toBe(false)
174
- })
175
-
176
- it('returns true for custom error class with AbortError name', () => {
177
- class MyAbortError extends Error {
178
- constructor() {
179
- super('custom message')
180
- this.name = 'AbortError'
181
- }
182
- }
183
- const error = new MyAbortError()
184
- expect(isAbortError(error)).toBe(true)
185
- })
186
- })
187
-
188
- describe('non-Error types', () => {
189
- it('returns false for string', () => {
190
- expect(isAbortError(ABORT_ERROR_MESSAGE)).toBe(false)
191
- })
192
-
193
- it('returns false for object with message property', () => {
194
- expect(isAbortError({ message: ABORT_ERROR_MESSAGE })).toBe(false)
195
- })
196
-
197
- it('returns false for object with name property', () => {
198
- expect(isAbortError({ name: 'AbortError' })).toBe(false)
199
- })
200
-
201
- it('returns false for null', () => {
202
- expect(isAbortError(null)).toBe(false)
203
- })
204
-
205
- it('returns false for undefined', () => {
206
- expect(isAbortError(undefined)).toBe(false)
207
- })
208
-
209
- it('returns false for number', () => {
210
- expect(isAbortError(42)).toBe(false)
211
- })
212
-
213
- it('returns false for array', () => {
214
- expect(isAbortError([ABORT_ERROR_MESSAGE])).toBe(false)
215
- })
216
-
217
- it('returns false for function', () => {
218
- expect(isAbortError(() => ABORT_ERROR_MESSAGE)).toBe(false)
219
- })
220
- })
221
- })
222
-
223
- describe('unwrapPromptResult with AbortError', () => {
224
- describe('successful results', () => {
225
- it('returns value for successful result', () => {
226
- const result = promptSuccess('test value')
227
- expect(unwrapPromptResult(result)).toBe('test value')
228
- })
229
-
230
- it('returns null for successful null result', () => {
231
- const result = promptSuccess(null)
232
- expect(unwrapPromptResult(result)).toBeNull()
233
- })
234
-
235
- it('returns undefined for successful undefined result', () => {
236
- const result = promptSuccess(undefined)
237
- expect(unwrapPromptResult(result)).toBeUndefined()
238
- })
239
-
240
- it('returns complex object for successful result', () => {
241
- const value = { nested: { array: [1, 2, 3] } }
242
- const result = promptSuccess(value)
243
- expect(unwrapPromptResult(result)).toEqual(value)
244
- })
245
- })
246
-
247
- describe('aborted results throw AbortError', () => {
248
- it('throws AbortError instance', () => {
249
- const result = promptAborted()
250
- try {
251
- unwrapPromptResult(result)
252
- expect(true).toBe(false) // Should not reach here
253
- } catch (error) {
254
- expect(error instanceof AbortError).toBe(true)
255
- }
256
- })
257
-
258
- it('thrown error has name AbortError', () => {
259
- const result = promptAborted()
260
- try {
261
- unwrapPromptResult(result)
262
- expect(true).toBe(false)
263
- } catch (error) {
264
- expect((error as Error).name).toBe('AbortError')
265
- }
266
- })
267
-
268
- it('thrown error includes reason in message', () => {
269
- const result = promptAborted('User cancelled')
270
- try {
271
- unwrapPromptResult(result)
272
- expect(true).toBe(false)
273
- } catch (error) {
274
- expect((error as Error).message).toBe('Request aborted: User cancelled')
275
- }
276
- })
277
-
278
- it('thrown error is detectable with isAbortError', () => {
279
- const result = promptAborted()
280
- try {
281
- unwrapPromptResult(result)
282
- expect(true).toBe(false)
283
- } catch (error) {
284
- expect(isAbortError(error)).toBe(true)
285
- }
286
- })
287
-
288
- it('thrown error with reason is detectable with isAbortError', () => {
289
- const result = promptAborted('timeout')
290
- try {
291
- unwrapPromptResult(result)
292
- expect(true).toBe(false)
293
- } catch (error) {
294
- expect(isAbortError(error)).toBe(true)
295
- }
296
- })
297
- })
298
- })
299
-
300
- describe('PromptResult integration patterns', () => {
301
- describe('early return pattern', () => {
302
- async function mockLlmCall(shouldAbort: boolean): Promise<PromptResult<string>> {
303
- if (shouldAbort) {
304
- return promptAborted('User cancelled')
305
- }
306
- return promptSuccess('LLM response')
307
- }
308
-
309
- async function callerWithEarlyReturn(shouldAbort: boolean): Promise<string | null> {
310
- const result = await mockLlmCall(shouldAbort)
311
- if (result.aborted) {
312
- return null
313
- }
314
- return result.value.toUpperCase()
315
- }
316
-
317
- it('returns transformed value on success', async () => {
318
- const result = await callerWithEarlyReturn(false)
319
- expect(result).toBe('LLM RESPONSE')
320
- })
321
-
322
- it('returns null on abort', async () => {
323
- const result = await callerWithEarlyReturn(true)
324
- expect(result).toBeNull()
325
- })
326
- })
327
-
328
- describe('unwrap with try/catch pattern', () => {
329
- async function mockLlmCall(shouldAbort: boolean): Promise<PromptResult<string>> {
330
- if (shouldAbort) {
331
- return promptAborted('Signal triggered')
332
- }
333
- return promptSuccess('Success response')
334
- }
335
-
336
- async function callerWithUnwrap(shouldAbort: boolean): Promise<string> {
337
- return unwrapPromptResult(await mockLlmCall(shouldAbort))
338
- }
339
-
340
- async function outerCaller(shouldAbort: boolean): Promise<{ result: string; wasAborted: boolean }> {
341
- try {
342
- const result = await callerWithUnwrap(shouldAbort)
343
- return { result, wasAborted: false }
344
- } catch (error) {
345
- if (isAbortError(error)) {
346
- return { result: '', wasAborted: true }
347
- }
348
- throw error // Rethrow non-abort errors
349
- }
350
- }
351
-
352
- it('returns result on success', async () => {
353
- const { result, wasAborted } = await outerCaller(false)
354
- expect(result).toBe('Success response')
355
- expect(wasAborted).toBe(false)
356
- })
357
-
358
- it('catches and identifies abort', async () => {
359
- const { result, wasAborted } = await outerCaller(true)
360
- expect(result).toBe('')
361
- expect(wasAborted).toBe(true)
362
- })
363
- })
364
-
365
- describe('nested function abort propagation', () => {
366
- async function deepestCall(signal: { aborted: boolean }): Promise<PromptResult<number>> {
367
- if (signal.aborted) {
368
- return promptAborted('Aborted at deepest level')
369
- }
370
- return promptSuccess(42)
371
- }
372
-
373
- async function middleCall(signal: { aborted: boolean }): Promise<PromptResult<string>> {
374
- const result = await deepestCall(signal)
375
- if (result.aborted) {
376
- return result // Propagate abort
377
- }
378
- return promptSuccess(`Value: ${result.value}`)
379
- }
380
-
381
- async function topCall(signal: { aborted: boolean }): Promise<PromptResult<string[]>> {
382
- const result = await middleCall(signal)
383
- if (result.aborted) {
384
- return result // Propagate abort
385
- }
386
- return promptSuccess([result.value, 'additional'])
387
- }
388
-
389
- it('propagates success through all levels', async () => {
390
- const signal = { aborted: false }
391
- const result = await topCall(signal)
392
- expect(result.aborted).toBe(false)
393
- if (!result.aborted) {
394
- expect(result.value).toEqual(['Value: 42', 'additional'])
395
- }
396
- })
397
-
398
- it('propagates abort from deepest level', async () => {
399
- const signal = { aborted: true }
400
- const result = await topCall(signal)
401
- expect(result.aborted).toBe(true)
402
- if (result.aborted) {
403
- expect(result.reason).toBe('Aborted at deepest level')
404
- }
405
- })
406
- })
407
-
408
- describe('mixed pattern with fallback', () => {
409
- async function primaryProvider(signal: { aborted: boolean }): Promise<PromptResult<string>> {
410
- if (signal.aborted) {
411
- return promptAborted()
412
- }
413
- // Simulate primary provider failure
414
- throw new Error('Primary provider unavailable')
415
- }
416
-
417
- async function fallbackProvider(signal: { aborted: boolean }): Promise<PromptResult<string>> {
418
- if (signal.aborted) {
419
- return promptAborted()
420
- }
421
- return promptSuccess('Fallback result')
422
- }
423
-
424
- async function callWithFallback(signal: { aborted: boolean }): Promise<PromptResult<string>> {
425
- try {
426
- const result = await primaryProvider(signal)
427
- // If aborted, don't try fallback
428
- if (result.aborted) {
429
- return result
430
- }
431
- return result
432
- } catch (error) {
433
- // Don't fall back on abort errors
434
- if (isAbortError(error)) {
435
- throw error
436
- }
437
- // Try fallback for other errors
438
- return fallbackProvider(signal)
439
- }
440
- }
441
-
442
- it('uses fallback on non-abort error', async () => {
443
- const signal = { aborted: false }
444
- const result = await callWithFallback(signal)
445
- expect(result.aborted).toBe(false)
446
- if (!result.aborted) {
447
- expect(result.value).toBe('Fallback result')
448
- }
449
- })
450
-
451
- it('does not use fallback on abort', async () => {
452
- const signal = { aborted: true }
453
- const result = await callWithFallback(signal)
454
- expect(result.aborted).toBe(true)
455
- })
456
- })
457
-
458
- describe('abort during async iteration', () => {
459
- async function* generateValues(signal: { aborted: boolean }): AsyncGenerator<PromptResult<number>> {
460
- for (let i = 0; i < 5; i++) {
461
- if (signal.aborted) {
462
- yield promptAborted(`Aborted at iteration ${i}`)
463
- return
464
- }
465
- yield promptSuccess(i)
466
- }
467
- }
468
-
469
- async function collectValues(signal: { aborted: boolean }): Promise<{ values: number[]; abortedAt?: string }> {
470
- const values: number[] = []
471
- for await (const result of generateValues(signal)) {
472
- if (result.aborted) {
473
- return { values, abortedAt: result.reason }
474
- }
475
- values.push(result.value)
476
- }
477
- return { values }
478
- }
479
-
480
- it('collects all values when not aborted', async () => {
481
- const signal = { aborted: false }
482
- const { values, abortedAt } = await collectValues(signal)
483
- expect(values).toEqual([0, 1, 2, 3, 4])
484
- expect(abortedAt).toBeUndefined()
485
- })
486
-
487
- it('stops iteration on abort', async () => {
488
- const signal = { aborted: false }
489
- // Simulate abort after first value
490
- const generator = generateValues(signal)
491
- const results: number[] = []
492
-
493
- for await (const result of generator) {
494
- if (result.aborted) break
495
- results.push(result.value)
496
- if (results.length === 2) {
497
- signal.aborted = true
498
- }
499
- }
500
-
501
- expect(results).toEqual([0, 1])
502
- })
503
- })
504
-
505
- describe('rethrow pattern in catch blocks', () => {
506
- async function innerOperation(): Promise<PromptResult<string>> {
507
- return promptAborted('Inner abort')
508
- }
509
-
510
- async function middleOperation(): Promise<string> {
511
- const result = await innerOperation()
512
- return unwrapPromptResult(result)
513
- }
514
-
515
- async function outerOperationBad(): Promise<string> {
516
- try {
517
- return await middleOperation()
518
- } catch (error) {
519
- // BAD: swallows abort error
520
- return 'default value'
521
- }
522
- }
523
-
524
- async function outerOperationGood(): Promise<string> {
525
- try {
526
- return await middleOperation()
527
- } catch (error) {
528
- // GOOD: rethrows abort error
529
- if (isAbortError(error)) {
530
- throw error
531
- }
532
- return 'default value'
533
- }
534
- }
535
-
536
- it('bad pattern swallows abort', async () => {
537
- const result = await outerOperationBad()
538
- // This shows the anti-pattern - abort was swallowed
539
- expect(result).toBe('default value')
540
- })
541
-
542
- it('good pattern propagates abort', async () => {
543
- await expect(outerOperationGood()).rejects.toThrow(ABORT_ERROR_MESSAGE)
544
- })
545
-
546
- it('good pattern rethrows AbortError that can be detected', async () => {
547
- try {
548
- await outerOperationGood()
549
- expect(true).toBe(false) // Should not reach
550
- } catch (error) {
551
- expect(isAbortError(error)).toBe(true)
552
- }
553
- })
554
- })
555
- })
556
-
557
- describe('ABORT_ERROR_MESSAGE constant', () => {
558
- it('has expected value', () => {
559
- expect(ABORT_ERROR_MESSAGE).toBe('Request aborted')
560
- })
561
-
562
- it('is used by AbortError class', () => {
563
- const error = new AbortError()
564
- expect(error.message).toBe(ABORT_ERROR_MESSAGE)
565
- })
566
-
567
- it('is detected by isAbortError', () => {
568
- const error = new Error(ABORT_ERROR_MESSAGE)
569
- expect(isAbortError(error)).toBe(true)
570
- })
571
- })
572
-
573
- describe('AbortController integration', () => {
574
- describe('signal.aborted check pattern', () => {
575
- async function mockLlmCallWithSignal(signal: AbortSignal): Promise<PromptResult<string>> {
576
- if (signal.aborted) {
577
- return promptAborted('Signal was already aborted')
578
- }
579
- // Simulate async work
580
- await new Promise((resolve) => setTimeout(resolve, 0))
581
- if (signal.aborted) {
582
- return promptAborted('Signal aborted during operation')
583
- }
584
- return promptSuccess('Operation completed')
585
- }
586
-
587
- it('returns success when signal is not aborted', async () => {
588
- const controller = new AbortController()
589
- const result = await mockLlmCallWithSignal(controller.signal)
590
- expect(result.aborted).toBe(false)
591
- if (!result.aborted) {
592
- expect(result.value).toBe('Operation completed')
593
- }
594
- })
595
-
596
- it('returns aborted when signal is pre-aborted', async () => {
597
- const controller = new AbortController()
598
- controller.abort()
599
- const result = await mockLlmCallWithSignal(controller.signal)
600
- expect(result.aborted).toBe(true)
601
- if (result.aborted) {
602
- expect(result.reason).toBe('Signal was already aborted')
603
- }
604
- })
605
- })
606
-
607
- describe('sequential operations with abort', () => {
608
- const callLog: string[] = []
609
-
610
- async function step1(signal: AbortSignal): Promise<PromptResult<string>> {
611
- callLog.push('step1')
612
- if (signal.aborted) return promptAborted('step1 aborted')
613
- return promptSuccess('step1 result')
614
- }
615
-
616
- async function step2(signal: AbortSignal): Promise<PromptResult<string>> {
617
- callLog.push('step2')
618
- if (signal.aborted) return promptAborted('step2 aborted')
619
- return promptSuccess('step2 result')
620
- }
621
-
622
- async function step3(signal: AbortSignal): Promise<PromptResult<string>> {
623
- callLog.push('step3')
624
- if (signal.aborted) return promptAborted('step3 aborted')
625
- return promptSuccess('step3 result')
626
- }
627
-
628
- async function runSequentialSteps(signal: AbortSignal): Promise<PromptResult<string[]>> {
629
- const results: string[] = []
630
-
631
- const r1 = await step1(signal)
632
- if (r1.aborted) return r1
633
- results.push(r1.value)
634
-
635
- const r2 = await step2(signal)
636
- if (r2.aborted) return r2
637
- results.push(r2.value)
638
-
639
- const r3 = await step3(signal)
640
- if (r3.aborted) return r3
641
- results.push(r3.value)
642
-
643
- return promptSuccess(results)
644
- }
645
-
646
- it('completes all steps when not aborted', async () => {
647
- callLog.length = 0
648
- const controller = new AbortController()
649
- const result = await runSequentialSteps(controller.signal)
650
- expect(result.aborted).toBe(false)
651
- if (!result.aborted) {
652
- expect(result.value).toEqual(['step1 result', 'step2 result', 'step3 result'])
653
- }
654
- expect(callLog).toEqual(['step1', 'step2', 'step3'])
655
- })
656
-
657
- it('stops at first step when pre-aborted', async () => {
658
- callLog.length = 0
659
- const controller = new AbortController()
660
- controller.abort()
661
- const result = await runSequentialSteps(controller.signal)
662
- expect(result.aborted).toBe(true)
663
- // Only step1 should be called, and it should return aborted immediately
664
- expect(callLog).toEqual(['step1'])
665
- })
666
- })
667
-
668
- describe('fallback should NOT occur on abort (user intent)', () => {
669
- let fallbackCalled = false
670
-
671
- async function primaryModel(signal: AbortSignal): Promise<PromptResult<string>> {
672
- if (signal.aborted) {
673
- return promptAborted('User cancelled')
674
- }
675
- return promptSuccess('Primary model response')
676
- }
677
-
678
- async function fallbackModel(signal: AbortSignal): Promise<PromptResult<string>> {
679
- fallbackCalled = true
680
- if (signal.aborted) {
681
- return promptAborted('User cancelled')
682
- }
683
- return promptSuccess('Fallback model response')
684
- }
685
-
686
- async function callWithFallbackOnError(
687
- signal: AbortSignal,
688
- primaryShouldThrowError: boolean,
689
- primaryShouldAbort: boolean,
690
- ): Promise<PromptResult<string>> {
691
- try {
692
- if (primaryShouldThrowError) {
693
- throw new Error('Primary provider unavailable')
694
- }
695
- const primaryResult = primaryShouldAbort
696
- ? promptAborted('User cancelled primary')
697
- : await primaryModel(signal)
698
-
699
- // Key pattern: if aborted, do NOT fall back - abort represents user intent
700
- if (primaryResult.aborted) {
701
- return primaryResult
702
- }
703
- return primaryResult
704
- } catch (error) {
705
- // Don't fall back on abort errors
706
- if (isAbortError(error)) {
707
- throw error
708
- }
709
- // Try fallback for other errors
710
- return fallbackModel(signal)
711
- }
712
- }
713
-
714
- it('returns primary result when not aborted', async () => {
715
- fallbackCalled = false
716
- const controller = new AbortController()
717
- const result = await callWithFallbackOnError(controller.signal, false, false)
718
- expect(result.aborted).toBe(false)
719
- if (!result.aborted) {
720
- expect(result.value).toBe('Primary model response')
721
- }
722
- expect(fallbackCalled).toBe(false)
723
- })
724
-
725
- it('propagates abort without fallback (respects user intent)', async () => {
726
- fallbackCalled = false
727
- const controller = new AbortController()
728
- const result = await callWithFallbackOnError(controller.signal, false, true)
729
- expect(result.aborted).toBe(true)
730
- // Verify fallback was never called - abort means user wants to stop, not retry
731
- expect(fallbackCalled).toBe(false)
732
- })
733
-
734
- it('uses fallback on non-abort error', async () => {
735
- fallbackCalled = false
736
- const controller = new AbortController()
737
- const result = await callWithFallbackOnError(controller.signal, true, false)
738
- expect(result.aborted).toBe(false)
739
- if (!result.aborted) {
740
- expect(result.value).toBe('Fallback model response')
741
- }
742
- // Verify fallback WAS called for non-abort error
743
- expect(fallbackCalled).toBe(true)
744
- })
745
- })
746
-
747
- describe('DOMException from AbortController', () => {
748
- it('native abort reason is detected by isAbortError', () => {
749
- const controller = new AbortController()
750
- controller.abort()
751
- // When you call controller.abort(), signal.reason becomes a DOMException
752
- // with name 'AbortError'
753
- const reason = controller.signal.reason
754
- expect(reason).toBeInstanceOf(DOMException)
755
- expect(isAbortError(reason)).toBe(true)
756
- })
757
-
758
- it('custom abort reason string is not detected as AbortError', () => {
759
- const controller = new AbortController()
760
- controller.abort('custom reason string')
761
- // When you provide a reason, signal.reason is that value, not a DOMException
762
- const reason = controller.signal.reason
763
- expect(isAbortError(reason)).toBe(false) // string is not an Error
764
- })
765
-
766
- it('custom abort reason Error with AbortError name is detected', () => {
767
- const controller = new AbortController()
768
- const customAbortError = new AbortError('custom abort')
769
- controller.abort(customAbortError)
770
- const reason = controller.signal.reason
771
- expect(isAbortError(reason)).toBe(true)
772
- })
773
- })
774
- })