mdcontext 0.0.1 → 0.2.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 (337) hide show
  1. package/.changeset/README.md +28 -0
  2. package/.changeset/config.json +11 -0
  3. package/.claude/settings.local.json +25 -0
  4. package/.github/workflows/ci.yml +83 -0
  5. package/.github/workflows/claude-code-review.yml +44 -0
  6. package/.github/workflows/claude.yml +85 -0
  7. package/.github/workflows/release.yml +113 -0
  8. package/.tldrignore +112 -0
  9. package/BACKLOG.md +338 -0
  10. package/CONTRIBUTING.md +186 -0
  11. package/NOTES/NOTES +44 -0
  12. package/README.md +434 -11
  13. package/biome.json +36 -0
  14. package/cspell.config.yaml +14 -0
  15. package/dist/chunk-23UPXDNL.js +3044 -0
  16. package/dist/chunk-2W7MO2DL.js +1366 -0
  17. package/dist/chunk-3NUAZGMA.js +1689 -0
  18. package/dist/chunk-7TOWB2XB.js +366 -0
  19. package/dist/chunk-7XOTOADQ.js +3065 -0
  20. package/dist/chunk-AH2PDM2K.js +3042 -0
  21. package/dist/chunk-BNXWSZ63.js +3742 -0
  22. package/dist/chunk-BTL5DJVU.js +3222 -0
  23. package/dist/chunk-HDHYG7E4.js +104 -0
  24. package/dist/chunk-HLR4KZBP.js +3234 -0
  25. package/dist/chunk-IP3FRFEB.js +1045 -0
  26. package/dist/chunk-KHU56VDO.js +3042 -0
  27. package/dist/chunk-KRYIFLQR.js +88 -0
  28. package/dist/chunk-LBSDNLEM.js +287 -0
  29. package/dist/chunk-MNTQ7HCP.js +2643 -0
  30. package/dist/chunk-MUJELQQ6.js +1387 -0
  31. package/dist/chunk-MXJGMSLV.js +2199 -0
  32. package/dist/chunk-N6QJGC3Z.js +2636 -0
  33. package/dist/chunk-OBELGBPM.js +1713 -0
  34. package/dist/chunk-OT7R5XTA.js +3192 -0
  35. package/dist/chunk-P7X4RA2T.js +106 -0
  36. package/dist/chunk-PIDUQNC2.js +3185 -0
  37. package/dist/chunk-POGCDIH4.js +3187 -0
  38. package/dist/chunk-PSIEOQGZ.js +3043 -0
  39. package/dist/chunk-PVRT3IHA.js +3238 -0
  40. package/dist/chunk-QNN4TT23.js +1430 -0
  41. package/dist/chunk-RE3R45RJ.js +3042 -0
  42. package/dist/chunk-S7E6TFX6.js +803 -0
  43. package/dist/chunk-SG6GLU4U.js +1378 -0
  44. package/dist/chunk-SJCDV2ST.js +274 -0
  45. package/dist/chunk-SYE5XLF3.js +104 -0
  46. package/dist/chunk-T5VLYBZD.js +103 -0
  47. package/dist/chunk-TOQB7VWU.js +3238 -0
  48. package/dist/chunk-VFNMZ4ZQ.js +3228 -0
  49. package/dist/chunk-VVTGZNBT.js +1629 -0
  50. package/dist/chunk-W7Q4RFEV.js +104 -0
  51. package/dist/chunk-XTYYVRLO.js +3190 -0
  52. package/dist/chunk-Y6MDYVJD.js +3063 -0
  53. package/dist/cli/main.d.ts +1 -0
  54. package/dist/cli/main.js +5458 -0
  55. package/dist/index.d.ts +653 -0
  56. package/dist/index.js +79 -0
  57. package/dist/mcp/server.d.ts +1 -0
  58. package/dist/mcp/server.js +472 -0
  59. package/dist/schema-BAWSG7KY.js +22 -0
  60. package/dist/schema-E3QUPL26.js +20 -0
  61. package/dist/schema-EHL7WUT6.js +20 -0
  62. package/docs/019-USAGE.md +625 -0
  63. package/docs/020-current-implementation.md +364 -0
  64. package/docs/021-DOGFOODING-FINDINGS.md +175 -0
  65. package/docs/BACKLOG.md +80 -0
  66. package/docs/CONFIG.md +1123 -0
  67. package/docs/DESIGN.md +439 -0
  68. package/docs/ERRORS.md +383 -0
  69. package/docs/PROJECT.md +88 -0
  70. package/docs/ROADMAP.md +407 -0
  71. package/docs/summarization.md +320 -0
  72. package/docs/test-links.md +9 -0
  73. package/justfile +40 -0
  74. package/package.json +74 -9
  75. package/pnpm-workspace.yaml +5 -0
  76. package/research/INDEX.md +315 -0
  77. package/research/code-review/README.md +90 -0
  78. package/research/code-review/cli-error-handling-review.md +979 -0
  79. package/research/code-review/code-review-validation-report.md +464 -0
  80. package/research/code-review/main-ts-review.md +1128 -0
  81. package/research/config-analysis/01-current-implementation.md +470 -0
  82. package/research/config-analysis/02-strategy-recommendation.md +428 -0
  83. package/research/config-analysis/03-task-candidates.md +715 -0
  84. package/research/config-analysis/033-research-configuration-management.md +828 -0
  85. package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
  86. package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
  87. package/research/config-docs/SUMMARY.md +357 -0
  88. package/research/config-docs/TEST-RESULTS.md +776 -0
  89. package/research/config-docs/TODO.md +542 -0
  90. package/research/config-docs/analysis.md +744 -0
  91. package/research/config-docs/fix-validation.md +502 -0
  92. package/research/config-docs/help-audit.md +264 -0
  93. package/research/config-docs/help-system-analysis.md +890 -0
  94. package/research/dogfood/consolidated-tool-evaluation.md +373 -0
  95. package/research/dogfood/strategy-a/a-synthesis.md +184 -0
  96. package/research/dogfood/strategy-a/a1-docs.md +226 -0
  97. package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
  98. package/research/dogfood/strategy-a/a3-llm.md +164 -0
  99. package/research/dogfood/strategy-b/b-synthesis.md +228 -0
  100. package/research/dogfood/strategy-b/b1-architecture.md +207 -0
  101. package/research/dogfood/strategy-b/b2-gaps.md +258 -0
  102. package/research/dogfood/strategy-b/b3-workflows.md +250 -0
  103. package/research/dogfood/strategy-c/c-synthesis.md +451 -0
  104. package/research/dogfood/strategy-c/c1-explorer.md +192 -0
  105. package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
  106. package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
  107. package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
  108. package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
  109. package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
  110. package/research/effect-cli-error-handling.md +845 -0
  111. package/research/effect-errors-as-values.md +943 -0
  112. package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
  113. package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
  114. package/research/errors-task-analysis/embeddings-analysis.md +709 -0
  115. package/research/errors-task-analysis/index-search-analysis.md +812 -0
  116. package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
  117. package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
  118. package/research/issue-review.md +603 -0
  119. package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
  120. package/research/llm-summarization/alternative-providers-2026.md +1428 -0
  121. package/research/llm-summarization/anthropic-2026.md +367 -0
  122. package/research/llm-summarization/claude-cli-integration.md +1706 -0
  123. package/research/llm-summarization/cli-integration-patterns.md +3155 -0
  124. package/research/llm-summarization/openai-2026.md +473 -0
  125. package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
  126. package/research/llm-summarization/opencode-cli-integration.md +1552 -0
  127. package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
  128. package/research/llm-summarization/prototype-results.md +56 -0
  129. package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
  130. package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
  131. package/research/mdcontext-error-analysis.md +521 -0
  132. package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
  133. package/research/mdcontext-pudding/01-index-embed.md +956 -0
  134. package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
  135. package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
  136. package/research/mdcontext-pudding/02-search.md +970 -0
  137. package/research/mdcontext-pudding/03-context.md +779 -0
  138. package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
  139. package/research/mdcontext-pudding/04-tree.md +704 -0
  140. package/research/mdcontext-pudding/05-config.md +1038 -0
  141. package/research/mdcontext-pudding/06-links-summary.txt +87 -0
  142. package/research/mdcontext-pudding/06-links.md +679 -0
  143. package/research/mdcontext-pudding/07-stats.md +693 -0
  144. package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
  145. package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
  146. package/research/mdcontext-pudding/README.md +168 -0
  147. package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
  148. package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
  149. package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
  150. package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
  151. package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
  152. package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
  153. package/research/research-quality-review.md +834 -0
  154. package/research/semantic-search/002-research-embedding-models.md +490 -0
  155. package/research/semantic-search/003-research-rag-alternatives.md +523 -0
  156. package/research/semantic-search/004-research-vector-search.md +841 -0
  157. package/research/semantic-search/032-research-semantic-search.md +427 -0
  158. package/research/semantic-search/embedding-text-analysis.md +156 -0
  159. package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
  160. package/research/semantic-search/query-processing-analysis.md +207 -0
  161. package/research/semantic-search/root-cause-and-solution.md +114 -0
  162. package/research/semantic-search/threshold-validation-report.md +69 -0
  163. package/research/semantic-search/vector-search-analysis.md +63 -0
  164. package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
  165. package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
  166. package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
  167. package/research/task-management-2026/03-lightweight-file-based.md +567 -0
  168. package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
  169. package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
  170. package/research/task-management-2026/linear/02-api-integrations.md +930 -0
  171. package/research/task-management-2026/linear/03-ai-features.md +368 -0
  172. package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
  173. package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
  174. package/research/test-path-issues.md +276 -0
  175. package/review/ALP-76/1-error-type-design.md +962 -0
  176. package/review/ALP-76/2-error-handling-patterns.md +906 -0
  177. package/review/ALP-76/3-error-presentation.md +624 -0
  178. package/review/ALP-76/4-test-coverage.md +625 -0
  179. package/review/ALP-76/5-migration-completeness.md +440 -0
  180. package/review/ALP-76/6-effect-best-practices.md +755 -0
  181. package/scripts/apply-branch-protection.sh +47 -0
  182. package/scripts/branch-protection-templates.json +79 -0
  183. package/scripts/prototype-summarization.ts +346 -0
  184. package/scripts/rebuild-hnswlib.js +58 -0
  185. package/scripts/setup-branch-protection.sh +64 -0
  186. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
  187. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
  188. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
  189. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
  190. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  191. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  192. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
  193. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
  194. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
  195. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
  196. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
  197. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
  198. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
  199. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
  200. package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
  201. package/src/cli/argv-preprocessor.test.ts +210 -0
  202. package/src/cli/argv-preprocessor.ts +202 -0
  203. package/src/cli/cli.test.ts +627 -0
  204. package/src/cli/commands/backlinks.ts +54 -0
  205. package/src/cli/commands/config-cmd.ts +642 -0
  206. package/src/cli/commands/context.ts +285 -0
  207. package/src/cli/commands/duplicates.ts +122 -0
  208. package/src/cli/commands/embeddings.ts +529 -0
  209. package/src/cli/commands/index-cmd.ts +480 -0
  210. package/src/cli/commands/index.ts +16 -0
  211. package/src/cli/commands/links.ts +52 -0
  212. package/src/cli/commands/search.ts +1281 -0
  213. package/src/cli/commands/stats.ts +149 -0
  214. package/src/cli/commands/tree.ts +128 -0
  215. package/src/cli/config-layer.ts +176 -0
  216. package/src/cli/error-handler.test.ts +235 -0
  217. package/src/cli/error-handler.ts +655 -0
  218. package/src/cli/flag-schemas.ts +341 -0
  219. package/src/cli/help.ts +588 -0
  220. package/src/cli/index.ts +9 -0
  221. package/src/cli/main.ts +435 -0
  222. package/src/cli/options.ts +41 -0
  223. package/src/cli/shared-error-handling.ts +199 -0
  224. package/src/cli/typo-suggester.test.ts +105 -0
  225. package/src/cli/typo-suggester.ts +130 -0
  226. package/src/cli/utils.ts +259 -0
  227. package/src/config/file-provider.test.ts +320 -0
  228. package/src/config/file-provider.ts +273 -0
  229. package/src/config/index.ts +72 -0
  230. package/src/config/integration.test.ts +667 -0
  231. package/src/config/precedence.test.ts +277 -0
  232. package/src/config/precedence.ts +451 -0
  233. package/src/config/schema.test.ts +414 -0
  234. package/src/config/schema.ts +603 -0
  235. package/src/config/service.test.ts +320 -0
  236. package/src/config/service.ts +243 -0
  237. package/src/config/testing.test.ts +264 -0
  238. package/src/config/testing.ts +110 -0
  239. package/src/core/index.ts +1 -0
  240. package/src/core/types.ts +113 -0
  241. package/src/duplicates/detector.test.ts +183 -0
  242. package/src/duplicates/detector.ts +414 -0
  243. package/src/duplicates/index.ts +18 -0
  244. package/src/embeddings/embedding-namespace.test.ts +300 -0
  245. package/src/embeddings/embedding-namespace.ts +947 -0
  246. package/src/embeddings/heading-boost.test.ts +222 -0
  247. package/src/embeddings/hnsw-build-options.test.ts +198 -0
  248. package/src/embeddings/hyde.test.ts +272 -0
  249. package/src/embeddings/hyde.ts +264 -0
  250. package/src/embeddings/index.ts +10 -0
  251. package/src/embeddings/openai-provider.ts +414 -0
  252. package/src/embeddings/pricing.json +22 -0
  253. package/src/embeddings/provider-constants.ts +204 -0
  254. package/src/embeddings/provider-errors.test.ts +967 -0
  255. package/src/embeddings/provider-errors.ts +565 -0
  256. package/src/embeddings/provider-factory.test.ts +240 -0
  257. package/src/embeddings/provider-factory.ts +225 -0
  258. package/src/embeddings/provider-integration.test.ts +788 -0
  259. package/src/embeddings/query-preprocessing.test.ts +187 -0
  260. package/src/embeddings/semantic-search-threshold.test.ts +508 -0
  261. package/src/embeddings/semantic-search.ts +1270 -0
  262. package/src/embeddings/types.ts +359 -0
  263. package/src/embeddings/vector-store.ts +708 -0
  264. package/src/embeddings/voyage-provider.ts +313 -0
  265. package/src/errors/errors.test.ts +845 -0
  266. package/src/errors/index.ts +533 -0
  267. package/src/index/ignore-patterns.test.ts +354 -0
  268. package/src/index/ignore-patterns.ts +305 -0
  269. package/src/index/index.ts +4 -0
  270. package/src/index/indexer.ts +684 -0
  271. package/src/index/storage.ts +260 -0
  272. package/src/index/types.ts +147 -0
  273. package/src/index/watcher.ts +189 -0
  274. package/src/index.ts +30 -0
  275. package/src/integration/search-keyword.test.ts +678 -0
  276. package/src/mcp/server.ts +612 -0
  277. package/src/parser/index.ts +1 -0
  278. package/src/parser/parser.test.ts +291 -0
  279. package/src/parser/parser.ts +394 -0
  280. package/src/parser/section-filter.test.ts +277 -0
  281. package/src/parser/section-filter.ts +392 -0
  282. package/src/search/__tests__/hybrid-search.test.ts +650 -0
  283. package/src/search/bm25-store.ts +366 -0
  284. package/src/search/cross-encoder.test.ts +253 -0
  285. package/src/search/cross-encoder.ts +406 -0
  286. package/src/search/fuzzy-search.test.ts +419 -0
  287. package/src/search/fuzzy-search.ts +273 -0
  288. package/src/search/hybrid-search.ts +448 -0
  289. package/src/search/path-matcher.test.ts +276 -0
  290. package/src/search/path-matcher.ts +33 -0
  291. package/src/search/query-parser.test.ts +260 -0
  292. package/src/search/query-parser.ts +319 -0
  293. package/src/search/searcher.test.ts +280 -0
  294. package/src/search/searcher.ts +724 -0
  295. package/src/search/wink-bm25.d.ts +30 -0
  296. package/src/summarization/cli-providers/claude.ts +202 -0
  297. package/src/summarization/cli-providers/detection.test.ts +273 -0
  298. package/src/summarization/cli-providers/detection.ts +118 -0
  299. package/src/summarization/cli-providers/index.ts +8 -0
  300. package/src/summarization/cost.test.ts +139 -0
  301. package/src/summarization/cost.ts +102 -0
  302. package/src/summarization/error-handler.test.ts +127 -0
  303. package/src/summarization/error-handler.ts +111 -0
  304. package/src/summarization/index.ts +102 -0
  305. package/src/summarization/pipeline.test.ts +498 -0
  306. package/src/summarization/pipeline.ts +231 -0
  307. package/src/summarization/prompts.test.ts +269 -0
  308. package/src/summarization/prompts.ts +133 -0
  309. package/src/summarization/provider-factory.test.ts +396 -0
  310. package/src/summarization/provider-factory.ts +178 -0
  311. package/src/summarization/types.ts +184 -0
  312. package/src/summarize/budget-bugs.test.ts +620 -0
  313. package/src/summarize/formatters.ts +419 -0
  314. package/src/summarize/index.ts +20 -0
  315. package/src/summarize/summarizer.test.ts +275 -0
  316. package/src/summarize/summarizer.ts +597 -0
  317. package/src/summarize/verify-bugs.test.ts +238 -0
  318. package/src/types/huggingface-transformers.d.ts +66 -0
  319. package/src/utils/index.ts +1 -0
  320. package/src/utils/tokens.test.ts +142 -0
  321. package/src/utils/tokens.ts +186 -0
  322. package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
  323. package/tests/fixtures/cli/.mdcontext/config.json +8 -0
  324. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
  325. package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
  326. package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
  327. package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
  328. package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
  329. package/tests/fixtures/cli/README.md +9 -0
  330. package/tests/fixtures/cli/api-reference.md +11 -0
  331. package/tests/fixtures/cli/getting-started.md +11 -0
  332. package/tests/integration/embed-index.test.ts +712 -0
  333. package/tests/integration/search-context.test.ts +469 -0
  334. package/tests/integration/search-semantic.test.ts +522 -0
  335. package/tsconfig.json +26 -0
  336. package/vitest.config.ts +16 -0
  337. package/vitest.setup.ts +12 -0
