@alumbwe/anvil 1.0.0

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 (529) hide show
  1. package/README.md +84 -0
  2. package/bin/anvil +2 -0
  3. package/bin/tree-sitter.wasm +0 -0
  4. package/package.json +67 -0
  5. package/src/__tests__/README.md +256 -0
  6. package/src/__tests__/bash-mode.test.ts +442 -0
  7. package/src/__tests__/cli-args.test.ts +181 -0
  8. package/src/__tests__/cli-harness.test.ts +52 -0
  9. package/src/__tests__/e2e/first-time-login.test.ts +173 -0
  10. package/src/__tests__/e2e/logout-relogin-flow.test.ts +110 -0
  11. package/src/__tests__/e2e/returning-user-auth.test.ts +113 -0
  12. package/src/__tests__/e2e-cli.test.ts +183 -0
  13. package/src/__tests__/helpers/launcher-disconnect-fixture.cjs +176 -0
  14. package/src/__tests__/helpers/mock-api-client.ts +64 -0
  15. package/src/__tests__/helpers/renderer-cleanup-fixture.tsx +89 -0
  16. package/src/__tests__/helpers/terminal-watchdog-fixture.ts +75 -0
  17. package/src/__tests__/home-directory-detection.test.ts +213 -0
  18. package/src/__tests__/integration/api-integration.test.ts +288 -0
  19. package/src/__tests__/integration/credentials-storage.test.ts +486 -0
  20. package/src/__tests__/integration/local-agents.test.ts +1187 -0
  21. package/src/__tests__/integration/login-flow-instrumentation.test.ts +134 -0
  22. package/src/__tests__/integration/login-polling-working.test.ts +285 -0
  23. package/src/__tests__/integration/queue-editing.test.tsx +261 -0
  24. package/src/__tests__/integration/usage-refresh-on-completion.test.ts +168 -0
  25. package/src/__tests__/integration-tmux.test.ts +188 -0
  26. package/src/__tests__/launcher-avx2-fallback.test.ts +630 -0
  27. package/src/__tests__/launcher-disconnect.test.ts +104 -0
  28. package/src/__tests__/mocks/hover-toggle-controller.ts +71 -0
  29. package/src/__tests__/open-tui-native-bundle.test.ts +378 -0
  30. package/src/__tests__/path-completion.test.ts +243 -0
  31. package/src/__tests__/project-files-chat-id.test.ts +34 -0
  32. package/src/__tests__/release/proxy-http-get.test.ts +667 -0
  33. package/src/__tests__/release/wrapper-safety.test.ts +412 -0
  34. package/src/__tests__/renderer-cleanup.test.ts +174 -0
  35. package/src/__tests__/rerender-perf.integration.test.ts +283 -0
  36. package/src/__tests__/terminal-reset-sequences.test.ts +36 -0
  37. package/src/__tests__/terminal-watchdog.test.ts +388 -0
  38. package/src/__tests__/test-utils.ts +271 -0
  39. package/src/__tests__/tmux-poc.ts +150 -0
  40. package/src/__tests__/unit/agent-mode-toggle.test.ts +137 -0
  41. package/src/__tests__/unit/copy-button.test.ts +157 -0
  42. package/src/__tests__/unit/create-run-config.test.ts +66 -0
  43. package/src/__tests__/unit/publish-confirmation.test.ts +70 -0
  44. package/src/__tests__/unit/segmented-control.test.ts +152 -0
  45. package/src/__tests__/utils/env.test.ts +169 -0
  46. package/src/__tests__/utils/project-picker.test.ts +39 -0
  47. package/src/agents/bundled-agents.generated.d.ts +14 -0
  48. package/src/agents/bundled-agents.generated.ts +3692 -0
  49. package/src/app.tsx +399 -0
  50. package/src/chat.tsx +1698 -0
  51. package/src/cli-args.ts +134 -0
  52. package/src/commands/__tests__/bash-command.test.ts +445 -0
  53. package/src/commands/__tests__/command-args.test.ts +349 -0
  54. package/src/commands/__tests__/copy-conversation-clipboard.test.ts +108 -0
  55. package/src/commands/__tests__/copy-conversation.test.ts +226 -0
  56. package/src/commands/__tests__/diagnostics-command.test.ts +35 -0
  57. package/src/commands/__tests__/freebuff-command-aliases.test.ts +56 -0
  58. package/src/commands/__tests__/image.test.ts +99 -0
  59. package/src/commands/__tests__/init.test.ts +456 -0
  60. package/src/commands/__tests__/new-command-rotates-chat.test.ts +91 -0
  61. package/src/commands/__tests__/process-diagnostics.test.ts +54 -0
  62. package/src/commands/__tests__/prompt-builders.test.ts +28 -0
  63. package/src/commands/__tests__/router-input.test.ts +298 -0
  64. package/src/commands/ads.ts +44 -0
  65. package/src/commands/command-registry.ts +726 -0
  66. package/src/commands/copy-conversation.ts +392 -0
  67. package/src/commands/help.ts +13 -0
  68. package/src/commands/image.ts +20 -0
  69. package/src/commands/init.ts +121 -0
  70. package/src/commands/process-diagnostics.ts +120 -0
  71. package/src/commands/prompt-builders.ts +100 -0
  72. package/src/commands/publish.ts +203 -0
  73. package/src/commands/router-utils.ts +80 -0
  74. package/src/commands/router.ts +487 -0
  75. package/src/commands/usage.ts +26 -0
  76. package/src/components/__tests__/ad-banner.test.tsx +83 -0
  77. package/src/components/__tests__/chat-input-bar.test.tsx +130 -0
  78. package/src/components/__tests__/freebuff-landing-heading-row.test.tsx +91 -0
  79. package/src/components/__tests__/freebuff-model-selector.test.tsx +519 -0
  80. package/src/components/__tests__/freebuff-offer-invariants.test.ts +54 -0
  81. package/src/components/__tests__/freebuff-takeover-prompt.test.tsx +212 -0
  82. package/src/components/__tests__/grid-layout.integration.test.tsx +304 -0
  83. package/src/components/__tests__/grid-layout.test.tsx +1051 -0
  84. package/src/components/__tests__/message-block.completion.test.tsx +104 -0
  85. package/src/components/__tests__/message-block.streaming.test.tsx +78 -0
  86. package/src/components/__tests__/message-with-agents.test.tsx +579 -0
  87. package/src/components/__tests__/multiline-input.test.tsx +1132 -0
  88. package/src/components/__tests__/queue-panel.test.tsx +398 -0
  89. package/src/components/__tests__/selectable-list.test.ts +232 -0
  90. package/src/components/__tests__/status-bar.test.tsx +44 -0
  91. package/src/components/__tests__/status-indicator.test.tsx +204 -0
  92. package/src/components/__tests__/terminal-thinking-orb.test.tsx +111 -0
  93. package/src/components/__tests__/user-error-banner.test.tsx +102 -0
  94. package/src/components/ad-banner.tsx +323 -0
  95. package/src/components/agent-checklist.tsx +350 -0
  96. package/src/components/agent-mode-toggle.tsx +241 -0
  97. package/src/components/anvil-mark.tsx +52 -0
  98. package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +432 -0
  99. package/src/components/ask-user/__tests__/validation.test.ts +213 -0
  100. package/src/components/ask-user/components/accordion-question.tsx +153 -0
  101. package/src/components/ask-user/components/custom-answer-input.tsx +66 -0
  102. package/src/components/ask-user/components/options-list.tsx +133 -0
  103. package/src/components/ask-user/components/question-header.tsx +80 -0
  104. package/src/components/ask-user/components/question-option.tsx +89 -0
  105. package/src/components/ask-user/constants.ts +38 -0
  106. package/src/components/ask-user/index.tsx +631 -0
  107. package/src/components/ask-user/utils/validation.ts +85 -0
  108. package/src/components/attachment-card.tsx +65 -0
  109. package/src/components/blocks/agent-block-grid.tsx +58 -0
  110. package/src/components/blocks/agent-branch-item.tsx +311 -0
  111. package/src/components/blocks/agent-branch-wrapper.tsx +493 -0
  112. package/src/components/blocks/agent-list-branch.tsx +77 -0
  113. package/src/components/blocks/ask-user-branch.tsx +81 -0
  114. package/src/components/blocks/block-helpers.ts +11 -0
  115. package/src/components/blocks/blocks-renderer.tsx +289 -0
  116. package/src/components/blocks/content-with-markdown.tsx +33 -0
  117. package/src/components/blocks/image-block.tsx +128 -0
  118. package/src/components/blocks/implementor-row.tsx +429 -0
  119. package/src/components/blocks/single-block.tsx +201 -0
  120. package/src/components/blocks/thinking-block.tsx +67 -0
  121. package/src/components/blocks/tool-block-group.tsx +57 -0
  122. package/src/components/blocks/tool-branch.tsx +206 -0
  123. package/src/components/blocks/user-content-copy.tsx +157 -0
  124. package/src/components/bottom-banner.tsx +136 -0
  125. package/src/components/build-mode-buttons.tsx +110 -0
  126. package/src/components/button.tsx +66 -0
  127. package/src/components/chat-header.tsx +77 -0
  128. package/src/components/chat-history-screen.tsx +413 -0
  129. package/src/components/chat-input-bar.tsx +523 -0
  130. package/src/components/clickable-title-box.tsx +66 -0
  131. package/src/components/clickable.tsx +119 -0
  132. package/src/components/collapse-button.tsx +34 -0
  133. package/src/components/copy-button.tsx +203 -0
  134. package/src/components/elapsed-timer.tsx +55 -0
  135. package/src/components/error-boundary.tsx +55 -0
  136. package/src/components/feedback-container.tsx +181 -0
  137. package/src/components/feedback-icon-button.tsx +89 -0
  138. package/src/components/feedback-input-mode.tsx +344 -0
  139. package/src/components/file-attachment-card.tsx +98 -0
  140. package/src/components/freebuff-active-session-summary.tsx +63 -0
  141. package/src/components/freebuff-landing-screen.tsx +865 -0
  142. package/src/components/freebuff-model-selector.tsx +1087 -0
  143. package/src/components/freebuff-referral-banner.tsx +604 -0
  144. package/src/components/freebuff-superseded-screen.tsx +62 -0
  145. package/src/components/grid-layout.tsx +80 -0
  146. package/src/components/help-banner.tsx +127 -0
  147. package/src/components/highlighted-text.tsx +52 -0
  148. package/src/components/image-card.tsx +167 -0
  149. package/src/components/image-thumbnail.tsx +114 -0
  150. package/src/components/input-cursor.tsx +77 -0
  151. package/src/components/input-mode-banner.tsx +57 -0
  152. package/src/components/load-previous-button.tsx +48 -0
  153. package/src/components/login-modal.tsx +475 -0
  154. package/src/components/message-block.tsx +346 -0
  155. package/src/components/message-footer.tsx +258 -0
  156. package/src/components/message-with-agents.tsx +506 -0
  157. package/src/components/mode-divider.tsx +43 -0
  158. package/src/components/multiline-input.tsx +1241 -0
  159. package/src/components/out-of-credits-banner.tsx +160 -0
  160. package/src/components/pending-attachments-banner.tsx +116 -0
  161. package/src/components/pending-bash-message.tsx +59 -0
  162. package/src/components/progress-bar.tsx +81 -0
  163. package/src/components/project-picker-screen.tsx +485 -0
  164. package/src/components/publish-confirmation.tsx +493 -0
  165. package/src/components/publish-container.tsx +687 -0
  166. package/src/components/queue-panel.tsx +310 -0
  167. package/src/components/raised-pill.tsx +95 -0
  168. package/src/components/renderers/plan-box.tsx +59 -0
  169. package/src/components/review-screen.tsx +112 -0
  170. package/src/components/scroll-to-bottom-button.tsx +34 -0
  171. package/src/components/segmented-control.tsx +187 -0
  172. package/src/components/selectable-list.tsx +248 -0
  173. package/src/components/selected-chips.tsx +80 -0
  174. package/src/components/separator.tsx +40 -0
  175. package/src/components/session-ended-banner.tsx +226 -0
  176. package/src/components/shimmer-text.tsx +248 -0
  177. package/src/components/status-bar.tsx +313 -0
  178. package/src/components/subscription-limit-banner.tsx +206 -0
  179. package/src/components/suggested-prompts.tsx +184 -0
  180. package/src/components/suggestion-menu.tsx +215 -0
  181. package/src/components/terminal-command-display.tsx +151 -0
  182. package/src/components/terminal-link.tsx +97 -0
  183. package/src/components/terminal-thinking-orb.tsx +95 -0
  184. package/src/components/text-attachment-card.tsx +77 -0
  185. package/src/components/thinking.tsx +109 -0
  186. package/src/components/tools/__tests__/apply-patch.test.tsx +84 -0
  187. package/src/components/tools/__tests__/code-search.test.tsx +45 -0
  188. package/src/components/tools/__tests__/gravity-index.test.ts +76 -0
  189. package/src/components/tools/__tests__/render-ui.test.tsx +92 -0
  190. package/src/components/tools/__tests__/run-terminal-command.test.ts +233 -0
  191. package/src/components/tools/apply-patch.tsx +95 -0
  192. package/src/components/tools/code-search.tsx +45 -0
  193. package/src/components/tools/composio.tsx +152 -0
  194. package/src/components/tools/diff-viewer.tsx +76 -0
  195. package/src/components/tools/glob.tsx +61 -0
  196. package/src/components/tools/gravity-index.tsx +77 -0
  197. package/src/components/tools/list-directory.tsx +61 -0
  198. package/src/components/tools/read-docs.tsx +37 -0
  199. package/src/components/tools/read-files.tsx +91 -0
  200. package/src/components/tools/read-subtree.tsx +44 -0
  201. package/src/components/tools/read-url.tsx +33 -0
  202. package/src/components/tools/registry.ts +126 -0
  203. package/src/components/tools/render-ui.tsx +122 -0
  204. package/src/components/tools/run-terminal-command.tsx +79 -0
  205. package/src/components/tools/skill.tsx +29 -0
  206. package/src/components/tools/str-replace.tsx +77 -0
  207. package/src/components/tools/suggest-followups.tsx +369 -0
  208. package/src/components/tools/task-completed.tsx +16 -0
  209. package/src/components/tools/tool-call-item.tsx +273 -0
  210. package/src/components/tools/types.ts +55 -0
  211. package/src/components/tools/web-search.tsx +33 -0
  212. package/src/components/tools/write-file.tsx +16 -0
  213. package/src/components/tools/write-todos.tsx +95 -0
  214. package/src/components/top-banner.tsx +178 -0
  215. package/src/components/usage-banner.tsx +244 -0
  216. package/src/components/user-error-banner.tsx +56 -0
  217. package/src/components/validation-error-popover.tsx +192 -0
  218. package/src/contexts/__tests__/chat-runtime-context.test.tsx +149 -0
  219. package/src/contexts/chat-runtime-context.tsx +240 -0
  220. package/src/data/slash-commands.test.ts +24 -0
  221. package/src/data/slash-commands.ts +249 -0
  222. package/src/entry.ts +12 -0
  223. package/src/hooks/__tests__/holds-live-freebuff-slot.test.ts +49 -0
  224. package/src/hooks/__tests__/session-fetch-signal.test.ts +63 -0
  225. package/src/hooks/__tests__/use-activity-query.test.ts +1000 -0
  226. package/src/hooks/__tests__/use-ask-user-bridge.test.ts +176 -0
  227. package/src/hooks/__tests__/use-auth-query.test.ts +21 -0
  228. package/src/hooks/__tests__/use-chat-input-remount.test.tsx +66 -0
  229. package/src/hooks/__tests__/use-clipboard.test.tsx +73 -0
  230. package/src/hooks/__tests__/use-connection-status.test.ts +112 -0
  231. package/src/hooks/__tests__/use-directory-browser.test.ts +203 -0
  232. package/src/hooks/__tests__/use-gravity-ad.test.ts +73 -0
  233. package/src/hooks/__tests__/use-grid-layout.test.ts +349 -0
  234. package/src/hooks/__tests__/use-input-history.test.ts +699 -0
  235. package/src/hooks/__tests__/use-message-queue-editing.test.tsx +209 -0
  236. package/src/hooks/__tests__/use-message-queue.test.tsx +71 -0
  237. package/src/hooks/__tests__/use-path-tab-completion.test.ts +315 -0
  238. package/src/hooks/__tests__/use-queue-controls.test.ts +68 -0
  239. package/src/hooks/__tests__/use-queue-ui.test.tsx +83 -0
  240. package/src/hooks/__tests__/use-searchable-list.test.ts +359 -0
  241. package/src/hooks/__tests__/use-send-message-timer.test.ts +146 -0
  242. package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +361 -0
  243. package/src/hooks/__tests__/use-suggestion-engine.test.ts +485 -0
  244. package/src/hooks/__tests__/use-terminal-focus.test.ts +66 -0
  245. package/src/hooks/__tests__/use-terminal-layout.test.ts +675 -0
  246. package/src/hooks/__tests__/use-timeout.test.ts +330 -0
  247. package/src/hooks/__tests__/use-usage-query.test.ts +502 -0
  248. package/src/hooks/__tests__/use-user-details-query.test.ts +195 -0
  249. package/src/hooks/helpers/__tests__/send-message.test.ts +1918 -0
  250. package/src/hooks/helpers/send-message.ts +654 -0
  251. package/src/hooks/stream-state.ts +83 -0
  252. package/src/hooks/use-activity-query.ts +668 -0
  253. package/src/hooks/use-agent-validation.ts +74 -0
  254. package/src/hooks/use-ask-user-bridge.ts +92 -0
  255. package/src/hooks/use-auth-query.ts +253 -0
  256. package/src/hooks/use-auth-state.ts +152 -0
  257. package/src/hooks/use-chat-input.ts +105 -0
  258. package/src/hooks/use-chat-keyboard.ts +321 -0
  259. package/src/hooks/use-chat-messages.ts +254 -0
  260. package/src/hooks/use-chat-state.ts +165 -0
  261. package/src/hooks/use-chat-streaming.ts +180 -0
  262. package/src/hooks/use-chat-ui.ts +131 -0
  263. package/src/hooks/use-clipboard.ts +151 -0
  264. package/src/hooks/use-connection-status.ts +142 -0
  265. package/src/hooks/use-directory-browser.ts +88 -0
  266. package/src/hooks/use-elapsed-time.ts +116 -0
  267. package/src/hooks/use-event.ts +34 -0
  268. package/src/hooks/use-exit-handler.ts +66 -0
  269. package/src/hooks/use-fetch-login-url.ts +65 -0
  270. package/src/hooks/use-fingerprint.ts +61 -0
  271. package/src/hooks/use-freebuff-ctrl-c-exit.ts +23 -0
  272. package/src/hooks/use-freebuff-session-progress.ts +34 -0
  273. package/src/hooks/use-freebuff-session.ts +806 -0
  274. package/src/hooks/use-freebuff-streak-query.ts +61 -0
  275. package/src/hooks/use-gravity-ad.ts +656 -0
  276. package/src/hooks/use-grid-layout.ts +73 -0
  277. package/src/hooks/use-input-history.ts +173 -0
  278. package/src/hooks/use-login-keyboard-handlers.ts +75 -0
  279. package/src/hooks/use-login-polling.ts +114 -0
  280. package/src/hooks/use-logo.tsx +161 -0
  281. package/src/hooks/use-message-queue.ts +387 -0
  282. package/src/hooks/use-now.ts +20 -0
  283. package/src/hooks/use-path-tab-completion.ts +88 -0
  284. package/src/hooks/use-publish-mutation.ts +60 -0
  285. package/src/hooks/use-queue-controls.ts +68 -0
  286. package/src/hooks/use-queue-ui.ts +74 -0
  287. package/src/hooks/use-scroll-management.ts +184 -0
  288. package/src/hooks/use-searchable-list.ts +98 -0
  289. package/src/hooks/use-send-message.ts +773 -0
  290. package/src/hooks/use-sheen-animation.tsx +88 -0
  291. package/src/hooks/use-subscription-query.ts +71 -0
  292. package/src/hooks/use-suggestion-engine.ts +783 -0
  293. package/src/hooks/use-terminal-breakpoints.ts +77 -0
  294. package/src/hooks/use-terminal-dimensions.ts +46 -0
  295. package/src/hooks/use-terminal-focus.ts +29 -0
  296. package/src/hooks/use-terminal-layout.ts +189 -0
  297. package/src/hooks/use-theme.tsx +140 -0
  298. package/src/hooks/use-timeout.ts +85 -0
  299. package/src/hooks/use-update-preference.ts +66 -0
  300. package/src/hooks/use-usage-monitor.ts +55 -0
  301. package/src/hooks/use-usage-query.ts +124 -0
  302. package/src/hooks/use-user-details-query.ts +93 -0
  303. package/src/hooks/use-why-did-you-update.ts +177 -0
  304. package/src/index.tsx +441 -0
  305. package/src/init/__tests__/init-direnv.test.ts +526 -0
  306. package/src/init/init-app.ts +34 -0
  307. package/src/init/init-direnv.ts +133 -0
  308. package/src/login/constants.ts +60 -0
  309. package/src/login/login-flow.ts +204 -0
  310. package/src/login/plain-login.ts +108 -0
  311. package/src/login/utils.ts +183 -0
  312. package/src/native/ripgrep.ts +80 -0
  313. package/src/polyfills/bun-strip-ansi.ts +12 -0
  314. package/src/pre-init/tree-sitter-wasm.ts +96 -0
  315. package/src/project-files.ts +102 -0
  316. package/src/smoke/terminal-command-broker.ts +439 -0
  317. package/src/state/__tests__/chat-store-focus.test.ts +22 -0
  318. package/src/state/__tests__/feedback-store.test.ts +257 -0
  319. package/src/state/chat-history-store.ts +30 -0
  320. package/src/state/chat-store.ts +544 -0
  321. package/src/state/feedback-store.ts +160 -0
  322. package/src/state/freebuff-model-store.ts +42 -0
  323. package/src/state/freebuff-session-store.ts +53 -0
  324. package/src/state/login-store.ts +150 -0
  325. package/src/state/message-block-store.ts +149 -0
  326. package/src/state/publish-store.ts +146 -0
  327. package/src/state/queue-panel-store.ts +26 -0
  328. package/src/state/review-store.ts +24 -0
  329. package/src/testing/env.ts +46 -0
  330. package/src/types/chat-state.ts +15 -0
  331. package/src/types/chat.ts +241 -0
  332. package/src/types/contracts/send-message.ts +12 -0
  333. package/src/types/env.ts +98 -0
  334. package/src/types/freebuff-session.ts +17 -0
  335. package/src/types/function-params.ts +28 -0
  336. package/src/types/react19-compat.d.ts +19 -0
  337. package/src/types/store.ts +111 -0
  338. package/src/types/theme-system.ts +144 -0
  339. package/src/types/utils.ts +3 -0
  340. package/src/utils/__tests__/active-run.test.ts +140 -0
  341. package/src/utils/__tests__/activity-tracker.test.ts +440 -0
  342. package/src/utils/__tests__/agent-display.test.ts +139 -0
  343. package/src/utils/__tests__/analytics-client.test.ts +262 -0
  344. package/src/utils/__tests__/anonymous-id.test.ts +74 -0
  345. package/src/utils/__tests__/anvil-api.test.ts +578 -0
  346. package/src/utils/__tests__/arrays.test.ts +264 -0
  347. package/src/utils/__tests__/bash-context-processor.test.ts +38 -0
  348. package/src/utils/__tests__/block-processor.test.ts +810 -0
  349. package/src/utils/__tests__/chat-history.test.ts +203 -0
  350. package/src/utils/__tests__/chat-input-key-intercept.test.ts +40 -0
  351. package/src/utils/__tests__/chat-meta.test.ts +130 -0
  352. package/src/utils/__tests__/clipboard-linux.test.ts +359 -0
  353. package/src/utils/__tests__/clipboard.test.ts +910 -0
  354. package/src/utils/__tests__/code-search-summary.test.ts +84 -0
  355. package/src/utils/__tests__/collapse-helpers.test.ts +1109 -0
  356. package/src/utils/__tests__/error-handling.test.ts +658 -0
  357. package/src/utils/__tests__/exit-cleanly.test.ts +110 -0
  358. package/src/utils/__tests__/feedback-helpers.test.ts +444 -0
  359. package/src/utils/__tests__/feedback-submission.test.ts +26 -0
  360. package/src/utils/__tests__/fetch-usage.test.ts +355 -0
  361. package/src/utils/__tests__/fingerprint.test.ts +144 -0
  362. package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +7 -0
  363. package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +3 -0
  364. package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +33 -0
  365. package/src/utils/__tests__/format-elapsed-time.test.ts +103 -0
  366. package/src/utils/__tests__/format-timeout.test.ts +87 -0
  367. package/src/utils/__tests__/freebuff-instance-owner.test.ts +69 -0
  368. package/src/utils/__tests__/freebuff-model-navigation.test.ts +102 -0
  369. package/src/utils/__tests__/freebuff-premium-reset.test.ts +79 -0
  370. package/src/utils/__tests__/freebuff-referral-cache.test.ts +116 -0
  371. package/src/utils/__tests__/freebuff-session-api.test.ts +215 -0
  372. package/src/utils/__tests__/freebuff-session-display.test.ts +23 -0
  373. package/src/utils/__tests__/freebuff-streak-line.test.ts +217 -0
  374. package/src/utils/__tests__/image-dimensions.test.ts +249 -0
  375. package/src/utils/__tests__/image-processor.test.ts +209 -0
  376. package/src/utils/__tests__/implementor-helpers.test.ts +1458 -0
  377. package/src/utils/__tests__/keyboard-actions.test.ts +660 -0
  378. package/src/utils/__tests__/layout-helpers.test.ts +36 -0
  379. package/src/utils/__tests__/lazy-response-ads.test.ts +167 -0
  380. package/src/utils/__tests__/log-shipper.test.ts +53 -0
  381. package/src/utils/__tests__/markdown-renderer.test.tsx +421 -0
  382. package/src/utils/__tests__/message-block-helpers.test.ts +1263 -0
  383. package/src/utils/__tests__/message-updater.test.ts +645 -0
  384. package/src/utils/__tests__/osc-timeout-scenarios.test.ts +172 -0
  385. package/src/utils/__tests__/pending-attachments.test.ts +277 -0
  386. package/src/utils/__tests__/polling-backoff.test.ts +51 -0
  387. package/src/utils/__tests__/queue-panel-actions.test.ts +148 -0
  388. package/src/utils/__tests__/run-state-storage.test.ts +919 -0
  389. package/src/utils/__tests__/safe-json.test.ts +77 -0
  390. package/src/utils/__tests__/sdk-event-handlers.test.ts +685 -0
  391. package/src/utils/__tests__/send-message-helpers.test.ts +1898 -0
  392. package/src/utils/__tests__/send-message-timer.test.ts +64 -0
  393. package/src/utils/__tests__/settings.test.ts +99 -0
  394. package/src/utils/__tests__/strings.test.ts +192 -0
  395. package/src/utils/__tests__/terminal-color-detection.test.ts +425 -0
  396. package/src/utils/__tests__/terminal-command-broker.test.ts +528 -0
  397. package/src/utils/__tests__/terminal-enter-detection.test.ts +77 -0
  398. package/src/utils/__tests__/terminal-io.test.ts +137 -0
  399. package/src/utils/__tests__/terminal-protocol-controller.test.ts +119 -0
  400. package/src/utils/__tests__/text-layout.test.ts +82 -0
  401. package/src/utils/__tests__/theme-platform-detection.test.ts +125 -0
  402. package/src/utils/__tests__/think-tag-parser.test.ts +159 -0
  403. package/src/utils/__tests__/trace-writer.test.ts +223 -0
  404. package/src/utils/__tests__/trim-chat-logs.test.ts +78 -0
  405. package/src/utils/__tests__/usage-banner-state.test.ts +298 -0
  406. package/src/utils/__tests__/validation-error-formatting.test.ts +126 -0
  407. package/src/utils/__tests__/windows-terminal-health.test.ts +101 -0
  408. package/src/utils/__tests__/write-file-atomic.test.ts +127 -0
  409. package/src/utils/active-run.ts +131 -0
  410. package/src/utils/activity-tracker.ts +66 -0
  411. package/src/utils/agent-display.ts +87 -0
  412. package/src/utils/agent-helpers.ts +27 -0
  413. package/src/utils/agent-id-utils.ts +14 -0
  414. package/src/utils/ai-message-id.ts +12 -0
  415. package/src/utils/analytics.ts +361 -0
  416. package/src/utils/anonymous-id.ts +87 -0
  417. package/src/utils/anvil-api.ts +612 -0
  418. package/src/utils/anvil-client.ts +217 -0
  419. package/src/utils/arrays.ts +117 -0
  420. package/src/utils/auth.ts +258 -0
  421. package/src/utils/bash-context-processor.ts +47 -0
  422. package/src/utils/bash-messages.ts +108 -0
  423. package/src/utils/block-margins.ts +35 -0
  424. package/src/utils/block-operations.ts +523 -0
  425. package/src/utils/block-processor.ts +213 -0
  426. package/src/utils/chat-history.ts +253 -0
  427. package/src/utils/chat-input-key-intercept.ts +52 -0
  428. package/src/utils/chat-meta.ts +90 -0
  429. package/src/utils/chat-scroll-accel.ts +151 -0
  430. package/src/utils/clipboard-image.ts +588 -0
  431. package/src/utils/clipboard.ts +397 -0
  432. package/src/utils/code-search-summary.ts +70 -0
  433. package/src/utils/collapse-helpers.ts +256 -0
  434. package/src/utils/config-dir.ts +24 -0
  435. package/src/utils/constants.ts +190 -0
  436. package/src/utils/create-event-handler-state.ts +73 -0
  437. package/src/utils/create-run-config.ts +121 -0
  438. package/src/utils/detect-shell.ts +116 -0
  439. package/src/utils/directory-browser.ts +72 -0
  440. package/src/utils/engagement.ts +55 -0
  441. package/src/utils/env.ts +92 -0
  442. package/src/utils/error-handling.ts +255 -0
  443. package/src/utils/error-messages.ts +62 -0
  444. package/src/utils/exit-cleanly.ts +86 -0
  445. package/src/utils/feedback-helpers.ts +103 -0
  446. package/src/utils/feedback-submission.ts +22 -0
  447. package/src/utils/fetch-usage.ts +76 -0
  448. package/src/utils/fingerprint.ts +245 -0
  449. package/src/utils/format-elapsed-time.ts +33 -0
  450. package/src/utils/format-session-units.ts +6 -0
  451. package/src/utils/format-timeout.ts +28 -0
  452. package/src/utils/format-validation-errors-for-message.ts +84 -0
  453. package/src/utils/freebuff-agent-selection.ts +33 -0
  454. package/src/utils/freebuff-instance-owner.ts +66 -0
  455. package/src/utils/freebuff-model-navigation.ts +50 -0
  456. package/src/utils/freebuff-premium-reset.ts +55 -0
  457. package/src/utils/freebuff-referral-cache.ts +51 -0
  458. package/src/utils/freebuff-session-api.ts +243 -0
  459. package/src/utils/freebuff-session-display.ts +21 -0
  460. package/src/utils/freebuff-streak-line.ts +93 -0
  461. package/src/utils/git.ts +17 -0
  462. package/src/utils/helpers.ts +47 -0
  463. package/src/utils/image-display.ts +67 -0
  464. package/src/utils/image-handler.ts +325 -0
  465. package/src/utils/image-processor.ts +133 -0
  466. package/src/utils/image-thumbnail.ts +78 -0
  467. package/src/utils/implementor-helpers.ts +787 -0
  468. package/src/utils/input-modes.ts +174 -0
  469. package/src/utils/keyboard-actions.ts +392 -0
  470. package/src/utils/keypad-keys.ts +47 -0
  471. package/src/utils/layout-helpers.ts +33 -0
  472. package/src/utils/lazy-response-ads.ts +1 -0
  473. package/src/utils/local-agent-registry.ts +474 -0
  474. package/src/utils/log-shipper.ts +126 -0
  475. package/src/utils/logger.ts +327 -0
  476. package/src/utils/markdown-renderer.tsx +1090 -0
  477. package/src/utils/math.ts +3 -0
  478. package/src/utils/message-block-helpers.ts +684 -0
  479. package/src/utils/message-history.ts +143 -0
  480. package/src/utils/message-tree-utils.ts +67 -0
  481. package/src/utils/message-updater.ts +279 -0
  482. package/src/utils/open-file.ts +146 -0
  483. package/src/utils/open-url.ts +63 -0
  484. package/src/utils/path-completion.ts +111 -0
  485. package/src/utils/path-helpers.ts +38 -0
  486. package/src/utils/pending-attachments.ts +360 -0
  487. package/src/utils/polling-backoff.ts +59 -0
  488. package/src/utils/project-picker.ts +12 -0
  489. package/src/utils/queue-panel-actions.ts +70 -0
  490. package/src/utils/recent-projects.ts +156 -0
  491. package/src/utils/renderer-cleanup.ts +233 -0
  492. package/src/utils/response-ad-positions.ts +1 -0
  493. package/src/utils/run-state-storage.ts +603 -0
  494. package/src/utils/safe-json.ts +153 -0
  495. package/src/utils/sdk-event-handlers.ts +571 -0
  496. package/src/utils/send-message-helpers.ts +210 -0
  497. package/src/utils/send-message-timer.ts +110 -0
  498. package/src/utils/settings.ts +226 -0
  499. package/src/utils/skill-registry.ts +94 -0
  500. package/src/utils/spawn-agent-matcher.ts +55 -0
  501. package/src/utils/status-indicator-state.ts +107 -0
  502. package/src/utils/stream-chunk-processor.ts +57 -0
  503. package/src/utils/strings.ts +271 -0
  504. package/src/utils/subscription.ts +31 -0
  505. package/src/utils/syntax-highlighter.tsx +19 -0
  506. package/src/utils/terminal-color-detection.ts +474 -0
  507. package/src/utils/terminal-command-broker.ts +467 -0
  508. package/src/utils/terminal-enter-detection.ts +63 -0
  509. package/src/utils/terminal-images.ts +229 -0
  510. package/src/utils/terminal-io.ts +85 -0
  511. package/src/utils/terminal-protocol-controller.ts +173 -0
  512. package/src/utils/terminal-reset-sequences.ts +33 -0
  513. package/src/utils/terminal-title.ts +84 -0
  514. package/src/utils/terminal-watchdog.ts +366 -0
  515. package/src/utils/text-layout.ts +149 -0
  516. package/src/utils/theme-config.ts +141 -0
  517. package/src/utils/theme-system.ts +1206 -0
  518. package/src/utils/think-tag-parser.ts +95 -0
  519. package/src/utils/time-format.ts +21 -0
  520. package/src/utils/trace-writer.ts +162 -0
  521. package/src/utils/ui-constants.ts +69 -0
  522. package/src/utils/update-checker.ts +110 -0
  523. package/src/utils/usage-banner-state.ts +146 -0
  524. package/src/utils/validation-error-formatting.ts +86 -0
  525. package/src/utils/validation-error-helpers.ts +19 -0
  526. package/src/utils/windows-terminal-health.ts +124 -0
  527. package/src/utils/word-wrap-utils.ts +53 -0
  528. package/src/utils/write-file-atomic.ts +54 -0
  529. package/src/utils/yield-to-event-loop.ts +9 -0
