@dizzlkheinz/ynab-mcpb 0.12.1

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 (435) hide show
  1. package/.chunkhound.json +11 -0
  2. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
  3. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
  4. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
  5. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
  6. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
  7. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
  8. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
  9. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
  10. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
  11. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
  12. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
  13. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
  14. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
  15. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
  16. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
  17. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
  18. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
  19. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
  20. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
  21. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
  22. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
  23. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
  24. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
  25. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
  26. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
  27. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
  28. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
  29. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
  30. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
  31. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
  32. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
  33. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
  34. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
  35. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
  36. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
  37. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
  38. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
  39. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
  40. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
  41. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
  42. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
  43. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
  44. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
  45. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
  46. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
  47. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
  48. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
  49. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
  50. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
  51. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
  52. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
  53. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
  54. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
  55. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
  56. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
  57. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
  58. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
  59. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
  60. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
  61. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
  62. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
  63. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
  64. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
  65. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
  66. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
  67. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
  68. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
  69. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
  70. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
  71. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
  72. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
  73. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
  74. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
  75. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
  76. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
  77. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
  78. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
  79. package/.dxtignore +57 -0
  80. package/.env.example +44 -0
  81. package/.gemini/settings.json +8 -0
  82. package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  83. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  84. package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  85. package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
  86. package/.github/pull_request_template.md +41 -0
  87. package/.github/workflows/ci-tests.yml +41 -0
  88. package/.github/workflows/claude-code-review.yml +57 -0
  89. package/.github/workflows/claude.yml +50 -0
  90. package/.github/workflows/full-integration.yml +22 -0
  91. package/.github/workflows/pr-description-check.yml +88 -0
  92. package/.github/workflows/publish.yml +33 -0
  93. package/.github/workflows/release.yml +89 -0
  94. package/.mcpbignore +58 -0
  95. package/.prettierignore +10 -0
  96. package/.prettierrc.json +10 -0
  97. package/ADOS-2-Module-1-Complete-Manual.md +757 -0
  98. package/AGENTS.md +36 -0
  99. package/CHANGELOG.md +187 -0
  100. package/CLAUDE.md +414 -0
  101. package/CODEREVIEW_RESPONSE.md +128 -0
  102. package/LICENSE +17 -0
  103. package/NUL +1 -0
  104. package/README.md +222 -0
  105. package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
  106. package/TESTING_NOTES.md +217 -0
  107. package/WARP.md +245 -0
  108. package/accountactivity-merged.csv +149 -0
  109. package/bin/ynab-mcp-server.cjs +4 -0
  110. package/bin/ynab-mcp-server.js +8 -0
  111. package/bundle-analysis.html +13110 -0
  112. package/dist/bundle/index.cjs +124 -0
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.js +85 -0
  115. package/dist/server/YNABMCPServer.d.ts +264 -0
  116. package/dist/server/YNABMCPServer.js +845 -0
  117. package/dist/server/budgetResolver.d.ts +15 -0
  118. package/dist/server/budgetResolver.js +99 -0
  119. package/dist/server/cacheManager.d.ts +74 -0
  120. package/dist/server/cacheManager.js +306 -0
  121. package/dist/server/config.d.ts +3 -0
  122. package/dist/server/config.js +19 -0
  123. package/dist/server/deltaCache.d.ts +61 -0
  124. package/dist/server/deltaCache.js +206 -0
  125. package/dist/server/deltaCache.merge.d.ts +9 -0
  126. package/dist/server/deltaCache.merge.js +111 -0
  127. package/dist/server/diagnostics.d.ts +90 -0
  128. package/dist/server/diagnostics.js +163 -0
  129. package/dist/server/errorHandler.d.ts +69 -0
  130. package/dist/server/errorHandler.js +524 -0
  131. package/dist/server/prompts.d.ts +31 -0
  132. package/dist/server/prompts.js +205 -0
  133. package/dist/server/rateLimiter.d.ts +27 -0
  134. package/dist/server/rateLimiter.js +82 -0
  135. package/dist/server/requestLogger.d.ts +62 -0
  136. package/dist/server/requestLogger.js +190 -0
  137. package/dist/server/resources.d.ts +39 -0
  138. package/dist/server/resources.js +85 -0
  139. package/dist/server/responseFormatter.d.ts +14 -0
  140. package/dist/server/responseFormatter.js +42 -0
  141. package/dist/server/securityMiddleware.d.ts +87 -0
  142. package/dist/server/securityMiddleware.js +117 -0
  143. package/dist/server/serverKnowledgeStore.d.ts +11 -0
  144. package/dist/server/serverKnowledgeStore.js +42 -0
  145. package/dist/server/toolRegistry.d.ts +85 -0
  146. package/dist/server/toolRegistry.js +272 -0
  147. package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
  148. package/dist/tools/__tests__/deltaTestUtils.js +26 -0
  149. package/dist/tools/accountTools.d.ts +37 -0
  150. package/dist/tools/accountTools.js +175 -0
  151. package/dist/tools/budgetTools.d.ts +10 -0
  152. package/dist/tools/budgetTools.js +68 -0
  153. package/dist/tools/categoryTools.d.ts +27 -0
  154. package/dist/tools/categoryTools.js +232 -0
  155. package/dist/tools/compareTransactions/formatter.d.ts +71 -0
  156. package/dist/tools/compareTransactions/formatter.js +97 -0
  157. package/dist/tools/compareTransactions/index.d.ts +30 -0
  158. package/dist/tools/compareTransactions/index.js +160 -0
  159. package/dist/tools/compareTransactions/matcher.d.ts +12 -0
  160. package/dist/tools/compareTransactions/matcher.js +140 -0
  161. package/dist/tools/compareTransactions/parser.d.ts +14 -0
  162. package/dist/tools/compareTransactions/parser.js +430 -0
  163. package/dist/tools/compareTransactions/types.d.ts +27 -0
  164. package/dist/tools/compareTransactions/types.js +1 -0
  165. package/dist/tools/compareTransactions.d.ts +1 -0
  166. package/dist/tools/compareTransactions.js +1 -0
  167. package/dist/tools/deltaFetcher.d.ts +22 -0
  168. package/dist/tools/deltaFetcher.js +137 -0
  169. package/dist/tools/deltaSupport.d.ts +20 -0
  170. package/dist/tools/deltaSupport.js +176 -0
  171. package/dist/tools/exportTransactions.d.ts +17 -0
  172. package/dist/tools/exportTransactions.js +191 -0
  173. package/dist/tools/monthTools.d.ts +16 -0
  174. package/dist/tools/monthTools.js +107 -0
  175. package/dist/tools/payeeTools.d.ts +17 -0
  176. package/dist/tools/payeeTools.js +82 -0
  177. package/dist/tools/reconcileAdapter.d.ts +25 -0
  178. package/dist/tools/reconcileAdapter.js +167 -0
  179. package/dist/tools/reconciliation/analyzer.d.ts +3 -0
  180. package/dist/tools/reconciliation/analyzer.js +567 -0
  181. package/dist/tools/reconciliation/executor.d.ts +94 -0
  182. package/dist/tools/reconciliation/executor.js +611 -0
  183. package/dist/tools/reconciliation/index.d.ts +54 -0
  184. package/dist/tools/reconciliation/index.js +249 -0
  185. package/dist/tools/reconciliation/matcher.d.ts +3 -0
  186. package/dist/tools/reconciliation/matcher.js +160 -0
  187. package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
  188. package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
  189. package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
  190. package/dist/tools/reconciliation/recommendationEngine.js +273 -0
  191. package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
  192. package/dist/tools/reconciliation/reportFormatter.js +214 -0
  193. package/dist/tools/reconciliation/types.d.ts +172 -0
  194. package/dist/tools/reconciliation/types.js +7 -0
  195. package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
  196. package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
  197. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
  198. package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
  199. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
  200. package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
  201. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
  202. package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
  203. package/dist/tools/schemas/outputs/index.d.ts +14 -0
  204. package/dist/tools/schemas/outputs/index.js +14 -0
  205. package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
  206. package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
  207. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
  208. package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
  209. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
  210. package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
  211. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
  212. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
  213. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
  214. package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
  215. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
  216. package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
  217. package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
  218. package/dist/tools/schemas/shared/commonOutputs.js +27 -0
  219. package/dist/tools/toolCategories.d.ts +32 -0
  220. package/dist/tools/toolCategories.js +32 -0
  221. package/dist/tools/transactionTools.d.ts +315 -0
  222. package/dist/tools/transactionTools.js +1722 -0
  223. package/dist/tools/utilityTools.d.ts +10 -0
  224. package/dist/tools/utilityTools.js +56 -0
  225. package/dist/types/index.d.ts +20 -0
  226. package/dist/types/index.js +16 -0
  227. package/dist/types/toolAnnotations.d.ts +7 -0
  228. package/dist/types/toolAnnotations.js +1 -0
  229. package/dist/utils/amountUtils.d.ts +3 -0
  230. package/dist/utils/amountUtils.js +10 -0
  231. package/dist/utils/dateUtils.d.ts +9 -0
  232. package/dist/utils/dateUtils.js +43 -0
  233. package/dist/utils/money.d.ts +21 -0
  234. package/dist/utils/money.js +51 -0
  235. package/docs/README.md +72 -0
  236. package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
  237. package/docs/assets/schemas/reconciliation-v2.json +338 -0
  238. package/docs/getting-started/CONFIGURATION.md +175 -0
  239. package/docs/getting-started/INSTALLATION.md +333 -0
  240. package/docs/getting-started/QUICKSTART.md +282 -0
  241. package/docs/guides/ARCHITECTURE.md +650 -0
  242. package/docs/guides/DEPLOYMENT.md +189 -0
  243. package/docs/guides/INTEGRATION_TESTING.md +730 -0
  244. package/docs/guides/TESTING.md +591 -0
  245. package/docs/reconciliation-flow.md +83 -0
  246. package/docs/reference/API.md +1450 -0
  247. package/docs/reference/EXAMPLES.md +946 -0
  248. package/docs/reference/TOOLS.md +348 -0
  249. package/docs/reference/TROUBLESHOOTING.md +481 -0
  250. package/esbuild.config.mjs +68 -0
  251. package/eslint.config.js +49 -0
  252. package/fix-types.sh +17 -0
  253. package/meta.json +12550 -0
  254. package/package.json +105 -0
  255. package/package.json.tmp +105 -0
  256. package/scripts/analyze-bundle.mjs +41 -0
  257. package/scripts/create-pr-description.js +203 -0
  258. package/scripts/generate-mcpb.ps1 +96 -0
  259. package/scripts/run-domain-integration-tests.js +33 -0
  260. package/scripts/run-generate-mcpb.js +29 -0
  261. package/scripts/run-throttled-integration-tests.js +116 -0
  262. package/scripts/test-delta-params.mjs +140 -0
  263. package/scripts/test-recommendations.ts +53 -0
  264. package/scripts/tmpTransaction.ts +48 -0
  265. package/scripts/validate-env.js +122 -0
  266. package/scripts/verify-build.js +105 -0
  267. package/scripts/watch-and-restart.ps1 +50 -0
  268. package/src/__tests__/comprehensive.integration.test.ts +1196 -0
  269. package/src/__tests__/delta.performance.test.ts +80 -0
  270. package/src/__tests__/performance.test.ts +725 -0
  271. package/src/__tests__/setup.ts +449 -0
  272. package/src/__tests__/testRunner.ts +444 -0
  273. package/src/__tests__/testUtils.ts +563 -0
  274. package/src/__tests__/workflows.e2e.test.ts +1675 -0
  275. package/src/index.ts +124 -0
  276. package/src/server/.gitkeep +1 -0
  277. package/src/server/YNABMCPServer.ts +1188 -0
  278. package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
  279. package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
  280. package/src/server/__tests__/budgetResolver.test.ts +425 -0
  281. package/src/server/__tests__/cacheManager.test.ts +880 -0
  282. package/src/server/__tests__/config.test.ts +166 -0
  283. package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
  284. package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
  285. package/src/server/__tests__/deltaCache.test.ts +774 -0
  286. package/src/server/__tests__/diagnostics.test.ts +823 -0
  287. package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
  288. package/src/server/__tests__/errorHandler.test.ts +416 -0
  289. package/src/server/__tests__/prompts.test.ts +354 -0
  290. package/src/server/__tests__/rateLimiter.test.ts +314 -0
  291. package/src/server/__tests__/requestLogger.test.ts +408 -0
  292. package/src/server/__tests__/resources.test.ts +299 -0
  293. package/src/server/__tests__/security.integration.test.ts +426 -0
  294. package/src/server/__tests__/securityMiddleware.test.ts +449 -0
  295. package/src/server/__tests__/server-startup.integration.test.ts +477 -0
  296. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
  297. package/src/server/__tests__/toolRegistry.test.ts +855 -0
  298. package/src/server/budgetResolver.ts +235 -0
  299. package/src/server/cacheManager.ts +503 -0
  300. package/src/server/config.ts +41 -0
  301. package/src/server/deltaCache.merge.ts +149 -0
  302. package/src/server/deltaCache.ts +341 -0
  303. package/src/server/diagnostics.ts +338 -0
  304. package/src/server/errorHandler.ts +756 -0
  305. package/src/server/prompts.ts +291 -0
  306. package/src/server/rateLimiter.ts +156 -0
  307. package/src/server/requestLogger.ts +344 -0
  308. package/src/server/resources.ts +168 -0
  309. package/src/server/responseFormatter.ts +51 -0
  310. package/src/server/securityMiddleware.ts +236 -0
  311. package/src/server/serverKnowledgeStore.ts +91 -0
  312. package/src/server/toolRegistry.ts +489 -0
  313. package/src/tools/.gitkeep +1 -0
  314. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
  315. package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
  316. package/src/tools/__tests__/accountTools.test.ts +653 -0
  317. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
  318. package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
  319. package/src/tools/__tests__/budgetTools.test.ts +423 -0
  320. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
  321. package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
  322. package/src/tools/__tests__/categoryTools.test.ts +622 -0
  323. package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
  324. package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
  325. package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
  326. package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
  327. package/src/tools/__tests__/compareTransactions.test.ts +342 -0
  328. package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
  329. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
  330. package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
  331. package/src/tools/__tests__/deltaSupport.test.ts +188 -0
  332. package/src/tools/__tests__/deltaTestUtils.ts +46 -0
  333. package/src/tools/__tests__/exportTransactions.test.ts +213 -0
  334. package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
  335. package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
  336. package/src/tools/__tests__/monthTools.test.ts +523 -0
  337. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
  338. package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
  339. package/src/tools/__tests__/payeeTools.test.ts +445 -0
  340. package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
  341. package/src/tools/__tests__/transactionTools.test.ts +3521 -0
  342. package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
  343. package/src/tools/__tests__/utilityTools.test.ts +205 -0
  344. package/src/tools/accountTools.ts +283 -0
  345. package/src/tools/budgetTools.ts +112 -0
  346. package/src/tools/categoryTools.ts +366 -0
  347. package/src/tools/compareTransactions/formatter.ts +163 -0
  348. package/src/tools/compareTransactions/index.ts +228 -0
  349. package/src/tools/compareTransactions/matcher.ts +240 -0
  350. package/src/tools/compareTransactions/parser.ts +557 -0
  351. package/src/tools/compareTransactions/types.ts +60 -0
  352. package/src/tools/compareTransactions.ts +3 -0
  353. package/src/tools/deltaFetcher.ts +278 -0
  354. package/src/tools/deltaSupport.ts +293 -0
  355. package/src/tools/exportTransactions.ts +273 -0
  356. package/src/tools/monthTools.ts +164 -0
  357. package/src/tools/payeeTools.ts +140 -0
  358. package/src/tools/reconcileAdapter.ts +312 -0
  359. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
  360. package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
  361. package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
  362. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
  363. package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
  364. package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
  365. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
  366. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
  367. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
  368. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
  369. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
  370. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
  371. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
  372. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
  373. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
  374. package/src/tools/reconciliation/analyzer.ts +824 -0
  375. package/src/tools/reconciliation/executor.ts +880 -0
  376. package/src/tools/reconciliation/index.ts +400 -0
  377. package/src/tools/reconciliation/matcher.ts +269 -0
  378. package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
  379. package/src/tools/reconciliation/recommendationEngine.ts +506 -0
  380. package/src/tools/reconciliation/reportFormatter.ts +363 -0
  381. package/src/tools/reconciliation/types.ts +314 -0
  382. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
  383. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
  384. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
  385. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
  386. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
  387. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
  388. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
  389. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
  390. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
  391. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
  392. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
  393. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
  394. package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
  395. package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
  396. package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
  397. package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
  398. package/src/tools/schemas/outputs/index.ts +270 -0
  399. package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
  400. package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
  401. package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
  402. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
  403. package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
  404. package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
  405. package/src/tools/schemas/shared/commonOutputs.ts +140 -0
  406. package/src/tools/toolCategories.ts +140 -0
  407. package/src/tools/transactionTools.ts +2509 -0
  408. package/src/tools/utilityTools.ts +90 -0
  409. package/src/types/.gitkeep +1 -0
  410. package/src/types/__tests__/index.test.ts +52 -0
  411. package/src/types/index.ts +67 -0
  412. package/src/types/integration-tests.d.ts +35 -0
  413. package/src/types/toolAnnotations.ts +44 -0
  414. package/src/utils/__tests__/dateUtils.test.ts +170 -0
  415. package/src/utils/__tests__/money.test.ts +189 -0
  416. package/src/utils/amountUtils.ts +32 -0
  417. package/src/utils/dateUtils.ts +108 -0
  418. package/src/utils/money.ts +123 -0
  419. package/test-csv-sample.csv +28 -0
  420. package/test-exports/sample_bank_statement.csv +7 -0
  421. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
  422. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
  423. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
  424. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
  425. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
  426. package/test-reconcile-autodetect.js +40 -0
  427. package/test-reconcile-tool.js +152 -0
  428. package/test-reconcile-with-csv.cjs +89 -0
  429. package/test-statement.csv +8 -0
  430. package/test_debug.js +47 -0
  431. package/test_simple.mjs +16 -0
  432. package/tsconfig.json +31 -0
  433. package/tsconfig.prod.json +18 -0
  434. package/vitest-reporters/split-json-reporter.ts +211 -0
  435. package/vitest.config.ts +96 -0