@@ -0,0 +1,967 @@
1
+ /**
2
+ * Provider Error Detection Unit Tests
3
+ *
4
+ * Tests for provider-specific error detection, suggestions, and titles
5
+ * for Ollama, LM Studio, OpenRouter, and generic network errors.
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest'
9
+ import {
10
+ detectProviderError,
11
+ detectProviderFromError,
12
+ getProviderErrorTitle,
13
+ getProviderSuggestions,
14
+ type ProviderError,
15
+ RECOMMENDED_OLLAMA_MODELS,
16
+ } from './provider-errors.js'
17
+
18
+ // ============================================================================
19
+ // detectProviderError Tests
20
+ // ============================================================================
21
+
22
+ describe('detectProviderError', () => {
23
+ // ==========================================================================
24
+ // Ollama Provider
25
+ // ==========================================================================
26
+
27
+ describe('Ollama', () => {
28
+ it('detects daemon-not-running from ECONNREFUSED on port 11434', () => {
29
+ const error = new Error('connect ECONNREFUSED 127.0.0.1:11434')
30
+ const result = detectProviderError('ollama', error)
31
+
32
+ expect(result).not.toBeNull()
33
+ expect(result?.type).toBe('daemon-not-running')
34
+ expect(result?.provider).toBe('ollama')
35
+ expect(result?.message).toBe('Ollama daemon is not running')
36
+ expect(result?.originalError).toBe(error)
37
+ })
38
+
39
+ it('detects daemon-not-running from localhost:11434', () => {
40
+ const error = new Error('Connection refused: localhost:11434')
41
+ const result = detectProviderError('ollama', error)
42
+
43
+ expect(result?.type).toBe('daemon-not-running')
44
+ expect(result?.provider).toBe('ollama')
45
+ })
46
+
47
+ it('detects model-not-found from "model X not found" message', () => {
48
+ const error = new Error("model 'nomic-embed-text' not found")
49
+ const result = detectProviderError('ollama', error)
50
+
51
+ expect(result).not.toBeNull()
52
+ expect(result?.type).toBe('model-not-found')
53
+ expect(result?.provider).toBe('ollama')
54
+ expect(result?.model).toBe('nomic-embed-text')
55
+ expect(result?.message).toBe(
56
+ "Model 'nomic-embed-text' is not installed in Ollama",
57
+ )
58
+ })
59
+
60
+ it('detects model-not-found from "model does not exist" message', () => {
61
+ const error = new Error('model "bge-m3" does not exist')
62
+ const result = detectProviderError('ollama', error)
63
+
64
+ expect(result?.type).toBe('model-not-found')
65
+ expect(result?.model).toBe('bge-m3')
66
+ })
67
+
68
+ it('detects model-not-found and extracts model name with colon pattern', () => {
69
+ const error = new Error('model: mxbai-embed-large not found')
70
+ const result = detectProviderError('ollama', error)
71
+
72
+ expect(result?.type).toBe('model-not-found')
73
+ expect(result?.model).toBe('mxbai-embed-large')
74
+ })
75
+
76
+ it('detects model-loading from "not ready" message', () => {
77
+ const error = new Error("model 'nomic-embed-text' is not ready yet")
78
+ const result = detectProviderError('ollama', error)
79
+
80
+ expect(result).not.toBeNull()
81
+ expect(result?.type).toBe('model-loading')
82
+ expect(result?.provider).toBe('ollama')
83
+ expect(result?.model).toBe('nomic-embed-text')
84
+ expect(result?.message).toBe("Model 'nomic-embed-text' is still loading")
85
+ })
86
+
87
+ it('detects model-loading from "loading" message', () => {
88
+ const error = new Error('model is loading, please wait')
89
+ const result = detectProviderError('ollama', error)
90
+
91
+ expect(result?.type).toBe('model-loading')
92
+ })
93
+
94
+ it('detects model-loading from "initializing" message with quoted model', () => {
95
+ const error = new Error("model 'bge-m3' is initializing")
96
+ const result = detectProviderError('ollama', error)
97
+
98
+ expect(result?.type).toBe('model-loading')
99
+ expect(result?.model).toBe('bge-m3')
100
+ })
101
+
102
+ it('falls back to network error detection for unrecognized Ollama errors', () => {
103
+ const error = new Error('Request timed out')
104
+ const result = detectProviderError('ollama', error)
105
+
106
+ expect(result?.type).toBe('connection-timeout')
107
+ expect(result?.provider).toBe('ollama')
108
+ })
109
+ })
110
+
111
+ // ==========================================================================
112
+ // LM Studio Provider
113
+ // ==========================================================================
114
+
115
+ describe('LM Studio', () => {
116
+ it('detects gui-not-running from ECONNREFUSED on port 1234', () => {
117
+ const error = new Error('connect ECONNREFUSED 127.0.0.1:1234')
118
+ const result = detectProviderError('lm-studio', error)
119
+
120
+ expect(result).not.toBeNull()
121
+ expect(result?.type).toBe('gui-not-running')
122
+ expect(result?.provider).toBe('lm-studio')
123
+ expect(result?.message).toBe(
124
+ 'LM Studio is not running or local server is not started',
125
+ )
126
+ })
127
+
128
+ it('detects gui-not-running from localhost:1234', () => {
129
+ const error = new Error('Connection refused: localhost:1234')
130
+ const result = detectProviderError('lm-studio', error)
131
+
132
+ expect(result?.type).toBe('gui-not-running')
133
+ expect(result?.provider).toBe('lm-studio')
134
+ })
135
+
136
+ it('detects model-not-found from "no model" message', () => {
137
+ const error = new Error('no model is currently loaded')
138
+ const result = detectProviderError('lm-studio', error)
139
+
140
+ expect(result).not.toBeNull()
141
+ expect(result?.type).toBe('model-not-found')
142
+ expect(result?.provider).toBe('lm-studio')
143
+ expect(result?.message).toBe('No embedding model is loaded in LM Studio')
144
+ })
145
+
146
+ it('detects model-not-found from "model not loaded" message', () => {
147
+ const error = new Error('model not loaded')
148
+ const result = detectProviderError('lm-studio', error)
149
+
150
+ expect(result?.type).toBe('model-not-found')
151
+ expect(result?.provider).toBe('lm-studio')
152
+ })
153
+
154
+ it('detects model-not-found from "select a model" message', () => {
155
+ const error = new Error('please select a model first')
156
+ const result = detectProviderError('lm-studio', error)
157
+
158
+ expect(result?.type).toBe('model-not-found')
159
+ expect(result?.provider).toBe('lm-studio')
160
+ })
161
+
162
+ it('falls back to network error detection for unrecognized LM Studio errors', () => {
163
+ const error = new Error('ETIMEDOUT')
164
+ const result = detectProviderError('lm-studio', error)
165
+
166
+ expect(result?.type).toBe('connection-timeout')
167
+ expect(result?.provider).toBe('lm-studio')
168
+ })
169
+ })
170
+
171
+ // ==========================================================================
172
+ // OpenRouter Provider
173
+ // ==========================================================================
174
+
175
+ describe('OpenRouter', () => {
176
+ it('detects invalid-api-key from 401 status', () => {
177
+ const error = new Error('Request failed with status 401')
178
+ const result = detectProviderError('openrouter', error)
179
+
180
+ expect(result).not.toBeNull()
181
+ expect(result?.type).toBe('invalid-api-key')
182
+ expect(result?.provider).toBe('openrouter')
183
+ expect(result?.message).toBe('Invalid or missing OpenRouter API key')
184
+ })
185
+
186
+ it('detects invalid-api-key from "unauthorized" message', () => {
187
+ const error = new Error('Unauthorized access')
188
+ const result = detectProviderError('openrouter', error)
189
+
190
+ expect(result?.type).toBe('invalid-api-key')
191
+ expect(result?.provider).toBe('openrouter')
192
+ })
193
+
194
+ it('detects invalid-api-key from "invalid api key" message', () => {
195
+ const error = new Error('invalid api key provided')
196
+ const result = detectProviderError('openrouter', error)
197
+
198
+ expect(result?.type).toBe('invalid-api-key')
199
+ })
200
+
201
+ it('detects invalid-api-key from "invalid_api_key" message', () => {
202
+ const error = new Error('error: invalid_api_key')
203
+ const result = detectProviderError('openrouter', error)
204
+
205
+ expect(result?.type).toBe('invalid-api-key')
206
+ })
207
+
208
+ it('detects rate-limited from 429 status', () => {
209
+ const error = new Error('Request failed with status 429')
210
+ const result = detectProviderError('openrouter', error)
211
+
212
+ expect(result).not.toBeNull()
213
+ expect(result?.type).toBe('rate-limited')
214
+ expect(result?.provider).toBe('openrouter')
215
+ expect(result?.message).toBe('OpenRouter rate limit reached')
216
+ })
217
+
218
+ it('detects rate-limited from "rate limit" message', () => {
219
+ const error = new Error('rate limit exceeded')
220
+ const result = detectProviderError('openrouter', error)
221
+
222
+ expect(result?.type).toBe('rate-limited')
223
+ expect(result?.provider).toBe('openrouter')
224
+ })
225
+
226
+ it('detects quota-exceeded from "quota" message', () => {
227
+ const error = new Error('your quota has been exceeded')
228
+ const result = detectProviderError('openrouter', error)
229
+
230
+ expect(result).not.toBeNull()
231
+ expect(result?.type).toBe('quota-exceeded')
232
+ expect(result?.provider).toBe('openrouter')
233
+ expect(result?.message).toBe('OpenRouter quota exceeded')
234
+ })
235
+
236
+ it('detects quota-exceeded from "insufficient" message', () => {
237
+ const error = new Error('insufficient credits')
238
+ const result = detectProviderError('openrouter', error)
239
+
240
+ expect(result?.type).toBe('quota-exceeded')
241
+ expect(result?.provider).toBe('openrouter')
242
+ })
243
+
244
+ it('detects model-unavailable from "model not available" message', () => {
245
+ const error = new Error("model 'gpt-4-turbo' is not available")
246
+ const result = detectProviderError('openrouter', error)
247
+
248
+ expect(result).not.toBeNull()
249
+ expect(result?.type).toBe('model-unavailable')
250
+ expect(result?.provider).toBe('openrouter')
251
+ expect(result?.model).toBe('gpt-4-turbo')
252
+ expect(result?.message).toBe(
253
+ "Model 'gpt-4-turbo' is not available via OpenRouter",
254
+ )
255
+ })
256
+
257
+ it('detects model-unavailable from "model not found" message', () => {
258
+ const error = new Error('model not found in available models')
259
+ const result = detectProviderError('openrouter', error)
260
+
261
+ expect(result?.type).toBe('model-unavailable')
262
+ expect(result?.provider).toBe('openrouter')
263
+ })
264
+
265
+ it('detects model-unavailable from "model not supported" message', () => {
266
+ const error = new Error('this model is not supported')
267
+ const result = detectProviderError('openrouter', error)
268
+
269
+ expect(result?.type).toBe('model-unavailable')
270
+ })
271
+
272
+ it('detects model-unavailable with model name extraction from quotes', () => {
273
+ const error = new Error("model 'claude-3-opus' not available")
274
+ const result = detectProviderError('openrouter', error)
275
+
276
+ expect(result?.type).toBe('model-unavailable')
277
+ expect(result?.model).toBe('claude-3-opus')
278
+ expect(result?.message).toBe(
279
+ "Model 'claude-3-opus' is not available via OpenRouter",
280
+ )
281
+ })
282
+
283
+ it('falls back to network error detection for unrecognized OpenRouter errors', () => {
284
+ const error = new Error('ENOTFOUND openrouter.ai')
285
+ const result = detectProviderError('openrouter', error)
286
+
287
+ expect(result?.type).toBe('network-error')
288
+ expect(result?.provider).toBe('openrouter')
289
+ })
290
+ })
291
+
292
+ // ==========================================================================
293
+ // Generic Network Errors
294
+ // ==========================================================================
295
+
296
+ describe('Generic Network Errors', () => {
297
+ it('detects connection-timeout from "timeout" message', () => {
298
+ const error = new Error('Connection timeout')
299
+ const result = detectProviderError('openai', error)
300
+
301
+ expect(result).not.toBeNull()
302
+ expect(result?.type).toBe('connection-timeout')
303
+ expect(result?.provider).toBe('openai')
304
+ expect(result?.message).toBe('Connection to openai timed out')
305
+ })
306
+
307
+ it('detects connection-timeout from ETIMEDOUT', () => {
308
+ const error = new Error('connect ETIMEDOUT')
309
+ const result = detectProviderError('openai', error)
310
+
311
+ expect(result?.type).toBe('connection-timeout')
312
+ })
313
+
314
+ it('detects connection-timeout from "timed out" message', () => {
315
+ const error = new Error('request timed out')
316
+ const result = detectProviderError('openai', error)
317
+
318
+ expect(result?.type).toBe('connection-timeout')
319
+ })
320
+
321
+ it('detects network-error from ENOTFOUND', () => {
322
+ const error = new Error('getaddrinfo ENOTFOUND api.openai.com')
323
+ const result = detectProviderError('openai', error)
324
+
325
+ expect(result).not.toBeNull()
326
+ expect(result?.type).toBe('network-error')
327
+ expect(result?.provider).toBe('openai')
328
+ expect(result?.message).toBe('Network error connecting to openai')
329
+ })
330
+
331
+ it('detects network-error from "dns" message', () => {
332
+ const error = new Error('DNS resolution failed')
333
+ const result = detectProviderError('openai', error)
334
+
335
+ expect(result?.type).toBe('network-error')
336
+ })
337
+
338
+ it('detects network-error from "network" message', () => {
339
+ const error = new Error('Network unreachable')
340
+ const result = detectProviderError('openai', error)
341
+
342
+ expect(result?.type).toBe('network-error')
343
+ })
344
+
345
+ it('detects connection-refused for generic provider', () => {
346
+ const error = new Error('ECONNREFUSED 192.168.1.100:8080')
347
+ const result = detectProviderError('openai', error)
348
+
349
+ expect(result?.type).toBe('connection-refused')
350
+ expect(result?.message).toBe('Cannot connect to openai server')
351
+ })
352
+
353
+ it('returns null for unrecognized errors', () => {
354
+ const error = new Error('Some random error that does not match patterns')
355
+ const result = detectProviderError('openai', error)
356
+
357
+ expect(result).toBeNull()
358
+ })
359
+
360
+ it('returns null for non-Error types', () => {
361
+ const result = detectProviderError('openai', 'string error')
362
+ expect(result).toBeNull()
363
+
364
+ const result2 = detectProviderError('openai', {
365
+ message: 'object error',
366
+ })
367
+ expect(result2).toBeNull()
368
+
369
+ const result3 = detectProviderError('openai', null)
370
+ expect(result3).toBeNull()
371
+ })
372
+ })
373
+
374
+ // ==========================================================================
375
+ // OpenAI Provider (fallback to network errors)
376
+ // ==========================================================================
377
+
378
+ describe('OpenAI', () => {
379
+ it('falls through to network error detection', () => {
380
+ const error = new Error('Connection timeout')
381
+ const result = detectProviderError('openai', error)
382
+
383
+ expect(result?.type).toBe('connection-timeout')
384
+ expect(result?.provider).toBe('openai')
385
+ })
386
+
387
+ it('returns null for unrecognized errors', () => {
388
+ const error = new Error('Unknown OpenAI SDK error')
389
+ const result = detectProviderError('openai', error)
390
+
391
+ expect(result).toBeNull()
392
+ })
393
+ })
394
+
395
+ // ==========================================================================
396
+ // Model Name Extraction
397
+ // ==========================================================================
398
+
399
+ describe('Model Name Extraction', () => {
400
+ it('extracts model name from single-quoted pattern', () => {
401
+ const error = new Error("model 'test-model' not found")
402
+ const result = detectProviderError('ollama', error)
403
+
404
+ expect(result?.model).toBe('test-model')
405
+ })
406
+
407
+ it('extracts model name from double-quoted pattern', () => {
408
+ const error = new Error('model "another-model" does not exist')
409
+ const result = detectProviderError('ollama', error)
410
+
411
+ expect(result?.model).toBe('another-model')
412
+ })
413
+
414
+ it('extracts model name from colon pattern', () => {
415
+ const error = new Error('model: some-model not found')
416
+ const result = detectProviderError('ollama', error)
417
+
418
+ expect(result?.model).toBe('some-model')
419
+ })
420
+
421
+ it('extracts model name from space pattern', () => {
422
+ const error = new Error('model my-model does not exist')
423
+ const result = detectProviderError('ollama', error)
424
+
425
+ expect(result?.model).toBe('my-model')
426
+ })
427
+ })
428
+ })
429
+
430
+ // ============================================================================
431
+ // detectProviderFromError Tests
432
+ // ============================================================================
433
+
434
+ describe('detectProviderFromError', () => {
435
+ it('detects Ollama from port 11434 in error', () => {
436
+ const error = new Error('connect ECONNREFUSED 127.0.0.1:11434')
437
+ const result = detectProviderFromError(error)
438
+
439
+ expect(result).toBe('ollama')
440
+ })
441
+
442
+ it('detects LM Studio from port 1234 in error', () => {
443
+ const error = new Error('connect ECONNREFUSED 127.0.0.1:1234')
444
+ const result = detectProviderFromError(error)
445
+
446
+ expect(result).toBe('lm-studio')
447
+ })
448
+
449
+ it('returns undefined for errors without recognized port', () => {
450
+ const error = new Error('connect ECONNREFUSED 127.0.0.1:8080')
451
+ const result = detectProviderFromError(error)
452
+
453
+ expect(result).toBeUndefined()
454
+ })
455
+
456
+ it('returns undefined for non-Error types', () => {
457
+ expect(detectProviderFromError('string error')).toBeUndefined()
458
+ expect(detectProviderFromError({ message: 'object' })).toBeUndefined()
459
+ expect(detectProviderFromError(null)).toBeUndefined()
460
+ })
461
+ })
462
+
463
+ // ============================================================================
464
+ // getProviderSuggestions Tests
465
+ // ============================================================================
466
+
467
+ describe('getProviderSuggestions', () => {
468
+ describe('Ollama suggestions', () => {
469
+ it('returns correct suggestions for daemon-not-running', () => {
470
+ const error: ProviderError = {
471
+ type: 'daemon-not-running',
472
+ provider: 'ollama',
473
+ message: 'Ollama daemon is not running',
474
+ originalError: new Error(),
475
+ }
476
+ const suggestions = getProviderSuggestions(error)
477
+
478
+ expect(suggestions).toContain('Start the Ollama daemon: ollama serve')
479
+ expect(suggestions).toContain(
480
+ 'Install Ollama: https://ollama.com/download',
481
+ )
482
+ })
483
+
484
+ it('returns model-specific suggestions for model-not-found with model name', () => {
485
+ const error: ProviderError = {
486
+ type: 'model-not-found',
487
+ provider: 'ollama',
488
+ message: "Model 'nomic-embed-text' is not installed",
489
+ model: 'nomic-embed-text',
490
+ originalError: new Error(),
491
+ }
492
+ const suggestions = getProviderSuggestions(error)
493
+
494
+ expect(suggestions).toContain(
495
+ 'Download the model: ollama pull nomic-embed-text',
496
+ )
497
+ expect(suggestions).toContain('Recommended embedding models:')
498
+ })
499
+
500
+ it('returns generic download suggestion when model name not available', () => {
501
+ const error: ProviderError = {
502
+ type: 'model-not-found',
503
+ provider: 'ollama',
504
+ message: 'Model not installed',
505
+ originalError: new Error(),
506
+ }
507
+ const suggestions = getProviderSuggestions(error)
508
+
509
+ expect(suggestions).toContain('Download an embedding model')
510
+ })
511
+
512
+ it('includes recommended models in suggestions', () => {
513
+ const error: ProviderError = {
514
+ type: 'model-not-found',
515
+ provider: 'ollama',
516
+ message: 'Model not installed',
517
+ originalError: new Error(),
518
+ }
519
+ const suggestions = getProviderSuggestions(error)
520
+
521
+ expect(suggestions.some((s) => s.includes('nomic-embed-text'))).toBe(true)
522
+ expect(suggestions.some((s) => s.includes('mxbai-embed-large'))).toBe(
523
+ true,
524
+ )
525
+ expect(suggestions.some((s) => s.includes('bge-m3'))).toBe(true)
526
+ })
527
+
528
+ it('returns model-specific suggestions for model-loading with model name', () => {
529
+ const error: ProviderError = {
530
+ type: 'model-loading',
531
+ provider: 'ollama',
532
+ message: "Model 'nomic-embed-text' is loading",
533
+ model: 'nomic-embed-text',
534
+ originalError: new Error(),
535
+ }
536
+ const suggestions = getProviderSuggestions(error)
537
+
538
+ expect(suggestions).toContain('Wait for the model to finish loading')
539
+ expect(suggestions).toContain(
540
+ 'Or pre-load it: ollama run nomic-embed-text',
541
+ )
542
+ })
543
+
544
+ it('returns generic suggestions for model-loading without model name', () => {
545
+ const error: ProviderError = {
546
+ type: 'model-loading',
547
+ provider: 'ollama',
548
+ message: 'Model is loading',
549
+ originalError: new Error(),
550
+ }
551
+ const suggestions = getProviderSuggestions(error)
552
+
553
+ expect(suggestions).toContain('Wait for the model to finish loading')
554
+ expect(suggestions).toContain(
555
+ 'First request may be slow while model loads',
556
+ )
557
+ })
558
+
559
+ it('returns Ollama-specific suggestions for connection-refused', () => {
560
+ const error: ProviderError = {
561
+ type: 'connection-refused',
562
+ provider: 'ollama',
563
+ message: 'Cannot connect',
564
+ originalError: new Error(),
565
+ }
566
+ const suggestions = getProviderSuggestions(error)
567
+
568
+ expect(suggestions).toContain('Start the Ollama daemon: ollama serve')
569
+ })
570
+ })
571
+
572
+ describe('LM Studio suggestions', () => {
573
+ it('returns correct suggestions for gui-not-running', () => {
574
+ const error: ProviderError = {
575
+ type: 'gui-not-running',
576
+ provider: 'lm-studio',
577
+ message: 'LM Studio is not running',
578
+ originalError: new Error(),
579
+ }
580
+ const suggestions = getProviderSuggestions(error)
581
+
582
+ expect(suggestions).toContain('Open LM Studio application')
583
+ expect(suggestions).toContain(
584
+ 'Go to Developer tab and start the local server',
585
+ )
586
+ expect(suggestions).toContain('Ensure an embedding model is loaded')
587
+ expect(suggestions).toContain(
588
+ 'Note: LM Studio requires GUI - consider Ollama for automation',
589
+ )
590
+ })
591
+
592
+ it('returns correct suggestions for model-not-found', () => {
593
+ const error: ProviderError = {
594
+ type: 'model-not-found',
595
+ provider: 'lm-studio',
596
+ message: 'No model loaded',
597
+ originalError: new Error(),
598
+ }
599
+ const suggestions = getProviderSuggestions(error)
600
+
601
+ expect(suggestions).toContain('Load an embedding model in LM Studio')
602
+ expect(suggestions).toContain(
603
+ 'Go to Models tab and download an embedding model',
604
+ )
605
+ expect(suggestions).toContain('Then load it in the Home tab')
606
+ })
607
+
608
+ it('returns LM Studio-specific suggestions for connection-refused', () => {
609
+ const error: ProviderError = {
610
+ type: 'connection-refused',
611
+ provider: 'lm-studio',
612
+ message: 'Cannot connect',
613
+ originalError: new Error(),
614
+ }
615
+ const suggestions = getProviderSuggestions(error)
616
+
617
+ expect(suggestions).toContain('Open LM Studio and start the local server')
618
+ })
619
+ })
620
+
621
+ describe('OpenRouter suggestions', () => {
622
+ it('returns correct suggestions for invalid-api-key', () => {
623
+ const error: ProviderError = {
624
+ type: 'invalid-api-key',
625
+ provider: 'openrouter',
626
+ message: 'Invalid API key',
627
+ originalError: new Error(),
628
+ }
629
+ const suggestions = getProviderSuggestions(error)
630
+
631
+ expect(suggestions).toContain(
632
+ 'Get an API key: https://openrouter.ai/keys',
633
+ )
634
+ expect(suggestions).toContain(
635
+ 'Set the key: export OPENROUTER_API_KEY=sk-or-...',
636
+ )
637
+ expect(suggestions).toContain('Or set: export OPENAI_API_KEY=sk-or-...')
638
+ expect(suggestions).toContain('Note: OpenRouter keys start with sk-or-')
639
+ })
640
+
641
+ it('returns correct suggestions for rate-limited', () => {
642
+ const error: ProviderError = {
643
+ type: 'rate-limited',
644
+ provider: 'openrouter',
645
+ message: 'Rate limit exceeded',
646
+ originalError: new Error(),
647
+ }
648
+ const suggestions = getProviderSuggestions(error)
649
+
650
+ expect(suggestions).toContain('Wait a moment and try again')
651
+ expect(suggestions).toContain(
652
+ 'OpenRouter shares rate limits across all users',
653
+ )
654
+ expect(suggestions).toContain(
655
+ 'Consider using Ollama for unlimited local inference',
656
+ )
657
+ expect(suggestions).toContain(
658
+ 'Or use OpenAI directly for higher rate limits',
659
+ )
660
+ })
661
+
662
+ it('returns correct suggestions for quota-exceeded', () => {
663
+ const error: ProviderError = {
664
+ type: 'quota-exceeded',
665
+ provider: 'openrouter',
666
+ message: 'Quota exceeded',
667
+ originalError: new Error(),
668
+ }
669
+ const suggestions = getProviderSuggestions(error)
670
+
671
+ expect(suggestions).toContain(
672
+ 'Check your OpenRouter balance: https://openrouter.ai/credits',
673
+ )
674
+ expect(suggestions).toContain('Add credits to continue using OpenRouter')
675
+ expect(suggestions).toContain('Or switch to a free provider like Ollama')
676
+ })
677
+
678
+ it('returns correct suggestions for model-unavailable', () => {
679
+ const error: ProviderError = {
680
+ type: 'model-unavailable',
681
+ provider: 'openrouter',
682
+ message: 'Model not available',
683
+ originalError: new Error(),
684
+ }
685
+ const suggestions = getProviderSuggestions(error)
686
+
687
+ expect(suggestions).toContain(
688
+ 'Check available models: https://openrouter.ai/models',
689
+ )
690
+ expect(suggestions).toContain(
691
+ 'Try: text-embedding-3-small or text-embedding-3-large',
692
+ )
693
+ })
694
+
695
+ it('returns correct suggestions for model-not-found on OpenRouter', () => {
696
+ const error: ProviderError = {
697
+ type: 'model-not-found',
698
+ provider: 'openrouter',
699
+ message: 'Model not found',
700
+ originalError: new Error(),
701
+ }
702
+ const suggestions = getProviderSuggestions(error)
703
+
704
+ expect(suggestions).toContain(
705
+ 'Check available models: https://openrouter.ai/models',
706
+ )
707
+ expect(suggestions).toContain(
708
+ 'Common embedding models: text-embedding-3-small, text-embedding-3-large',
709
+ )
710
+ })
711
+ })
712
+
713
+ describe('Generic error suggestions', () => {
714
+ it('returns correct suggestions for connection-timeout', () => {
715
+ const error: ProviderError = {
716
+ type: 'connection-timeout',
717
+ provider: 'openai',
718
+ message: 'Connection timed out',
719
+ originalError: new Error(),
720
+ }
721
+ const suggestions = getProviderSuggestions(error)
722
+
723
+ expect(suggestions).toContain('Check your network connection')
724
+ expect(suggestions).toContain(
725
+ 'The server may be overloaded, try again later',
726
+ )
727
+ expect(suggestions).toContain('Consider increasing timeout in config')
728
+ })
729
+
730
+ it('returns correct suggestions for network-error', () => {
731
+ const error: ProviderError = {
732
+ type: 'network-error',
733
+ provider: 'openai',
734
+ message: 'Network error',
735
+ originalError: new Error(),
736
+ }
737
+ const suggestions = getProviderSuggestions(error)
738
+
739
+ expect(suggestions).toContain('Check your internet connection')
740
+ expect(suggestions).toContain('Check if the server is reachable')
741
+ expect(suggestions).toContain('Try again later')
742
+ })
743
+
744
+ it('returns generic suggestions for connection-refused on unknown provider', () => {
745
+ const error: ProviderError = {
746
+ type: 'connection-refused',
747
+ provider: 'openai',
748
+ message: 'Cannot connect',
749
+ originalError: new Error(),
750
+ }
751
+ const suggestions = getProviderSuggestions(error)
752
+
753
+ expect(suggestions).toContain('Check that the server is running')
754
+ })
755
+
756
+ it('returns generic suggestions for unknown error type', () => {
757
+ const error: ProviderError = {
758
+ type: 'unknown',
759
+ provider: 'openai',
760
+ message: 'Unknown error',
761
+ originalError: new Error(),
762
+ }
763
+ const suggestions = getProviderSuggestions(error)
764
+
765
+ expect(suggestions).toContain('Check the error details above')
766
+ })
767
+
768
+ it('returns generic suggestion for model-not-found on unknown provider', () => {
769
+ const error: ProviderError = {
770
+ type: 'model-not-found',
771
+ provider: 'openai',
772
+ message: 'Model not found',
773
+ originalError: new Error(),
774
+ }
775
+ const suggestions = getProviderSuggestions(error)
776
+
777
+ expect(suggestions).toContain('Check that the model name is correct')
778
+ })
779
+ })
780
+ })
781
+
782
+ // ============================================================================
783
+ // getProviderErrorTitle Tests
784
+ // ============================================================================
785
+
786
+ describe('getProviderErrorTitle', () => {
787
+ it('returns correct title for daemon-not-running', () => {
788
+ const error: ProviderError = {
789
+ type: 'daemon-not-running',
790
+ provider: 'ollama',
791
+ message: 'Test',
792
+ originalError: new Error(),
793
+ }
794
+ expect(getProviderErrorTitle(error)).toBe('Ollama is not running')
795
+ })
796
+
797
+ it('returns correct title for gui-not-running', () => {
798
+ const error: ProviderError = {
799
+ type: 'gui-not-running',
800
+ provider: 'lm-studio',
801
+ message: 'Test',
802
+ originalError: new Error(),
803
+ }
804
+ expect(getProviderErrorTitle(error)).toBe('LM Studio is not running')
805
+ })
806
+
807
+ it('returns correct title for model-not-found with model name', () => {
808
+ const error: ProviderError = {
809
+ type: 'model-not-found',
810
+ provider: 'ollama',
811
+ message: 'Test',
812
+ model: 'nomic-embed-text',
813
+ originalError: new Error(),
814
+ }
815
+ expect(getProviderErrorTitle(error)).toBe(
816
+ "Model 'nomic-embed-text' not found",
817
+ )
818
+ })
819
+
820
+ it('returns correct title for model-not-found without model name', () => {
821
+ const error: ProviderError = {
822
+ type: 'model-not-found',
823
+ provider: 'ollama',
824
+ message: 'Test',
825
+ originalError: new Error(),
826
+ }
827
+ expect(getProviderErrorTitle(error)).toBe('Model not found')
828
+ })
829
+
830
+ it('returns correct title for model-loading with model name', () => {
831
+ const error: ProviderError = {
832
+ type: 'model-loading',
833
+ provider: 'ollama',
834
+ message: 'Test',
835
+ model: 'bge-m3',
836
+ originalError: new Error(),
837
+ }
838
+ expect(getProviderErrorTitle(error)).toBe("Model 'bge-m3' is still loading")
839
+ })
840
+
841
+ it('returns correct title for model-loading without model name', () => {
842
+ const error: ProviderError = {
843
+ type: 'model-loading',
844
+ provider: 'ollama',
845
+ message: 'Test',
846
+ originalError: new Error(),
847
+ }
848
+ expect(getProviderErrorTitle(error)).toBe('Model is still loading')
849
+ })
850
+
851
+ it('returns correct title for invalid-api-key', () => {
852
+ const error: ProviderError = {
853
+ type: 'invalid-api-key',
854
+ provider: 'openrouter',
855
+ message: 'Test',
856
+ originalError: new Error(),
857
+ }
858
+ expect(getProviderErrorTitle(error)).toBe('Invalid API key')
859
+ })
860
+
861
+ it('returns correct title for rate-limited', () => {
862
+ const error: ProviderError = {
863
+ type: 'rate-limited',
864
+ provider: 'openrouter',
865
+ message: 'Test',
866
+ originalError: new Error(),
867
+ }
868
+ expect(getProviderErrorTitle(error)).toBe('Rate limit exceeded')
869
+ })
870
+
871
+ it('returns correct title for quota-exceeded', () => {
872
+ const error: ProviderError = {
873
+ type: 'quota-exceeded',
874
+ provider: 'openrouter',
875
+ message: 'Test',
876
+ originalError: new Error(),
877
+ }
878
+ expect(getProviderErrorTitle(error)).toBe('Quota exceeded')
879
+ })
880
+
881
+ it('returns correct title for model-unavailable', () => {
882
+ const error: ProviderError = {
883
+ type: 'model-unavailable',
884
+ provider: 'openrouter',
885
+ message: 'Test',
886
+ originalError: new Error(),
887
+ }
888
+ expect(getProviderErrorTitle(error)).toBe('Model not available')
889
+ })
890
+
891
+ it('returns correct title for connection-refused with provider', () => {
892
+ const error: ProviderError = {
893
+ type: 'connection-refused',
894
+ provider: 'ollama',
895
+ message: 'Test',
896
+ originalError: new Error(),
897
+ }
898
+ expect(getProviderErrorTitle(error)).toBe('Cannot connect to ollama')
899
+ })
900
+
901
+ it('returns correct title for connection-timeout with provider', () => {
902
+ const error: ProviderError = {
903
+ type: 'connection-timeout',
904
+ provider: 'openai',
905
+ message: 'Test',
906
+ originalError: new Error(),
907
+ }
908
+ expect(getProviderErrorTitle(error)).toBe('Connection to openai timed out')
909
+ })
910
+
911
+ it('returns correct title for network-error', () => {
912
+ const error: ProviderError = {
913
+ type: 'network-error',
914
+ provider: 'openai',
915
+ message: 'Test',
916
+ originalError: new Error(),
917
+ }
918
+ expect(getProviderErrorTitle(error)).toBe('Network error')
919
+ })
920
+
921
+ it('returns correct title for unknown error type', () => {
922
+ const error: ProviderError = {
923
+ type: 'unknown',
924
+ provider: 'openai',
925
+ message: 'Test',
926
+ originalError: new Error(),
927
+ }
928
+ expect(getProviderErrorTitle(error)).toBe('Embedding error')
929
+ })
930
+ })
931
+
932
+ // ============================================================================
933
+ // RECOMMENDED_OLLAMA_MODELS Tests
934
+ // ============================================================================
935
+
936
+ describe('RECOMMENDED_OLLAMA_MODELS', () => {
937
+ it('contains the expected recommended models', () => {
938
+ expect(RECOMMENDED_OLLAMA_MODELS).toHaveLength(3)
939
+
940
+ const names = RECOMMENDED_OLLAMA_MODELS.map((m) => m.name)
941
+ expect(names).toContain('nomic-embed-text')
942
+ expect(names).toContain('mxbai-embed-large')
943
+ expect(names).toContain('bge-m3')
944
+ })
945
+
946
+ it('has correct dimensions for nomic-embed-text', () => {
947
+ const model = RECOMMENDED_OLLAMA_MODELS.find(
948
+ (m) => m.name === 'nomic-embed-text',
949
+ )
950
+ expect(model?.dims).toBe(768)
951
+ expect(model?.note).toBe('recommended, fast')
952
+ })
953
+
954
+ it('has correct dimensions for mxbai-embed-large', () => {
955
+ const model = RECOMMENDED_OLLAMA_MODELS.find(
956
+ (m) => m.name === 'mxbai-embed-large',
957
+ )
958
+ expect(model?.dims).toBe(1024)
959
+ expect(model?.note).toBe('higher quality')
960
+ })
961
+
962
+ it('has correct dimensions for bge-m3', () => {
963
+ const model = RECOMMENDED_OLLAMA_MODELS.find((m) => m.name === 'bge-m3')
964
+ expect(model?.dims).toBe(1024)
965
+ expect(model?.note).toBe('multilingual')
966
+ })
967
+ })