@@ -0,0 +1,486 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
4
+
5
+ import {
6
+ clearMockedModules,
7
+ mockModule,
8
+ } from '@anvil/common/testing/mock-modules'
9
+ import {
10
+ describe,
11
+ test,
12
+ expect,
13
+ beforeEach,
14
+ afterEach,
15
+ mock,
16
+ spyOn,
17
+ } from 'bun:test'
18
+
19
+ import { setProjectRoot } from '../../project-files'
20
+ import * as authModule from '../../utils/auth'
21
+ import { saveUserCredentials, getUserCredentials } from '../../utils/auth'
22
+
23
+ import type { User } from '../../utils/auth'
24
+
25
+ /**
26
+ * Integration tests for credential storage and retrieval
27
+ *
28
+ * These tests verify the complete flow of saving, loading, and managing
29
+ * user credentials on the file system. Credentials are stored in:
30
+ * - Dev: ~/.config/manicode-dev/credentials.json
31
+ * - Prod: ~/.config/manicode/credentials.json
32
+ *
33
+ * Tests ensure:
34
+ * - Directories are created if missing
35
+ * - JSON format is correct and parseable
36
+ * - Credentials persist across CLI restarts
37
+ * - File operations are atomic (no partial writes)
38
+ * - Environment variable detection works (dev vs prod)
39
+ */
40
+
41
+ const TEST_USER: User = {
42
+ id: 'test-user-123',
43
+ name: 'Test User',
44
+ email: 'test@example.com',
45
+ authToken: 'test-session-token-abc',
46
+ fingerprintId: 'test-fingerprint',
47
+ fingerprintHash: 'test-hash',
48
+ }
49
+
50
+ describe('Credentials Storage Integration', () => {
51
+ let tempConfigDir: string
52
+
53
+ beforeEach(() => {
54
+ // Create temporary config directory for tests
55
+ tempConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manicode-test-'))
56
+
57
+ // Set project root to avoid "Project root not set" error in logger
58
+ setProjectRoot(tempConfigDir)
59
+
60
+ // Mock getConfigDir to use temp directory
61
+ spyOn(authModule, 'getConfigDir').mockReturnValue(tempConfigDir)
62
+ spyOn(authModule, 'getCredentialsPath').mockReturnValue(
63
+ path.join(tempConfigDir, 'credentials.json'),
64
+ )
65
+ })
66
+
67
+ afterEach(() => {
68
+ // Clean up temp directory
69
+ if (fs.existsSync(tempConfigDir)) {
70
+ fs.rmSync(tempConfigDir, { recursive: true, force: true })
71
+ }
72
+
73
+ mock.restore()
74
+ clearMockedModules()
75
+ })
76
+
77
+ describe('P0: File System Operations', () => {
78
+ test('should create config directory if it does not exist', () => {
79
+ // Ensure directory doesn't exist initially
80
+ if (fs.existsSync(tempConfigDir)) {
81
+ fs.rmSync(tempConfigDir, { recursive: true })
82
+ }
83
+ expect(fs.existsSync(tempConfigDir)).toBe(false)
84
+
85
+ // Call saveUserCredentials - should create directory
86
+ saveUserCredentials(TEST_USER)
87
+
88
+ // Verify directory was created
89
+ expect(fs.existsSync(tempConfigDir)).toBe(true)
90
+
91
+ // Verify it's a directory
92
+ const stats = fs.statSync(tempConfigDir)
93
+ expect(stats.isDirectory()).toBe(true)
94
+
95
+ // Verify credentials file was created in the directory
96
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
97
+ expect(fs.existsSync(credentialsPath)).toBe(true)
98
+ })
99
+
100
+ test('should write credentials.json with correct JSON format', () => {
101
+ // Call saveUserCredentials
102
+ saveUserCredentials(TEST_USER)
103
+
104
+ // Read the credentials file
105
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
106
+ const fileContent = fs.readFileSync(credentialsPath, 'utf8')
107
+
108
+ // Parse JSON
109
+ const parsed = JSON.parse(fileContent)
110
+
111
+ // Verify structure is { default: { ...user fields } }
112
+ expect(parsed).toHaveProperty('default')
113
+ expect(typeof parsed.default).toBe('object')
114
+
115
+ // Verify all user fields are present
116
+ expect(parsed.default.id).toBe(TEST_USER.id)
117
+ expect(parsed.default.name).toBe(TEST_USER.name)
118
+ expect(parsed.default.email).toBe(TEST_USER.email)
119
+ expect(parsed.default.authToken).toBe(TEST_USER.authToken)
120
+ expect(parsed.default.fingerprintId).toBe(TEST_USER.fingerprintId)
121
+ expect(parsed.default.fingerprintHash).toBe(TEST_USER.fingerprintHash)
122
+ })
123
+
124
+ test('should overwrite existing credentials when saving new ones', () => {
125
+ // Save initial credentials
126
+ saveUserCredentials(TEST_USER)
127
+
128
+ // Read and verify first credentials
129
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
130
+ let fileContent = fs.readFileSync(credentialsPath, 'utf8')
131
+ let parsed = JSON.parse(fileContent)
132
+ expect(parsed.default.id).toBe(TEST_USER.id)
133
+
134
+ // Save different credentials
135
+ const newUser: User = {
136
+ id: 'different-user-456',
137
+ name: 'Different User',
138
+ email: 'different@example.com',
139
+ authToken: 'different-token',
140
+ fingerprintId: 'different-fingerprint',
141
+ fingerprintHash: 'different-hash',
142
+ }
143
+ saveUserCredentials(newUser)
144
+
145
+ // Read again
146
+ fileContent = fs.readFileSync(credentialsPath, 'utf8')
147
+ parsed = JSON.parse(fileContent)
148
+
149
+ // Verify new credentials replaced old ones
150
+ expect(parsed.default.id).toBe(newUser.id)
151
+ expect(parsed.default.name).toBe(newUser.name)
152
+ expect(parsed.default.email).toBe(newUser.email)
153
+ expect(parsed.default.authToken).toBe(newUser.authToken)
154
+
155
+ // Verify only one 'default' entry exists
156
+ const keys = Object.keys(parsed)
157
+ expect(keys.length).toBe(1)
158
+ expect(keys[0]).toBe('default')
159
+ })
160
+
161
+ test('should use manicode-test directory in test environment', async () => {
162
+ // Restore getConfigDir to use real implementation for this test
163
+ mock.restore()
164
+
165
+ await mockModule('@anvil/common/env', () => ({
166
+ env: { NEXT_PUBLIC_CB_ENVIRONMENT: 'test' },
167
+ }))
168
+
169
+ // Call real getConfigDir to verify it includes '-dev'
170
+ const configDir = authModule.getConfigDir()
171
+ expect(configDir).toEqual(
172
+ path.join(os.homedir(), '.config', 'manicode-test'),
173
+ )
174
+ })
175
+
176
+ test('should use manicode-dev directory in development environment', async () => {
177
+ // Restore getConfigDir to use real implementation for this test
178
+ mock.restore()
179
+
180
+ await mockModule('@anvil/common/env', () => ({
181
+ env: { NEXT_PUBLIC_CB_ENVIRONMENT: 'dev' },
182
+ }))
183
+
184
+ // Call real getConfigDir to verify it includes '-dev'
185
+ const configDir = authModule.getConfigDir()
186
+ expect(configDir).toEqual(
187
+ path.join(os.homedir(), '.config', 'manicode-dev'),
188
+ )
189
+ })
190
+
191
+ test('should use manicode directory in production environment', async () => {
192
+ // Restore getConfigDir to use real implementation
193
+ mock.restore()
194
+
195
+ // Set environment to prod (or unset it)
196
+ await mockModule('@anvil/common/env', () => ({
197
+ env: { NEXT_PUBLIC_CB_ENVIRONMENT: 'prod' },
198
+ }))
199
+
200
+ // Call real getConfigDir to verify it doesn't include '-dev'
201
+ const configDir = authModule.getConfigDir()
202
+ expect(configDir).toEqual(path.join(os.homedir(), '.config', 'manicode'))
203
+ })
204
+
205
+ test('should allow credentials to persist across simulated CLI restarts', () => {
206
+ // Save credentials
207
+ saveUserCredentials(TEST_USER)
208
+
209
+ // Simulate CLI restart by calling getUserCredentials
210
+ // (simulates reading from disk on fresh startup)
211
+ const loadedCredentials = getUserCredentials()
212
+
213
+ // Verify credentials are loaded from file
214
+ expect(loadedCredentials).not.toBeNull()
215
+ expect(loadedCredentials).toBeDefined()
216
+
217
+ // Verify all fields match what was saved
218
+ expect(loadedCredentials!.id).toBe(TEST_USER.id)
219
+ expect(loadedCredentials!.name).toBe(TEST_USER.name)
220
+ expect(loadedCredentials!.email).toBe(TEST_USER.email)
221
+ expect(loadedCredentials!.authToken).toBe(TEST_USER.authToken)
222
+ expect(loadedCredentials!.fingerprintId).toBe(TEST_USER.fingerprintId)
223
+ expect(loadedCredentials!.fingerprintHash).toBe(TEST_USER.fingerprintHash)
224
+ })
225
+
226
+ test('drops the removed ChatGPT integration token on the next write', () => {
227
+ // Anyone who ran /connect-chatgpt still has an OAuth access + refresh
228
+ // token for their own ChatGPT account sitting in this file. Nothing reads
229
+ // it now and the command that managed it is gone, so it must not survive
230
+ // a rewrite — otherwise it is orphaned on disk with no way to clear it.
231
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
232
+ fs.writeFileSync(
233
+ credentialsPath,
234
+ JSON.stringify({
235
+ default: { ...TEST_USER, authToken: 'stale' },
236
+ chatgptOAuth: {
237
+ accessToken: 'a',
238
+ refreshToken: 'r',
239
+ expiresAt: 1,
240
+ connectedAt: 1,
241
+ },
242
+ someOtherKey: 'kept',
243
+ }),
244
+ )
245
+
246
+ saveUserCredentials(TEST_USER)
247
+
248
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
249
+ expect(parsed.chatgptOAuth).toBeUndefined()
250
+ // Only that one key goes: unrelated entries and the login still stand.
251
+ expect(parsed.someOtherKey).toBe('kept')
252
+ expect(parsed.default.authToken).toBe(TEST_USER.authToken)
253
+ })
254
+ })
255
+
256
+ describe('P0: Credential Format Validation', () => {
257
+ test('should save user ID in credentials', () => {
258
+ saveUserCredentials(TEST_USER)
259
+
260
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
261
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
262
+
263
+ expect(parsed.default.id).toBe(TEST_USER.id)
264
+ })
265
+
266
+ test('should save user name in credentials', () => {
267
+ saveUserCredentials(TEST_USER)
268
+
269
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
270
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
271
+
272
+ expect(parsed.default.name).toBe(TEST_USER.name)
273
+ })
274
+
275
+ test('should save user email in credentials', () => {
276
+ saveUserCredentials(TEST_USER)
277
+
278
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
279
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
280
+
281
+ expect(parsed.default.email).toBe(TEST_USER.email)
282
+ })
283
+
284
+ test('should save authToken (session token) in credentials', () => {
285
+ saveUserCredentials(TEST_USER)
286
+
287
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
288
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
289
+
290
+ // authToken is the most critical field for authentication
291
+ expect(parsed.default.authToken).toBe(TEST_USER.authToken)
292
+ expect(parsed.default.authToken).toBeTruthy()
293
+ })
294
+
295
+ test('should save fingerprintId in credentials', () => {
296
+ saveUserCredentials(TEST_USER)
297
+
298
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
299
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
300
+
301
+ expect(parsed.default.fingerprintId).toBe(TEST_USER.fingerprintId)
302
+ })
303
+
304
+ test('should save fingerprintHash in credentials', () => {
305
+ saveUserCredentials(TEST_USER)
306
+
307
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
308
+ const parsed = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
309
+
310
+ expect(parsed.default.fingerprintHash).toBe(TEST_USER.fingerprintHash)
311
+ })
312
+
313
+ test('should produce valid, parseable JSON', () => {
314
+ saveUserCredentials(TEST_USER)
315
+
316
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
317
+ const fileContent = fs.readFileSync(credentialsPath, 'utf8')
318
+
319
+ // Verify JSON.parse doesn't throw
320
+ let parsed: any
321
+ expect(() => {
322
+ parsed = JSON.parse(fileContent)
323
+ }).not.toThrow()
324
+
325
+ // Verify parsed object has expected structure
326
+ expect(parsed).toHaveProperty('default')
327
+ expect(typeof parsed.default).toBe('object')
328
+ expect(parsed.default).toHaveProperty('id')
329
+ expect(parsed.default).toHaveProperty('authToken')
330
+ })
331
+ })
332
+
333
+ describe('P2: File System Edge Cases', () => {
334
+ test('should preserve file permissions when writing credentials', () => {
335
+ // Save credentials
336
+ saveUserCredentials(TEST_USER)
337
+
338
+ // Check file permissions
339
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
340
+ const stats = fs.statSync(credentialsPath)
341
+ const mode = stats.mode
342
+
343
+ // On Unix systems, check permissions
344
+ if (process.platform !== 'win32') {
345
+ // File should be readable by user
346
+ expect((mode & 0o400) !== 0).toBe(true)
347
+
348
+ // File should be writable by user
349
+ expect((mode & 0o200) !== 0).toBe(true)
350
+ } else {
351
+ // On Windows, just verify file exists and is accessible
352
+ expect(fs.existsSync(credentialsPath)).toBe(true)
353
+ }
354
+ })
355
+
356
+ test('should handle write permission errors gracefully', () => {
357
+ // Mock fs.writeFileSync to throw EACCES error
358
+ const writeError = new Error(
359
+ 'EACCES: permission denied',
360
+ ) as NodeJS.ErrnoException
361
+ writeError.code = 'EACCES'
362
+
363
+ const writeFileSyncSpy = spyOn(fs, 'writeFileSync').mockImplementation(
364
+ () => {
365
+ throw writeError
366
+ },
367
+ )
368
+
369
+ // Attempt to save credentials - should throw since we're not catching in saveUserCredentials
370
+ expect(() => {
371
+ saveUserCredentials(TEST_USER)
372
+ }).toThrow('EACCES')
373
+
374
+ // Verify writeFileSync was attempted
375
+ expect(writeFileSyncSpy).toHaveBeenCalled()
376
+ })
377
+
378
+ test('should show clear error message on permission denial', () => {
379
+ // This test verifies that when permission is denied, the error is logged
380
+ const writeError = new Error(
381
+ "EACCES: permission denied, open '/test/credentials.json'",
382
+ ) as NodeJS.ErrnoException
383
+ writeError.code = 'EACCES'
384
+
385
+ spyOn(fs, 'writeFileSync').mockImplementation(() => {
386
+ throw writeError
387
+ })
388
+
389
+ // Attempt to save credentials - will throw and get logged
390
+ expect(() => {
391
+ saveUserCredentials(TEST_USER)
392
+ }).toThrow()
393
+
394
+ // The actual error logging happens in saveUserCredentials via logger.error
395
+ // The error message includes the error details which would help users diagnose
396
+ })
397
+
398
+ test('should gracefully degrade if credentials cannot be written', () => {
399
+ // This tests that the error is thrown (not silently swallowed)
400
+ // The caller (login mutation) is responsible for handling the error gracefully
401
+ const writeError = new Error(
402
+ 'ENOSPC: no space left on device',
403
+ ) as NodeJS.ErrnoException
404
+ writeError.code = 'ENOSPC'
405
+
406
+ spyOn(fs, 'writeFileSync').mockImplementation(() => {
407
+ throw writeError
408
+ })
409
+
410
+ // Attempt to save - should throw to caller who can handle it
411
+ expect(() => {
412
+ saveUserCredentials(TEST_USER)
413
+ }).toThrow('ENOSPC')
414
+
415
+ // The login mutation's onError handler would catch this and allow
416
+ // the user to continue with in-memory credentials
417
+ })
418
+ })
419
+
420
+ describe('P2: Concurrent Operations', () => {
421
+ test('should handle rapid saves without race conditions', () => {
422
+ // Create different user objects for rapid saves
423
+ const users: User[] = []
424
+ for (let i = 0; i < 5; i++) {
425
+ users.push({
426
+ id: `user-${i}`,
427
+ name: `User ${i}`,
428
+ email: `user${i}@example.com`,
429
+ authToken: `token-${i}`,
430
+ fingerprintId: `fingerprint-${i}`,
431
+ fingerprintHash: `hash-${i}`,
432
+ })
433
+ }
434
+
435
+ // Call saveUserCredentials 5 times rapidly
436
+ users.forEach((user) => saveUserCredentials(user))
437
+
438
+ // Read final credentials
439
+ const credentialsPath = path.join(tempConfigDir, 'credentials.json')
440
+ const fileContent = fs.readFileSync(credentialsPath, 'utf8')
441
+ const parsed = JSON.parse(fileContent)
442
+
443
+ // Verify file contains the last saved data (user-4)
444
+ expect(parsed.default.id).toBe('user-4')
445
+ expect(parsed.default.name).toBe('User 4')
446
+
447
+ // Verify no corrupted/partial data - JSON should be valid
448
+ expect(parsed).toHaveProperty('default')
449
+ expect(typeof parsed.default.authToken).toBe('string')
450
+ })
451
+
452
+ test('should handle read during write without corruption', () => {
453
+ // Since fs.writeFileSync is synchronous, there's no actual concurrency
454
+ // But we can verify that writes are atomic (no partial data)
455
+
456
+ // Save initial credentials
457
+ saveUserCredentials(TEST_USER)
458
+
459
+ // Read credentials
460
+ const loadedBefore = getUserCredentials()
461
+ expect(loadedBefore).not.toBeNull()
462
+ expect(loadedBefore!.id).toBe(TEST_USER.id)
463
+
464
+ // Save new credentials
465
+ const newUser: User = {
466
+ id: 'new-user-789',
467
+ name: 'New User',
468
+ email: 'new@example.com',
469
+ authToken: 'new-token',
470
+ fingerprintId: 'new-fingerprint',
471
+ fingerprintHash: 'new-hash',
472
+ }
473
+ saveUserCredentials(newUser)
474
+
475
+ // Read again - should get complete new data (not partial/mixed)
476
+ const loadedAfter = getUserCredentials()
477
+ expect(loadedAfter).not.toBeNull()
478
+ expect(loadedAfter!.id).toBe(newUser.id)
479
+ expect(loadedAfter!.name).toBe(newUser.name)
480
+ expect(loadedAfter!.authToken).toBe(newUser.authToken)
481
+
482
+ // Verify NOT corrupted (not mixed old and new data)
483
+ expect(loadedAfter!.id).not.toBe(TEST_USER.id)
484
+ })
485
+ })
486
+ })