@@ -0,0 +1,1196 @@
1
+ /**
2
+ * Comprehensive integration tests for YNAB MCP Server
3
+ * These tests use mocked YNAB API responses to test complete workflows
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, beforeAll, afterAll, vi } from 'vitest';
7
+ import { YNABMCPServer } from '../server/YNABMCPServer.js';
8
+ import { executeToolCall, parseToolResult, validateToolResult, waitFor } from './testUtils.js';
9
+ import { cacheManager } from '../server/cacheManager.js';
10
+
11
+ // Mock the YNAB SDK
12
+ vi.mock('ynab', () => {
13
+ const mockAPI = {
14
+ budgets: {
15
+ getBudgets: vi.fn(),
16
+ getBudgetById: vi.fn(),
17
+ },
18
+ accounts: {
19
+ getAccounts: vi.fn(),
20
+ getAccountById: vi.fn(),
21
+ createAccount: vi.fn(),
22
+ },
23
+ transactions: {
24
+ getTransactions: vi.fn(),
25
+ getTransactionsByAccount: vi.fn(),
26
+ getTransactionsByCategory: vi.fn(),
27
+ getTransactionById: vi.fn(),
28
+ createTransaction: vi.fn(),
29
+ updateTransaction: vi.fn(),
30
+ deleteTransaction: vi.fn(),
31
+ },
32
+ categories: {
33
+ getCategories: vi.fn(),
34
+ getCategoryById: vi.fn(),
35
+ updateMonthCategory: vi.fn(),
36
+ },
37
+ payees: {
38
+ getPayees: vi.fn(),
39
+ getPayeeById: vi.fn(),
40
+ },
41
+ months: {
42
+ getBudgetMonth: vi.fn(),
43
+ getBudgetMonths: vi.fn(),
44
+ },
45
+ user: {
46
+ getUser: vi.fn(),
47
+ },
48
+ };
49
+
50
+ return {
51
+ API: vi.fn(() => mockAPI),
52
+ utils: {
53
+ convertMilliUnitsToCurrencyAmount: vi.fn(
54
+ (milliunits: number, currencyDecimalDigits: number = 2) => {
55
+ const amount = milliunits / 1000;
56
+ return Number(amount.toFixed(currencyDecimalDigits));
57
+ },
58
+ ),
59
+ convertCurrencyAmountToMilliUnits: vi.fn((amount: number) => Math.round(amount * 1000)),
60
+ },
61
+ };
62
+ });
63
+
64
+ const TEST_BUDGET_UUID = '00000000-0000-0000-0000-000000000001';
65
+
66
+ describe('YNAB utils mock', () => {
67
+ it(
68
+ 'converts milliunits using SDK rounding rules',
69
+ { meta: { tier: 'domain', domain: 'workflows' } },
70
+ async () => {
71
+ const { utils } = await import('ynab');
72
+
73
+ expect(utils.convertMilliUnitsToCurrencyAmount(123456, 2)).toBe(123.46);
74
+ expect(utils.convertMilliUnitsToCurrencyAmount(123456, 3)).toBe(123.456);
75
+ expect(utils.convertMilliUnitsToCurrencyAmount(-98765, 2)).toBe(-98.77);
76
+ },
77
+ );
78
+ });
79
+
80
+ describe('YNAB MCP Server - Comprehensive Integration Tests', () => {
81
+ let server: YNABMCPServer;
82
+ let mockYnabAPI: any;
83
+
84
+ beforeEach(async () => {
85
+ // Set up environment
86
+ process.env['YNAB_ACCESS_TOKEN'] = 'test-token';
87
+
88
+ // Create server instance
89
+ server = new YNABMCPServer();
90
+
91
+ // Get the mocked YNAB API instance
92
+ const { API } = await import('ynab');
93
+ mockYnabAPI = new (API as any)();
94
+
95
+ // Reset all mocks
96
+ vi.clearAllMocks();
97
+ });
98
+
99
+ describe('Complete Budget Management Integration', () => {
100
+ it(
101
+ 'should handle complete budget listing and retrieval workflow',
102
+ { meta: { tier: 'domain', domain: 'workflows' } },
103
+ async () => {
104
+ // Mock budget list response
105
+ const mockBudgets = {
106
+ data: {
107
+ budgets: [
108
+ {
109
+ id: 'budget-1',
110
+ name: 'Test Budget 1',
111
+ last_modified_on: '2024-01-01T00:00:00Z',
112
+ first_month: '2024-01-01',
113
+ last_month: '2024-12-01',
114
+ date_format: { format: 'MM/DD/YYYY' },
115
+ currency_format: {
116
+ iso_code: 'USD',
117
+ example_format: '$123.45',
118
+ decimal_digits: 2,
119
+ decimal_separator: '.',
120
+ symbol_first: true,
121
+ group_separator: ',',
122
+ currency_symbol: '$',
123
+ display_symbol: true,
124
+ },
125
+ },
126
+ {
127
+ id: 'budget-2',
128
+ name: 'Test Budget 2',
129
+ last_modified_on: '2024-01-02T00:00:00Z',
130
+ first_month: '2024-01-01',
131
+ last_month: '2024-12-01',
132
+ date_format: { format: 'MM/DD/YYYY' },
133
+ currency_format: {
134
+ iso_code: 'USD',
135
+ example_format: '$123.45',
136
+ decimal_digits: 2,
137
+ decimal_separator: '.',
138
+ symbol_first: true,
139
+ group_separator: ',',
140
+ currency_symbol: '$',
141
+ display_symbol: true,
142
+ },
143
+ },
144
+ ],
145
+ },
146
+ };
147
+
148
+ mockYnabAPI.budgets.getBudgets.mockResolvedValue(mockBudgets);
149
+
150
+ // Test budget listing
151
+ const listResult = await executeToolCall(server, 'ynab:list_budgets');
152
+ validateToolResult(listResult);
153
+
154
+ const budgets = parseToolResult(listResult);
155
+ expect(budgets.data.budgets).toHaveLength(2);
156
+ expect(budgets.data.budgets[0].name).toBe('Test Budget 1');
157
+ expect(budgets.data.budgets[1].name).toBe('Test Budget 2');
158
+
159
+ // Mock specific budget response
160
+ const mockBudget = {
161
+ data: {
162
+ budget: {
163
+ id: 'budget-1',
164
+ name: 'Test Budget 1',
165
+ last_modified_on: '2024-01-01T00:00:00Z',
166
+ first_month: '2024-01-01',
167
+ last_month: '2024-12-01',
168
+ date_format: { format: 'MM/DD/YYYY' },
169
+ currency_format: {
170
+ iso_code: 'USD',
171
+ example_format: '$123.45',
172
+ decimal_digits: 2,
173
+ decimal_separator: '.',
174
+ symbol_first: true,
175
+ group_separator: ',',
176
+ currency_symbol: '$',
177
+ display_symbol: true,
178
+ },
179
+ accounts: [],
180
+ payees: [],
181
+ category_groups: [],
182
+ months: [],
183
+ },
184
+ },
185
+ };
186
+
187
+ mockYnabAPI.budgets.getBudgetById.mockResolvedValue(mockBudget);
188
+
189
+ // Test specific budget retrieval
190
+ const getResult = await executeToolCall(server, 'ynab:get_budget', {
191
+ budget_id: 'budget-1',
192
+ });
193
+ validateToolResult(getResult);
194
+
195
+ const budget = parseToolResult(getResult);
196
+ expect(budget.data.budget.id).toBe('budget-1');
197
+ expect(budget.data.budget.name).toBe('Test Budget 1');
198
+
199
+ // Verify API calls
200
+ expect(mockYnabAPI.budgets.getBudgets).toHaveBeenCalledTimes(1);
201
+ expect(mockYnabAPI.budgets.getBudgetById).toHaveBeenCalledWith('budget-1');
202
+ },
203
+ );
204
+
205
+ it(
206
+ 'should handle budget retrieval errors gracefully',
207
+ { meta: { tier: 'domain', domain: 'workflows' } },
208
+ async () => {
209
+ // Mock API error
210
+ const apiError = new Error('Budget not found');
211
+ (apiError as any).error = {
212
+ id: '404.2',
213
+ name: 'not_found',
214
+ description: 'Budget not found',
215
+ };
216
+ mockYnabAPI.budgets.getBudgetById.mockRejectedValue(apiError);
217
+
218
+ // Test error handling
219
+ try {
220
+ await executeToolCall(server, 'ynab:get_budget', {
221
+ budget_id: 'invalid-budget',
222
+ });
223
+ expect.fail('Should have thrown an error');
224
+ } catch (error) {
225
+ expect(error).toBeDefined();
226
+ }
227
+
228
+ expect(mockYnabAPI.budgets.getBudgetById).toHaveBeenCalledWith('invalid-budget');
229
+ },
230
+ );
231
+ });
232
+
233
+ describe('Complete Account Management Integration', () => {
234
+ it(
235
+ 'should handle complete account workflow',
236
+ { meta: { tier: 'domain', domain: 'workflows' } },
237
+ async () => {
238
+ const budgetId = TEST_BUDGET_UUID;
239
+
240
+ // Mock accounts list
241
+ const mockAccounts = {
242
+ data: {
243
+ accounts: [
244
+ {
245
+ id: 'account-1',
246
+ name: 'Checking Account',
247
+ type: 'checking',
248
+ on_budget: true,
249
+ closed: false,
250
+ balance: 100000, // $100.00
251
+ cleared_balance: 95000,
252
+ uncleared_balance: 5000,
253
+ transfer_payee_id: 'payee-transfer-1',
254
+ },
255
+ {
256
+ id: 'account-2',
257
+ name: 'Savings Account',
258
+ type: 'savings',
259
+ on_budget: true,
260
+ closed: false,
261
+ note: 'Emergency fund',
262
+ balance: 500000, // $500.00
263
+ cleared_balance: 500000,
264
+ uncleared_balance: 0,
265
+ transfer_payee_id: 'payee-transfer-2',
266
+ },
267
+ ],
268
+ },
269
+ };
270
+
271
+ mockYnabAPI.accounts.getAccounts.mockResolvedValue(mockAccounts);
272
+
273
+ // Test account listing
274
+ const listResult = await executeToolCall(server, 'ynab:list_accounts', {
275
+ budget_id: budgetId,
276
+ });
277
+ validateToolResult(listResult);
278
+
279
+ const accounts = parseToolResult(listResult);
280
+ expect(accounts.data.accounts).toHaveLength(2);
281
+ expect(accounts.data.accounts[0].name).toBe('Checking Account');
282
+ expect(accounts.data.accounts[1].name).toBe('Savings Account');
283
+
284
+ // Mock specific account response
285
+ const mockAccount = {
286
+ data: {
287
+ account: mockAccounts.data.accounts[0],
288
+ },
289
+ };
290
+
291
+ mockYnabAPI.accounts.getAccountById.mockResolvedValue(mockAccount);
292
+
293
+ // Test specific account retrieval
294
+ const getResult = await executeToolCall(server, 'ynab:get_account', {
295
+ budget_id: budgetId,
296
+ account_id: 'account-1',
297
+ });
298
+ validateToolResult(getResult);
299
+
300
+ const account = parseToolResult(getResult);
301
+ expect(account.data.account.id).toBe('account-1');
302
+ expect(account.data.account.name).toBe('Checking Account');
303
+ expect(account.data.account.balance).toBe(100);
304
+
305
+ // Mock account creation
306
+ const newAccount = {
307
+ id: 'account-3',
308
+ name: 'New Test Account',
309
+ type: 'checking',
310
+ on_budget: true,
311
+ closed: false,
312
+ balance: 0,
313
+ cleared_balance: 0,
314
+ uncleared_balance: 0,
315
+ transfer_payee_id: 'payee-transfer-3',
316
+ };
317
+
318
+ const mockCreateResponse = {
319
+ data: {
320
+ account: newAccount,
321
+ },
322
+ };
323
+
324
+ mockYnabAPI.accounts.createAccount.mockResolvedValue(mockCreateResponse);
325
+
326
+ // Test account creation
327
+ const createResult = await executeToolCall(server, 'ynab:create_account', {
328
+ budget_id: budgetId,
329
+ name: 'New Test Account',
330
+ type: 'checking',
331
+ balance: 0,
332
+ });
333
+ validateToolResult(createResult);
334
+
335
+ const createdAccount = parseToolResult(createResult);
336
+ expect(createdAccount.data.account.name).toBe('New Test Account');
337
+ expect(createdAccount.data.account.type).toBe('checking');
338
+
339
+ // Verify API calls
340
+ expect(mockYnabAPI.accounts.getAccounts).toHaveBeenCalledWith(budgetId);
341
+ expect(mockYnabAPI.accounts.getAccountById).toHaveBeenCalledWith(budgetId, 'account-1');
342
+ expect(mockYnabAPI.accounts.createAccount).toHaveBeenCalledWith(budgetId, {
343
+ account: {
344
+ name: 'New Test Account',
345
+ type: 'checking',
346
+ balance: 0,
347
+ },
348
+ });
349
+ },
350
+ );
351
+ });
352
+
353
+ describe('Complete Transaction Management Integration', () => {
354
+ // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
355
+ it.skip(
356
+ 'should handle complete transaction workflow',
357
+ { meta: { tier: 'domain', domain: 'workflows' } },
358
+ async () => {
359
+ const budgetId = TEST_BUDGET_UUID;
360
+ const accountId = 'test-account';
361
+
362
+ // Mock transactions list
363
+ const mockTransactions = {
364
+ data: {
365
+ transactions: [
366
+ {
367
+ id: 'transaction-1',
368
+ date: '2024-01-15',
369
+ amount: -5000, // $5.00 outflow
370
+ memo: 'Coffee shop',
371
+ cleared: 'cleared',
372
+ approved: true,
373
+ flag_color: null,
374
+ account_id: accountId,
375
+ payee_id: 'payee-1',
376
+ category_id: 'category-1',
377
+ transfer_account_id: null,
378
+ },
379
+ {
380
+ id: 'transaction-2',
381
+ date: '2024-01-16',
382
+ amount: 100000, // $100.00 inflow
383
+ memo: 'Salary',
384
+ cleared: 'cleared',
385
+ approved: true,
386
+ flag_color: null,
387
+ account_id: accountId,
388
+ payee_id: 'payee-2',
389
+ category_id: null,
390
+ transfer_account_id: null,
391
+ },
392
+ ],
393
+ },
394
+ };
395
+
396
+ mockYnabAPI.transactions.getTransactionsByAccount.mockResolvedValue(mockTransactions);
397
+
398
+ // Test transaction listing
399
+ const listResult = await executeToolCall(server, 'ynab:list_transactions', {
400
+ budget_id: budgetId,
401
+ account_id: accountId,
402
+ });
403
+ validateToolResult(listResult);
404
+
405
+ const transactions = parseToolResult(listResult);
406
+ expect(transactions.data.transactions).toHaveLength(2);
407
+ expect(transactions.data.transactions[0].memo).toBe('Coffee shop');
408
+ expect(transactions.data.transactions[1].memo).toBe('Salary');
409
+
410
+ // Mock specific transaction response
411
+ const mockTransaction = {
412
+ data: {
413
+ transaction: mockTransactions.data.transactions[0],
414
+ },
415
+ };
416
+
417
+ mockYnabAPI.transactions.getTransactionById.mockResolvedValue(mockTransaction);
418
+
419
+ // Test specific transaction retrieval
420
+ const getResult = await executeToolCall(server, 'ynab:get_transaction', {
421
+ budget_id: budgetId,
422
+ transaction_id: 'transaction-1',
423
+ });
424
+ validateToolResult(getResult);
425
+
426
+ const transaction = parseToolResult(getResult);
427
+ expect(transaction.data.transaction.id).toBe('transaction-1');
428
+ expect(transaction.data.transaction.memo).toBe('Coffee shop');
429
+ expect(transaction.data.transaction.amount).toBe(-5);
430
+
431
+ // Mock transaction creation
432
+ const newTransaction = {
433
+ id: 'transaction-3',
434
+ date: '2024-01-17',
435
+ amount: -2500,
436
+ memo: 'Test transaction',
437
+ cleared: 'uncleared',
438
+ approved: true,
439
+ flag_color: null,
440
+ account_id: accountId,
441
+ payee_id: null,
442
+ category_id: 'category-1',
443
+ transfer_account_id: null,
444
+ };
445
+
446
+ const mockCreateResponse = {
447
+ data: {
448
+ transaction: newTransaction,
449
+ },
450
+ };
451
+
452
+ mockYnabAPI.transactions.createTransaction.mockResolvedValue(mockCreateResponse);
453
+
454
+ // Test transaction creation
455
+ const createResult = await executeToolCall(server, 'ynab:create_transaction', {
456
+ budget_id: budgetId,
457
+ account_id: accountId,
458
+ category_id: 'category-1',
459
+ payee_name: 'Test Payee',
460
+ amount: -2500,
461
+ memo: 'Test transaction',
462
+ date: '2024-01-17',
463
+ cleared: 'uncleared',
464
+ });
465
+ validateToolResult(createResult);
466
+
467
+ const createdTransaction = parseToolResult(createResult);
468
+ expect(createdTransaction.data.transaction.memo).toBe('Test transaction');
469
+ expect(createdTransaction.data.transaction.amount).toBe(-2.5);
470
+
471
+ // Mock transaction update
472
+ const updatedTransaction = { ...newTransaction, memo: 'Updated memo' };
473
+ const mockUpdateResponse = {
474
+ data: {
475
+ transaction: updatedTransaction,
476
+ },
477
+ };
478
+
479
+ mockYnabAPI.transactions.updateTransaction.mockResolvedValue(mockUpdateResponse);
480
+
481
+ // Test transaction update
482
+ const updateResult = await executeToolCall(server, 'ynab:update_transaction', {
483
+ budget_id: budgetId,
484
+ transaction_id: 'transaction-3',
485
+ memo: 'Updated memo',
486
+ });
487
+ validateToolResult(updateResult);
488
+
489
+ const updated = parseToolResult(updateResult);
490
+ expect(updated.data.transaction.memo).toBe('Updated memo');
491
+
492
+ // Mock transaction deletion
493
+ mockYnabAPI.transactions.deleteTransaction.mockResolvedValue({
494
+ data: {
495
+ transaction: { ...updatedTransaction, deleted: true },
496
+ },
497
+ });
498
+
499
+ // Test transaction deletion
500
+ const deleteResult = await executeToolCall(server, 'ynab:delete_transaction', {
501
+ budget_id: budgetId,
502
+ transaction_id: 'transaction-3',
503
+ });
504
+ validateToolResult(deleteResult);
505
+
506
+ // Verify API calls
507
+ expect(mockYnabAPI.transactions.getTransactionsByAccount).toHaveBeenCalledWith(
508
+ budgetId,
509
+ accountId,
510
+ undefined,
511
+ );
512
+ expect(mockYnabAPI.transactions.getTransactionById).toHaveBeenCalledWith(
513
+ budgetId,
514
+ 'transaction-1',
515
+ );
516
+ expect(mockYnabAPI.transactions.createTransaction).toHaveBeenCalled();
517
+ expect(mockYnabAPI.transactions.updateTransaction).toHaveBeenCalled();
518
+ expect(mockYnabAPI.transactions.deleteTransaction).toHaveBeenCalledWith(
519
+ budgetId,
520
+ 'transaction-3',
521
+ );
522
+ },
523
+ );
524
+
525
+ it(
526
+ 'should handle transaction filtering',
527
+ { meta: { tier: 'domain', domain: 'workflows' } },
528
+ async () => {
529
+ const budgetId = TEST_BUDGET_UUID;
530
+
531
+ // Mock filtered transactions
532
+ mockYnabAPI.transactions.getTransactions.mockResolvedValue({
533
+ data: {
534
+ transactions: [
535
+ {
536
+ id: 'filtered-transaction',
537
+ date: '2024-01-15',
538
+ amount: -1000,
539
+ memo: 'Filtered transaction',
540
+ cleared: 'cleared',
541
+ approved: true,
542
+ account_id: 'account-1',
543
+ category_id: 'category-1',
544
+ },
545
+ ],
546
+ },
547
+ });
548
+
549
+ // Test filtering by date
550
+ const dateFilterResult = await executeToolCall(server, 'ynab:list_transactions', {
551
+ budget_id: budgetId,
552
+ since_date: '2024-01-01',
553
+ });
554
+ validateToolResult(dateFilterResult);
555
+
556
+ // Also mock account/category specific endpoints
557
+ mockYnabAPI.transactions.getTransactionsByAccount.mockResolvedValue({
558
+ data: {
559
+ transactions: [
560
+ {
561
+ id: 'filtered-transaction',
562
+ date: '2024-01-15',
563
+ amount: -1000,
564
+ memo: 'Filtered transaction',
565
+ cleared: 'cleared',
566
+ approved: true,
567
+ account_id: 'account-1',
568
+ category_id: 'category-1',
569
+ },
570
+ ],
571
+ },
572
+ });
573
+ mockYnabAPI.transactions.getTransactionsByCategory.mockResolvedValue({
574
+ data: {
575
+ transactions: [
576
+ {
577
+ id: 'filtered-transaction',
578
+ date: '2024-01-15',
579
+ amount: -1000,
580
+ memo: 'Filtered transaction',
581
+ cleared: 'cleared',
582
+ approved: true,
583
+ account_id: 'account-1',
584
+ category_id: 'category-1',
585
+ },
586
+ ],
587
+ },
588
+ });
589
+
590
+ // Test filtering by account
591
+ const accountFilterResult = await executeToolCall(server, 'ynab:list_transactions', {
592
+ budget_id: budgetId,
593
+ account_id: 'account-1',
594
+ });
595
+ validateToolResult(accountFilterResult);
596
+
597
+ // Test filtering by category
598
+ const categoryFilterResult = await executeToolCall(server, 'ynab:list_transactions', {
599
+ budget_id: budgetId,
600
+ category_id: 'category-1',
601
+ });
602
+ validateToolResult(categoryFilterResult);
603
+
604
+ // Verify API calls with different parameters
605
+ expect(mockYnabAPI.transactions.getTransactions).toHaveBeenCalledTimes(1);
606
+ expect(mockYnabAPI.transactions.getTransactionsByAccount).toHaveBeenCalledTimes(1);
607
+ expect(mockYnabAPI.transactions.getTransactionsByCategory).toHaveBeenCalledTimes(1);
608
+ },
609
+ );
610
+ });
611
+
612
+ describe('Complete Category Management Integration', () => {
613
+ it(
614
+ 'should handle complete category workflow',
615
+ { meta: { tier: 'domain', domain: 'workflows' } },
616
+ async () => {
617
+ const budgetId = TEST_BUDGET_UUID;
618
+
619
+ // Mock categories response
620
+ const mockCategories = {
621
+ data: {
622
+ category_groups: [
623
+ {
624
+ id: 'group-1',
625
+ name: 'Immediate Obligations',
626
+ hidden: false,
627
+ deleted: false,
628
+ categories: [
629
+ {
630
+ id: 'category-1',
631
+ category_group_id: 'group-1',
632
+ name: 'Rent/Mortgage',
633
+ hidden: false,
634
+ budgeted: 150000, // $150.00
635
+ activity: -150000,
636
+ balance: 0,
637
+ // goal_type omitted (undefined, not null)
638
+ deleted: false,
639
+ },
640
+ {
641
+ id: 'category-2',
642
+ category_group_id: 'group-1',
643
+ name: 'Utilities',
644
+ hidden: false,
645
+ budgeted: 10000, // $10.00
646
+ activity: -8500,
647
+ balance: 1500,
648
+ // goal_type omitted (undefined, not null)
649
+ deleted: false,
650
+ },
651
+ ],
652
+ },
653
+ ],
654
+ },
655
+ };
656
+
657
+ mockYnabAPI.categories.getCategories.mockResolvedValue(mockCategories);
658
+
659
+ // Test category listing
660
+ const listResult = await executeToolCall(server, 'ynab:list_categories', {
661
+ budget_id: budgetId,
662
+ });
663
+ validateToolResult(listResult);
664
+
665
+ const categories = parseToolResult(listResult);
666
+ expect(categories.data.category_groups).toHaveLength(1);
667
+ expect(categories.data.categories).toHaveLength(2);
668
+ expect(categories.data.categories[0].name).toBe('Rent/Mortgage');
669
+
670
+ // Mock specific category response
671
+ const mockCategory = {
672
+ data: {
673
+ category: mockCategories.data.category_groups[0].categories[0],
674
+ },
675
+ };
676
+
677
+ mockYnabAPI.categories.getCategoryById.mockResolvedValue(mockCategory);
678
+
679
+ // Test specific category retrieval
680
+ const getResult = await executeToolCall(server, 'ynab:get_category', {
681
+ budget_id: budgetId,
682
+ category_id: 'category-1',
683
+ });
684
+ validateToolResult(getResult);
685
+
686
+ const category = parseToolResult(getResult);
687
+ expect(category.data.category.id).toBe('category-1');
688
+ expect(category.data.category.name).toBe('Rent/Mortgage');
689
+ expect(category.data.category.budgeted).toBe(150);
690
+
691
+ // Mock category update
692
+ const updatedCategory = {
693
+ ...mockCategories.data.category_groups[0].categories[0],
694
+ budgeted: 160000, // $160.00
695
+ };
696
+
697
+ const mockUpdateResponse = {
698
+ data: {
699
+ category: updatedCategory,
700
+ },
701
+ };
702
+
703
+ mockYnabAPI.categories.updateMonthCategory.mockResolvedValue(mockUpdateResponse);
704
+
705
+ // Test category budget update
706
+ const updateResult = await executeToolCall(server, 'ynab:update_category', {
707
+ budget_id: budgetId,
708
+ category_id: 'category-1',
709
+ budgeted: 160000,
710
+ });
711
+ validateToolResult(updateResult);
712
+
713
+ const updated = parseToolResult(updateResult);
714
+ expect(updated.data.category.budgeted).toBe(160);
715
+
716
+ // Verify API calls
717
+ expect(mockYnabAPI.categories.getCategories).toHaveBeenCalledWith(budgetId);
718
+ expect(mockYnabAPI.categories.getCategoryById).toHaveBeenCalledWith(budgetId, 'category-1');
719
+ expect(mockYnabAPI.categories.updateMonthCategory).toHaveBeenCalled();
720
+ },
721
+ );
722
+ });
723
+
724
+ describe('Complete Utility Tools Integration', () => {
725
+ it(
726
+ 'should handle user information retrieval',
727
+ { meta: { tier: 'domain', domain: 'workflows' } },
728
+ async () => {
729
+ // Mock user response
730
+ const mockUser = {
731
+ data: {
732
+ user: {
733
+ id: 'user-123',
734
+ email: 'test@example.com',
735
+ },
736
+ },
737
+ };
738
+
739
+ mockYnabAPI.user.getUser.mockResolvedValue(mockUser);
740
+
741
+ // Test user retrieval
742
+ const userResult = await executeToolCall(server, 'ynab:get_user');
743
+ validateToolResult(userResult);
744
+
745
+ const user = parseToolResult(userResult);
746
+ expect(user.data.user.id).toBe('user-123');
747
+
748
+ expect(mockYnabAPI.user.getUser).toHaveBeenCalledTimes(1);
749
+ },
750
+ );
751
+
752
+ it(
753
+ 'should handle amount conversion',
754
+ { meta: { tier: 'domain', domain: 'workflows' } },
755
+ async () => {
756
+ // Test dollar to milliunits conversion
757
+ const toMilliunitsResult = await executeToolCall(server, 'ynab:convert_amount', {
758
+ amount: 25.75,
759
+ to_milliunits: true,
760
+ });
761
+ validateToolResult(toMilliunitsResult);
762
+
763
+ const toMilli = parseToolResult(toMilliunitsResult);
764
+ expect(toMilli.data.conversion.converted_amount).toBe(25750);
765
+ expect(toMilli.data.conversion.description).toBe('$25.75 = 25750 milliunits');
766
+
767
+ // Test milliunits to dollar conversion
768
+ const toDollarsResult = await executeToolCall(server, 'ynab:convert_amount', {
769
+ amount: 25750,
770
+ to_milliunits: false,
771
+ });
772
+ validateToolResult(toDollarsResult);
773
+
774
+ const dollars = parseToolResult(toDollarsResult);
775
+ expect(dollars.data.conversion.converted_amount).toBe(25.75);
776
+ expect(dollars.data.conversion.description).toBe('25750 milliunits = $25.75');
777
+ },
778
+ );
779
+ });
780
+
781
+ describe('Error Handling Integration', () => {
782
+ it(
783
+ 'should handle various API error scenarios',
784
+ { meta: { tier: 'domain', domain: 'workflows' } },
785
+ async () => {
786
+ // Test 401 Unauthorized
787
+ const authError = new Error('Unauthorized');
788
+ (authError as any).error = { id: '401', name: 'unauthorized', description: 'Unauthorized' };
789
+ mockYnabAPI.budgets.getBudgets.mockRejectedValue(authError);
790
+
791
+ try {
792
+ await executeToolCall(server, 'ynab:list_budgets');
793
+ expect.fail('Should have thrown an error');
794
+ } catch (error) {
795
+ expect(error).toBeDefined();
796
+ }
797
+
798
+ // Test 404 Not Found
799
+ const notFoundError = new Error('Not Found');
800
+ (notFoundError as any).error = {
801
+ id: '404.2',
802
+ name: 'not_found',
803
+ description: 'Budget not found',
804
+ };
805
+ mockYnabAPI.budgets.getBudgetById.mockRejectedValue(notFoundError);
806
+
807
+ try {
808
+ await executeToolCall(server, 'ynab:get_budget', { budget_id: 'invalid' });
809
+ expect.fail('Should have thrown an error');
810
+ } catch (error) {
811
+ expect(error).toBeDefined();
812
+ }
813
+
814
+ // Test 429 Rate Limit
815
+ const rateLimitError = new Error('Too Many Requests');
816
+ (rateLimitError as any).error = {
817
+ id: '429',
818
+ name: 'rate_limit',
819
+ description: 'Rate limit exceeded',
820
+ };
821
+ mockYnabAPI.accounts.getAccounts.mockRejectedValue(rateLimitError);
822
+
823
+ try {
824
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: 'test' });
825
+ expect.fail('Should have thrown an error');
826
+ } catch (error) {
827
+ expect(error).toBeDefined();
828
+ }
829
+ },
830
+ );
831
+
832
+ it(
833
+ 'should validate input parameters',
834
+ { meta: { tier: 'domain', domain: 'workflows' } },
835
+ async () => {
836
+ // Test missing required parameters
837
+ try {
838
+ await executeToolCall(server, 'ynab:get_budget', {});
839
+ expect.fail('Should have thrown validation error');
840
+ } catch (error) {
841
+ expect(error).toBeDefined();
842
+ }
843
+
844
+ // Test invalid parameter types
845
+ try {
846
+ await executeToolCall(server, 'ynab:create_transaction', {
847
+ budget_id: 'test',
848
+ account_id: 'test',
849
+ amount: 'invalid-amount', // Should be number
850
+ date: '2024-01-01',
851
+ });
852
+ expect.fail('Should have thrown validation error');
853
+ } catch (error) {
854
+ expect(error).toBeDefined();
855
+ }
856
+ },
857
+ );
858
+ });
859
+
860
+ describe('Caching Integration Tests', () => {
861
+ let previousNodeEnv: string | undefined;
862
+
863
+ beforeAll(() => {
864
+ previousNodeEnv = process.env['NODE_ENV'];
865
+ process.env['NODE_ENV'] = 'development';
866
+ });
867
+
868
+ beforeEach(() => {
869
+ cacheManager.clear();
870
+ process.env['NODE_ENV'] = 'development';
871
+ });
872
+
873
+ afterAll(() => {
874
+ // Restore NODE_ENV after all caching tests complete
875
+ if (previousNodeEnv === undefined) {
876
+ delete process.env['NODE_ENV'];
877
+ } else {
878
+ process.env['NODE_ENV'] = previousNodeEnv;
879
+ }
880
+ });
881
+
882
+ // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
883
+ it.skip(
884
+ 'should cache budget list requests and improve performance on subsequent calls',
885
+ { meta: { tier: 'domain', domain: 'workflows' } },
886
+ async () => {
887
+ const mockBudgets = {
888
+ data: {
889
+ budgets: [
890
+ {
891
+ id: 'budget-1',
892
+ name: 'Test Budget',
893
+ last_modified_on: '2024-01-01T00:00:00Z',
894
+ first_month: '2024-01-01',
895
+ last_month: '2024-12-01',
896
+ date_format: { format: 'MM/DD/YYYY' },
897
+ currency_format: {
898
+ iso_code: 'USD',
899
+ example_format: '$123.45',
900
+ decimal_digits: 2,
901
+ decimal_separator: '.',
902
+ symbol_first: true,
903
+ group_separator: ',',
904
+ currency_symbol: '$',
905
+ display_symbol: true,
906
+ },
907
+ },
908
+ ],
909
+ },
910
+ };
911
+
912
+ mockYnabAPI.budgets.getBudgets.mockResolvedValue(mockBudgets);
913
+
914
+ const statsBeforeFirstCall = cacheManager.getStats();
915
+ const initialSize = statsBeforeFirstCall.size;
916
+
917
+ // First call - should hit API and cache result
918
+ const firstResult = await executeToolCall(server, 'ynab:list_budgets');
919
+ validateToolResult(firstResult);
920
+
921
+ const firstParsed = parseToolResult(firstResult);
922
+ expect(firstParsed.data.cached).toBe(false);
923
+ expect(firstParsed.data.cache_info).toBe('Fresh data retrieved from YNAB API');
924
+
925
+ // Cache should have grown
926
+ const statsAfterFirstCall = cacheManager.getStats();
927
+ expect(statsAfterFirstCall.size).toBeGreaterThan(initialSize);
928
+
929
+ // Second call - should hit cache
930
+ const secondResult = await executeToolCall(server, 'ynab:list_budgets');
931
+ validateToolResult(secondResult);
932
+
933
+ const secondParsed = parseToolResult(secondResult);
934
+ expect(secondParsed.data.cached).toBe(true);
935
+ expect(secondParsed.data.cache_info).toBe(
936
+ 'Data retrieved from cache for improved performance',
937
+ );
938
+
939
+ // API should only have been called once
940
+ expect(mockYnabAPI.budgets.getBudgets).toHaveBeenCalledTimes(1);
941
+
942
+ // Cache hit count should have increased
943
+ const finalStats = cacheManager.getStats();
944
+ expect(finalStats.hits).toBeGreaterThan(statsBeforeFirstCall.hits);
945
+ },
946
+ );
947
+
948
+ it(
949
+ 'should invalidate cache on write operations',
950
+ { meta: { tier: 'domain', domain: 'workflows' } },
951
+ async () => {
952
+ const budgetId = TEST_BUDGET_UUID;
953
+
954
+ // Mock responses
955
+ const mockAccounts = {
956
+ data: {
957
+ accounts: [
958
+ {
959
+ id: 'account-1',
960
+ name: 'Test Account',
961
+ type: 'checking',
962
+ on_budget: true,
963
+ closed: false,
964
+ balance: 100000,
965
+ cleared_balance: 95000,
966
+ uncleared_balance: 5000,
967
+ },
968
+ ],
969
+ },
970
+ };
971
+
972
+ const mockCreatedAccount = {
973
+ data: {
974
+ account: {
975
+ id: 'account-2',
976
+ name: 'New Account',
977
+ type: 'savings',
978
+ on_budget: true,
979
+ closed: false,
980
+ balance: 0,
981
+ cleared_balance: 0,
982
+ uncleared_balance: 0,
983
+ },
984
+ },
985
+ };
986
+
987
+ mockYnabAPI.accounts.getAccounts.mockResolvedValue(mockAccounts);
988
+ mockYnabAPI.accounts.createAccount.mockResolvedValue(mockCreatedAccount);
989
+
990
+ // First, populate cache with account list
991
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: budgetId });
992
+
993
+ // Verify cache has entries
994
+ const statsAfterRead = cacheManager.getStats();
995
+ expect(statsAfterRead.size).toBeGreaterThan(0);
996
+
997
+ // Create a new account (write operation)
998
+ await executeToolCall(server, 'ynab:create_account', {
999
+ budget_id: budgetId,
1000
+ name: 'New Account',
1001
+ type: 'savings',
1002
+ });
1003
+
1004
+ // Next call to list accounts should hit API again (cache was invalidated)
1005
+ mockYnabAPI.accounts.getAccounts.mockClear();
1006
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: budgetId });
1007
+
1008
+ // Verify API was called again after cache invalidation
1009
+ expect(mockYnabAPI.accounts.getAccounts).toHaveBeenCalledTimes(1);
1010
+ },
1011
+ );
1012
+
1013
+ // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
1014
+ it.skip(
1015
+ 'should not cache filtered transaction requests',
1016
+ { meta: { tier: 'domain', domain: 'workflows' } },
1017
+ async () => {
1018
+ const budgetId = TEST_BUDGET_UUID;
1019
+
1020
+ const mockTransactions = {
1021
+ data: {
1022
+ transactions: [
1023
+ {
1024
+ id: 'transaction-1',
1025
+ date: '2024-01-15',
1026
+ amount: -5000,
1027
+ memo: 'Test transaction',
1028
+ cleared: 'cleared',
1029
+ approved: true,
1030
+ account_id: 'account-1',
1031
+ category_id: 'category-1',
1032
+ },
1033
+ ],
1034
+ },
1035
+ };
1036
+
1037
+ mockYnabAPI.transactions.getTransactions.mockResolvedValue(mockTransactions);
1038
+ mockYnabAPI.transactions.getTransactionsByAccount.mockResolvedValue(mockTransactions);
1039
+
1040
+ const statsBeforeUnfiltered = cacheManager.getStats();
1041
+
1042
+ // Unfiltered request - should be cached
1043
+ const unfilteredResult = await executeToolCall(server, 'ynab:list_transactions', {
1044
+ budget_id: budgetId,
1045
+ });
1046
+ const unfilteredParsed = parseToolResult(unfilteredResult);
1047
+ expect(unfilteredParsed.data.cached).toBe(false); // First call, cache miss
1048
+
1049
+ const statsAfterUnfiltered = cacheManager.getStats();
1050
+ expect(statsAfterUnfiltered.size).toBeGreaterThan(statsBeforeUnfiltered.size);
1051
+
1052
+ // Filtered request - should NOT be cached
1053
+ const filteredResult = await executeToolCall(server, 'ynab:list_transactions', {
1054
+ budget_id: budgetId,
1055
+ account_id: 'account-1',
1056
+ });
1057
+ const filteredParsed = parseToolResult(filteredResult);
1058
+ expect(filteredParsed.data.cached).toBe(false);
1059
+ expect(filteredParsed.data.cache_info).toBe('Fresh data retrieved from YNAB API');
1060
+
1061
+ // Cache size should not have increased for filtered request
1062
+ const statsAfterFiltered = cacheManager.getStats();
1063
+ expect(statsAfterFiltered.size).toBe(statsAfterUnfiltered.size);
1064
+
1065
+ // Both API methods should have been called
1066
+ expect(mockYnabAPI.transactions.getTransactions).toHaveBeenCalledTimes(1);
1067
+ expect(mockYnabAPI.transactions.getTransactionsByAccount).toHaveBeenCalledTimes(1);
1068
+ },
1069
+ );
1070
+
1071
+ it(
1072
+ 'should handle cache warming after setting default budget',
1073
+ { meta: { tier: 'domain', domain: 'workflows' } },
1074
+ async () => {
1075
+ const budgetId = TEST_BUDGET_UUID;
1076
+
1077
+ // Mock all the responses for cache warming
1078
+ mockYnabAPI.accounts.getAccounts.mockResolvedValue({
1079
+ data: { accounts: [] },
1080
+ });
1081
+ mockYnabAPI.budgets.getBudgetById.mockResolvedValue({
1082
+ data: { budget: { id: budgetId, name: 'Warm Cache Budget' } },
1083
+ });
1084
+ mockYnabAPI.categories.getCategories.mockResolvedValue({
1085
+ data: { category_groups: [] },
1086
+ });
1087
+ mockYnabAPI.payees.getPayees.mockResolvedValue({
1088
+ data: { payees: [] },
1089
+ });
1090
+
1091
+ const statsBeforeSet = cacheManager.getStats();
1092
+ const initialSize = statsBeforeSet.size;
1093
+
1094
+ // Set default budget (should trigger cache warming)
1095
+ await executeToolCall(server, 'ynab:set_default_budget', {
1096
+ budget_id: budgetId,
1097
+ });
1098
+
1099
+ // Wait for cache warming to populate entries (fire-and-forget process)
1100
+ let statsAfterSet = cacheManager.getStats();
1101
+ await waitFor(
1102
+ () => {
1103
+ statsAfterSet = cacheManager.getStats();
1104
+ return statsAfterSet.size > initialSize;
1105
+ },
1106
+ 1000,
1107
+ 50,
1108
+ );
1109
+
1110
+ // Cache should have more entries due to warming
1111
+ expect(statsAfterSet.size).toBeGreaterThan(initialSize);
1112
+
1113
+ // Verify that cache warming API calls were made
1114
+ expect(mockYnabAPI.accounts.getAccounts).toHaveBeenCalled();
1115
+ expect(mockYnabAPI.categories.getCategories).toHaveBeenCalled();
1116
+ expect(mockYnabAPI.payees.getPayees).toHaveBeenCalled();
1117
+ },
1118
+ );
1119
+
1120
+ it(
1121
+ 'should handle cache clear operation',
1122
+ { meta: { tier: 'domain', domain: 'workflows' } },
1123
+ async () => {
1124
+ // Populate cache with some data
1125
+ mockYnabAPI.budgets.getBudgets.mockResolvedValue({
1126
+ data: { budgets: [] },
1127
+ });
1128
+
1129
+ await executeToolCall(server, 'ynab:list_budgets');
1130
+
1131
+ // Verify cache has entries
1132
+ const statsAfterPopulation = cacheManager.getStats();
1133
+ expect(statsAfterPopulation.size).toBeGreaterThan(0);
1134
+
1135
+ // Clear cache
1136
+ await executeToolCall(server, 'ynab:clear_cache');
1137
+
1138
+ // Verify cache is empty
1139
+ const statsAfterClear = cacheManager.getStats();
1140
+ expect(statsAfterClear.size).toBe(0);
1141
+ expect(statsAfterClear.hits).toBe(0);
1142
+ expect(statsAfterClear.misses).toBe(0);
1143
+ },
1144
+ );
1145
+
1146
+ // TODO: Re-enable after DeltaFetcher cache integration alignment (see docs/plans/2025-11-15-cache-test-alignment.md)
1147
+ it.skip(
1148
+ 'should respect cache TTL and return fresh data after expiration',
1149
+ { meta: { tier: 'domain', domain: 'workflows' } },
1150
+ async () => {
1151
+ // Note: This test is conceptual since TTL testing requires time manipulation
1152
+ // In a real scenario, we would mock the Date.now() or use a test clock
1153
+
1154
+ const mockBudgets = {
1155
+ data: {
1156
+ budgets: [
1157
+ {
1158
+ id: 'budget-1',
1159
+ name: 'Test Budget',
1160
+ last_modified_on: '2024-01-01T00:00:00Z',
1161
+ first_month: '2024-01-01',
1162
+ last_month: '2024-12-01',
1163
+ date_format: { format: 'MM/DD/YYYY' },
1164
+ currency_format: {
1165
+ iso_code: 'USD',
1166
+ example_format: '$123.45',
1167
+ decimal_digits: 2,
1168
+ decimal_separator: '.',
1169
+ symbol_first: true,
1170
+ group_separator: ',',
1171
+ currency_symbol: '$',
1172
+ display_symbol: true,
1173
+ },
1174
+ },
1175
+ ],
1176
+ },
1177
+ };
1178
+
1179
+ mockYnabAPI.budgets.getBudgets.mockResolvedValue(mockBudgets);
1180
+
1181
+ // First call - cache miss
1182
+ const firstResult = await executeToolCall(server, 'ynab:list_budgets');
1183
+ const firstParsed = parseToolResult(firstResult);
1184
+ expect(firstParsed.data.cached).toBe(false);
1185
+
1186
+ // Second call - cache hit
1187
+ const secondResult = await executeToolCall(server, 'ynab:list_budgets');
1188
+ const secondParsed = parseToolResult(secondResult);
1189
+ expect(secondParsed.data.cached).toBe(true);
1190
+
1191
+ // Verify API was only called once (second call used cache)
1192
+ expect(mockYnabAPI.budgets.getBudgets).toHaveBeenCalledTimes(1);
1193
+ },
1194
+ );
1195
+ });
1196
+ });