@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,1675 @@
1
+ /**
2
+ * End-to-end workflow tests for YNAB MCP Server
3
+ * These tests require a real YNAB API key and test budget
4
+ */
5
+
6
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
7
+ import { YNABMCPServer } from '../server/YNABMCPServer.js';
8
+ import { getCurrentMonth } from '../utils/dateUtils.js';
9
+ import {
10
+ getTestConfig,
11
+ createTestServer,
12
+ executeToolCall,
13
+ parseToolResult,
14
+ isErrorResult,
15
+ getErrorMessage,
16
+ TestData,
17
+ TestDataCleanup,
18
+ YNABAssertions,
19
+ validateOutputSchema,
20
+ } from './testUtils.js';
21
+ import { testEnv } from './setup.js';
22
+
23
+ const runE2ETests = process.env['SKIP_E2E_TESTS'] !== 'true';
24
+ const describeE2E = runE2ETests ? describe : describe.skip;
25
+
26
+ describeE2E('YNAB MCP Server - End-to-End Workflows', () => {
27
+ let server: YNABMCPServer;
28
+ let testConfig: ReturnType<typeof getTestConfig>;
29
+ let cleanup: TestDataCleanup;
30
+ let testBudgetId: string;
31
+ let testAccountId: string;
32
+
33
+ beforeAll(async () => {
34
+ testConfig = getTestConfig();
35
+
36
+ if (testConfig.skipE2ETests) {
37
+ console.warn('Skipping E2E tests - no real API key or SKIP_E2E_TESTS=true');
38
+ return;
39
+ }
40
+
41
+ server = await createTestServer();
42
+ cleanup = new TestDataCleanup();
43
+
44
+ // Get the first budget for testing
45
+ const budgetsResult = await executeToolCall(server, 'ynab:list_budgets');
46
+ const budgets = parseToolResult(budgetsResult);
47
+ const budgetList = budgets.data?.budgets ?? [];
48
+
49
+ if (!budgetList.length && !testConfig.testBudgetId) {
50
+ throw new Error('No budgets found for testing. Please create a test budget in YNAB.');
51
+ }
52
+
53
+ testBudgetId = testConfig.testBudgetId ?? budgetList[0]?.id;
54
+
55
+ // Get the first account for testing
56
+ const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
57
+ budget_id: testBudgetId,
58
+ });
59
+ const accounts = parseToolResult(accountsResult);
60
+ const accountList = accounts.data?.accounts ?? [];
61
+
62
+ if (!accountList.length) {
63
+ if (testConfig.testAccountId) {
64
+ testAccountId = testConfig.testAccountId;
65
+ } else {
66
+ throw new Error('No accounts found for testing. Please create a test account in YNAB.');
67
+ }
68
+ } else {
69
+ testAccountId = testConfig.testAccountId ?? accountList[0].id;
70
+ }
71
+ });
72
+
73
+ afterAll(async () => {
74
+ if (testConfig.skipE2ETests) return;
75
+
76
+ if (cleanup && server && testBudgetId) {
77
+ await cleanup.cleanup(server, testBudgetId);
78
+ }
79
+ });
80
+
81
+ beforeEach(() => {
82
+ if (testConfig.skipE2ETests) {
83
+ // Skip individual tests if E2E tests are disabled
84
+ return;
85
+ }
86
+ });
87
+
88
+ describe('Complete Budget Management Workflow', () => {
89
+ it('should retrieve and validate budget information', async () => {
90
+ if (testConfig.skipE2ETests) return;
91
+
92
+ // List all budgets
93
+ const budgetsResult = await executeToolCall(server, 'ynab:list_budgets');
94
+
95
+ // Validate output schema
96
+ const budgetsValidation = validateOutputSchema(server, 'list_budgets', budgetsResult);
97
+ expect(budgetsValidation.valid).toBe(true);
98
+ if (!budgetsValidation.valid) {
99
+ console.error('list_budgets schema validation errors:', budgetsValidation.errors);
100
+ }
101
+
102
+ const budgets = parseToolResult(budgetsResult);
103
+
104
+ // Verify backward compatibility contract: parseToolResult returns {success: true, data: ...}
105
+ expect(budgets).toHaveProperty('success');
106
+ expect(budgets.success).toBe(true);
107
+ expect(budgets).toHaveProperty('data');
108
+
109
+ expect(budgets.data).toBeDefined();
110
+ expect(budgets.data.budgets).toBeDefined();
111
+ expect(Array.isArray(budgets.data.budgets)).toBe(true);
112
+ expect(budgets.data.budgets.length).toBeGreaterThan(0);
113
+
114
+ // Validate budget structure
115
+ budgets.data.budgets.forEach(YNABAssertions.assertBudget);
116
+
117
+ // Get specific budget details
118
+ const budgetResult = await executeToolCall(server, 'ynab:get_budget', {
119
+ budget_id: testBudgetId,
120
+ });
121
+
122
+ // Validate output schema
123
+ const budgetValidation = validateOutputSchema(server, 'get_budget', budgetResult);
124
+ expect(budgetValidation.valid).toBe(true);
125
+ if (!budgetValidation.valid) {
126
+ console.error('get_budget schema validation errors:', budgetValidation.errors);
127
+ }
128
+
129
+ const budget = parseToolResult(budgetResult);
130
+
131
+ expect(budget.data).toBeDefined();
132
+ expect(budget.data.budget).toBeDefined();
133
+ YNABAssertions.assertBudget(budget.data.budget);
134
+ expect(budget.data.budget.id).toBe(testBudgetId);
135
+ });
136
+
137
+ it('should retrieve user information', async () => {
138
+ if (testConfig.skipE2ETests) return;
139
+
140
+ const userResult = await executeToolCall(server, 'ynab:get_user');
141
+ const user = parseToolResult(userResult);
142
+
143
+ expect(user.data).toBeDefined();
144
+ expect(user.data.user).toBeDefined();
145
+ expect(typeof user.data.user.id).toBe('string');
146
+ });
147
+ });
148
+
149
+ describe('Complete Account Management Workflow', () => {
150
+ it('should list and retrieve account information', async () => {
151
+ if (testConfig.skipE2ETests) return;
152
+
153
+ // List all accounts
154
+ const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
155
+ budget_id: testBudgetId,
156
+ });
157
+
158
+ // Validate output schema
159
+ const accountsValidation = validateOutputSchema(server, 'list_accounts', accountsResult);
160
+ expect(accountsValidation.valid).toBe(true);
161
+ if (!accountsValidation.valid) {
162
+ console.error('list_accounts schema validation errors:', accountsValidation.errors);
163
+ }
164
+
165
+ const accounts = parseToolResult(accountsResult);
166
+
167
+ // Verify backward compatibility contract: parseToolResult returns {success: true, data: ...}
168
+ expect(accounts).toHaveProperty('success');
169
+ expect(accounts.success).toBe(true);
170
+ expect(accounts).toHaveProperty('data');
171
+
172
+ expect(accounts.data).toBeDefined();
173
+ expect(accounts.data.accounts).toBeDefined();
174
+ expect(Array.isArray(accounts.data.accounts)).toBe(true);
175
+ expect(accounts.data.accounts.length).toBeGreaterThan(0);
176
+
177
+ // Validate account structures
178
+ accounts.data.accounts.forEach(YNABAssertions.assertAccount);
179
+
180
+ // Get specific account details
181
+ const accountResult = await executeToolCall(server, 'ynab:get_account', {
182
+ budget_id: testBudgetId,
183
+ account_id: testAccountId,
184
+ });
185
+
186
+ // Validate output schema
187
+ const accountValidation = validateOutputSchema(server, 'get_account', accountResult);
188
+ expect(accountValidation.valid).toBe(true);
189
+ if (!accountValidation.valid) {
190
+ console.error('get_account schema validation errors:', accountValidation.errors);
191
+ }
192
+
193
+ const account = parseToolResult(accountResult);
194
+
195
+ expect(account.data).toBeDefined();
196
+ expect(account.data.account).toBeDefined();
197
+ YNABAssertions.assertAccount(account.data.account);
198
+ expect(account.data.account.id).toBe(testAccountId);
199
+
200
+ // Reconcile account as part of account management workflow
201
+ const reconcileResult = await executeToolCall(server, 'ynab:reconcile_account', {
202
+ budget_id: testBudgetId,
203
+ account_id: testAccountId,
204
+ cleared_balance: account.data.account.cleared_balance,
205
+ });
206
+
207
+ // Validate reconcile_account output schema
208
+ const reconcileValidation = validateOutputSchema(
209
+ server,
210
+ 'reconcile_account',
211
+ reconcileResult,
212
+ );
213
+ expect(reconcileValidation.valid).toBe(true);
214
+ if (!reconcileValidation.valid) {
215
+ console.error('reconcile_account schema validation errors:', reconcileValidation.errors);
216
+ }
217
+ });
218
+
219
+ it('should create a new account', async () => {
220
+ if (testConfig.skipE2ETests) return;
221
+
222
+ const accountName = TestData.generateAccountName();
223
+
224
+ const createResult = await executeToolCall(server, 'ynab:create_account', {
225
+ budget_id: testBudgetId,
226
+ name: accountName,
227
+ type: 'checking',
228
+ balance: 10000, // $10.00
229
+ });
230
+
231
+ // Validate output schema
232
+ const createValidation = validateOutputSchema(server, 'create_account', createResult);
233
+ expect(createValidation.valid).toBe(true);
234
+ if (!createValidation.valid) {
235
+ console.error('create_account schema validation errors:', createValidation.errors);
236
+ }
237
+
238
+ const createdAccount = parseToolResult(createResult);
239
+
240
+ expect(createdAccount.data).toBeDefined();
241
+ expect(createdAccount.data.account).toBeDefined();
242
+ YNABAssertions.assertAccount(createdAccount.data.account);
243
+ expect(createdAccount.data.account.name).toBe(accountName);
244
+ expect(createdAccount.data.account.type).toBe('checking');
245
+
246
+ // Track for cleanup
247
+ cleanup.trackAccount(createdAccount.data.account.id);
248
+
249
+ // Verify account appears in list
250
+ const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
251
+ budget_id: testBudgetId,
252
+ });
253
+ const accounts = parseToolResult(accountsResult);
254
+
255
+ const foundAccount = accounts.data.accounts.find(
256
+ (acc: any) => acc.id === createdAccount.data.account.id,
257
+ );
258
+ expect(foundAccount).toBeDefined();
259
+ expect(foundAccount.name).toBe(accountName);
260
+ });
261
+ });
262
+
263
+ describe('Complete Transaction Management Workflow', () => {
264
+ let testTransactionId: string;
265
+
266
+ it('should create, retrieve, update, and delete a transaction', async () => {
267
+ if (testConfig.skipE2ETests) return;
268
+
269
+ // Get categories for transaction creation
270
+ const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
271
+ budget_id: testBudgetId,
272
+ });
273
+ const categories = parseToolResult(categoriesResult);
274
+
275
+ expect(categories.data.category_groups).toBeDefined();
276
+ expect(Array.isArray(categories.data.category_groups)).toBe(true);
277
+
278
+ // Find a non-hidden category
279
+ let testCategoryId: string | undefined;
280
+ for (const group of categories.data.category_groups) {
281
+ const availableCategory = group.categories?.find((cat: any) => !cat.hidden);
282
+ if (availableCategory) {
283
+ testCategoryId = availableCategory.id;
284
+ break;
285
+ }
286
+ }
287
+
288
+ // Create a transaction
289
+ const transactionData = TestData.generateTransaction(testAccountId, testCategoryId);
290
+
291
+ const createResult = await executeToolCall(server, 'ynab:create_transaction', {
292
+ budget_id: testBudgetId,
293
+ ...transactionData,
294
+ });
295
+
296
+ // Validate create_transaction output schema
297
+ const createValidation = validateOutputSchema(server, 'create_transaction', createResult);
298
+ expect(createValidation.valid).toBe(true);
299
+ if (!createValidation.valid) {
300
+ console.error('create_transaction schema validation errors:', createValidation.errors);
301
+ }
302
+
303
+ const createdTransaction = parseToolResult(createResult);
304
+
305
+ // Verify backward compatibility contract: parseToolResult returns {success: true, data: ...}
306
+ expect(createdTransaction).toHaveProperty('success');
307
+ expect(createdTransaction.success).toBe(true);
308
+ expect(createdTransaction).toHaveProperty('data');
309
+
310
+ expect(createdTransaction.data).toBeDefined();
311
+ expect(createdTransaction.data.transaction).toBeDefined();
312
+ YNABAssertions.assertTransaction(createdTransaction.data.transaction);
313
+
314
+ testTransactionId = createdTransaction.data.transaction.id;
315
+ cleanup.trackTransaction(testTransactionId);
316
+
317
+ // Retrieve the transaction
318
+ const getResult = await executeToolCall(server, 'ynab:get_transaction', {
319
+ budget_id: testBudgetId,
320
+ transaction_id: testTransactionId,
321
+ });
322
+
323
+ // Validate get_transaction output schema
324
+ const getValidation = validateOutputSchema(server, 'get_transaction', getResult);
325
+ expect(getValidation.valid).toBe(true);
326
+ if (!getValidation.valid) {
327
+ console.error('get_transaction schema validation errors:', getValidation.errors);
328
+ }
329
+
330
+ const retrievedTransaction = parseToolResult(getResult);
331
+
332
+ expect(retrievedTransaction.data).toBeDefined();
333
+ expect(retrievedTransaction.data.transaction).toBeDefined();
334
+ expect(retrievedTransaction.data.transaction.id).toBe(testTransactionId);
335
+ YNABAssertions.assertTransaction(retrievedTransaction.data.transaction);
336
+
337
+ // Update the transaction
338
+ const updatedMemo = `Updated memo ${Date.now()}`;
339
+ const updateResult = await executeToolCall(server, 'ynab:update_transaction', {
340
+ budget_id: testBudgetId,
341
+ transaction_id: testTransactionId,
342
+ memo: updatedMemo,
343
+ });
344
+
345
+ // Validate update_transaction output schema
346
+ const updateValidation = validateOutputSchema(server, 'update_transaction', updateResult);
347
+ expect(updateValidation.valid).toBe(true);
348
+ if (!updateValidation.valid) {
349
+ console.error('update_transaction schema validation errors:', updateValidation.errors);
350
+ }
351
+
352
+ const updatedTransaction = parseToolResult(updateResult);
353
+
354
+ expect(updatedTransaction.data).toBeDefined();
355
+ expect(updatedTransaction.data.transaction).toBeDefined();
356
+ expect(updatedTransaction.data.transaction.memo).toBe(updatedMemo);
357
+
358
+ // List transactions and verify our transaction is included
359
+ const listResult = await executeToolCall(server, 'ynab:list_transactions', {
360
+ budget_id: testBudgetId,
361
+ account_id: testAccountId,
362
+ });
363
+
364
+ // Validate list_transactions output schema
365
+ const listValidation = validateOutputSchema(server, 'list_transactions', listResult);
366
+ expect(listValidation.valid).toBe(true);
367
+ if (!listValidation.valid) {
368
+ console.error('list_transactions schema validation errors:', listValidation.errors);
369
+ }
370
+
371
+ const transactions = parseToolResult(listResult);
372
+
373
+ expect(transactions.data).toBeDefined();
374
+ expect(transactions.data.transactions).toBeDefined();
375
+ expect(Array.isArray(transactions.data.transactions)).toBe(true);
376
+
377
+ const foundTransaction = transactions.data.transactions.find(
378
+ (txn: any) => txn.id === testTransactionId,
379
+ );
380
+ expect(foundTransaction).toBeDefined();
381
+ expect(foundTransaction.memo).toBe(updatedMemo);
382
+
383
+ // Delete the transaction
384
+ const deleteResult = await executeToolCall(server, 'ynab:delete_transaction', {
385
+ budget_id: testBudgetId,
386
+ transaction_id: testTransactionId,
387
+ });
388
+
389
+ // Validate delete_transaction output schema
390
+ const deleteValidation = validateOutputSchema(server, 'delete_transaction', deleteResult);
391
+ expect(deleteValidation.valid).toBe(true);
392
+ if (!deleteValidation.valid) {
393
+ console.error('delete_transaction schema validation errors:', deleteValidation.errors);
394
+ }
395
+
396
+ const deleteResponse = parseToolResult(deleteResult);
397
+
398
+ expect(deleteResponse.data).toBeDefined();
399
+
400
+ // Verify transaction is deleted (should return error when trying to retrieve)
401
+ const getDeletedResult = await executeToolCall(server, 'ynab:get_transaction', {
402
+ budget_id: testBudgetId,
403
+ transaction_id: testTransactionId,
404
+ });
405
+ expect(isErrorResult(getDeletedResult)).toBe(true);
406
+ // Expected - transaction should not be found
407
+ expect(getDeletedResult.content).toBeDefined();
408
+ expect(getDeletedResult.content.length).toBeGreaterThan(0);
409
+ });
410
+
411
+ it('should filter transactions by date and account', async () => {
412
+ if (testConfig.skipE2ETests) return;
413
+
414
+ const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
415
+
416
+ // List transactions since last month
417
+ const recentResult = await executeToolCall(server, 'ynab:list_transactions', {
418
+ budget_id: testBudgetId,
419
+ since_date: lastMonth,
420
+ });
421
+ const recentTransactions = parseToolResult(recentResult);
422
+
423
+ expect(recentTransactions.data).toBeDefined();
424
+ expect(recentTransactions.data.transactions).toBeDefined();
425
+ expect(Array.isArray(recentTransactions.data.transactions)).toBe(true);
426
+
427
+ // List transactions for specific account
428
+ const accountResult = await executeToolCall(server, 'ynab:list_transactions', {
429
+ budget_id: testBudgetId,
430
+ account_id: testAccountId,
431
+ });
432
+ const accountTransactions = parseToolResult(accountResult);
433
+
434
+ expect(accountTransactions.data).toBeDefined();
435
+ expect(accountTransactions.data.transactions).toBeDefined();
436
+ expect(Array.isArray(accountTransactions.data.transactions)).toBe(true);
437
+
438
+ // All transactions should be for the specified account
439
+ accountTransactions.data.transactions.forEach((txn: any) => {
440
+ expect(txn.account_id).toBe(testAccountId);
441
+ });
442
+ });
443
+
444
+ it('should export and compare transactions', async () => {
445
+ if (testConfig.skipE2ETests) return;
446
+
447
+ // Export transactions as part of transaction management workflow
448
+ const exportResult = await executeToolCall(server, 'ynab:export_transactions', {
449
+ budget_id: testBudgetId,
450
+ account_id: testAccountId,
451
+ });
452
+
453
+ // Validate export_transactions output schema
454
+ const exportValidation = validateOutputSchema(server, 'export_transactions', exportResult);
455
+ expect(exportValidation.valid).toBe(true);
456
+ if (!exportValidation.valid) {
457
+ console.error('export_transactions schema validation errors:', exportValidation.errors);
458
+ }
459
+
460
+ const exportData = parseToolResult(exportResult);
461
+ expect(exportData.data).toBeDefined();
462
+
463
+ // Compare transactions as part of transaction management workflow
464
+ const csvData = `Date,Payee,Amount\n2025-01-15,Test Comparison Payee,-25.00`;
465
+ const compareResult = await executeToolCall(server, 'ynab:compare_transactions', {
466
+ budget_id: testBudgetId,
467
+ account_id: testAccountId,
468
+ csv_data: csvData,
469
+ start_date: '2025-01-01',
470
+ end_date: '2025-01-31',
471
+ });
472
+
473
+ // Validate compare_transactions output schema
474
+ const compareValidation = validateOutputSchema(server, 'compare_transactions', compareResult);
475
+ expect(compareValidation.valid).toBe(true);
476
+ if (!compareValidation.valid) {
477
+ console.error('compare_transactions schema validation errors:', compareValidation.errors);
478
+ }
479
+
480
+ const compareData = parseToolResult(compareResult);
481
+ expect(compareData.data).toBeDefined();
482
+ });
483
+
484
+ it('should create and update transactions in bulk', async () => {
485
+ if (testConfig.skipE2ETests) return;
486
+
487
+ // Create multiple transactions as part of bulk workflow
488
+ const transactions = [
489
+ {
490
+ account_id: testAccountId,
491
+ date: new Date().toISOString().split('T')[0],
492
+ amount: -1500,
493
+ payee_name: `Bulk Workflow Payee 1 ${Date.now()}`,
494
+ memo: 'Bulk workflow test 1',
495
+ cleared: 'uncleared' as const,
496
+ },
497
+ {
498
+ account_id: testAccountId,
499
+ date: new Date().toISOString().split('T')[0],
500
+ amount: -2500,
501
+ payee_name: `Bulk Workflow Payee 2 ${Date.now()}`,
502
+ memo: 'Bulk workflow test 2',
503
+ cleared: 'uncleared' as const,
504
+ },
505
+ ];
506
+
507
+ const createBulkResult = await executeToolCall(server, 'ynab:create_transactions', {
508
+ budget_id: testBudgetId,
509
+ transactions,
510
+ });
511
+
512
+ // Validate create_transactions (bulk) output schema
513
+ const createBulkValidation = validateOutputSchema(
514
+ server,
515
+ 'create_transactions',
516
+ createBulkResult,
517
+ );
518
+ expect(createBulkValidation.valid).toBe(true);
519
+ if (!createBulkValidation.valid) {
520
+ console.error('create_transactions schema validation errors:', createBulkValidation.errors);
521
+ }
522
+
523
+ const createdBulk = parseToolResult(createBulkResult);
524
+ expect(createdBulk.data?.transactions).toBeDefined();
525
+ expect(Array.isArray(createdBulk.data.transactions)).toBe(true);
526
+ expect(createdBulk.data.transactions.length).toBe(2);
527
+
528
+ // Track for cleanup
529
+ const transactionIds = createdBulk.data.transactions.map((txn: any) => txn.id);
530
+ transactionIds.forEach((id: string) => cleanup.trackTransaction(id));
531
+
532
+ // Update transactions in bulk as part of workflow
533
+ const updateBulkResult = await executeToolCall(server, 'ynab:update_transactions', {
534
+ budget_id: testBudgetId,
535
+ transactions: transactionIds.map((id: string, index: number) => ({
536
+ id,
537
+ memo: `Updated bulk memo ${index + 1}`,
538
+ })),
539
+ });
540
+
541
+ // Validate update_transactions (bulk) output schema
542
+ const updateBulkValidation = validateOutputSchema(
543
+ server,
544
+ 'update_transactions',
545
+ updateBulkResult,
546
+ );
547
+ expect(updateBulkValidation.valid).toBe(true);
548
+ if (!updateBulkValidation.valid) {
549
+ console.error('update_transactions schema validation errors:', updateBulkValidation.errors);
550
+ }
551
+
552
+ const updatedBulk = parseToolResult(updateBulkResult);
553
+ expect(updatedBulk.data?.transactions).toBeDefined();
554
+ expect(Array.isArray(updatedBulk.data.transactions)).toBe(true);
555
+ });
556
+
557
+ it('should create receipt split transaction', async () => {
558
+ if (testConfig.skipE2ETests) return;
559
+
560
+ // Get categories for the receipt split
561
+ const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
562
+ budget_id: testBudgetId,
563
+ });
564
+ const categories = parseToolResult(categoriesResult);
565
+
566
+ // Find a non-hidden category
567
+ let testCategoryName: string | undefined;
568
+ for (const group of categories.data.category_groups) {
569
+ const availableCategory = group.categories?.find((cat: any) => !cat.hidden);
570
+ if (availableCategory) {
571
+ testCategoryName = availableCategory.name;
572
+ break;
573
+ }
574
+ }
575
+
576
+ if (!testCategoryName) {
577
+ console.warn('No available categories found for receipt split test');
578
+ return;
579
+ }
580
+
581
+ // Create receipt split transaction as part of transaction workflow
582
+ const receiptResult = await executeToolCall(server, 'ynab:create_receipt_split_transaction', {
583
+ budget_id: testBudgetId,
584
+ account_id: testAccountId,
585
+ date: new Date().toISOString().split('T')[0],
586
+ payee_name: `Receipt Workflow ${Date.now()}`,
587
+ tax_amount: 150,
588
+ receipt_items: [
589
+ {
590
+ category_name: testCategoryName,
591
+ amount: 2000,
592
+ },
593
+ ],
594
+ });
595
+
596
+ // Validate create_receipt_split_transaction output schema
597
+ const receiptValidation = validateOutputSchema(
598
+ server,
599
+ 'create_receipt_split_transaction',
600
+ receiptResult,
601
+ );
602
+ expect(receiptValidation.valid).toBe(true);
603
+ if (!receiptValidation.valid) {
604
+ console.error(
605
+ 'create_receipt_split_transaction schema validation errors:',
606
+ receiptValidation.errors,
607
+ );
608
+ }
609
+
610
+ const receiptData = parseToolResult(receiptResult);
611
+ expect(receiptData.data?.transaction).toBeDefined();
612
+
613
+ // Track for cleanup
614
+ if (receiptData.data.transaction.id) {
615
+ cleanup.trackTransaction(receiptData.data.transaction.id);
616
+ }
617
+ });
618
+ });
619
+
620
+ describe('Complete Category Management Workflow', () => {
621
+ it('should list categories and update category budget', async () => {
622
+ if (testConfig.skipE2ETests) return;
623
+
624
+ // List all categories
625
+ const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
626
+ budget_id: testBudgetId,
627
+ });
628
+
629
+ // Validate list_categories output schema
630
+ const listValidation = validateOutputSchema(server, 'list_categories', categoriesResult);
631
+ expect(listValidation.valid).toBe(true);
632
+ if (!listValidation.valid) {
633
+ console.error('list_categories schema validation errors:', listValidation.errors);
634
+ }
635
+
636
+ const categories = parseToolResult(categoriesResult);
637
+
638
+ expect(categories.data).toBeDefined();
639
+ expect(categories.data.category_groups).toBeDefined();
640
+ expect(Array.isArray(categories.data.category_groups)).toBe(true);
641
+
642
+ // Find a category to test with
643
+ let testCategoryId: string | undefined;
644
+ let testCategory: any;
645
+
646
+ for (const group of categories.data.category_groups) {
647
+ if (group.categories && group.categories.length > 0) {
648
+ testCategory = group.categories.find((cat: any) => !cat.hidden);
649
+ if (testCategory) {
650
+ testCategoryId = testCategory.id;
651
+ break;
652
+ }
653
+ }
654
+ }
655
+
656
+ if (!testCategoryId) {
657
+ console.warn('No available categories found for testing');
658
+ return;
659
+ }
660
+
661
+ // Get specific category details
662
+ const categoryResult = await executeToolCall(server, 'ynab:get_category', {
663
+ budget_id: testBudgetId,
664
+ category_id: testCategoryId,
665
+ });
666
+
667
+ // Validate get_category output schema
668
+ const getValidation = validateOutputSchema(server, 'get_category', categoryResult);
669
+ expect(getValidation.valid).toBe(true);
670
+ if (!getValidation.valid) {
671
+ console.error('get_category schema validation errors:', getValidation.errors);
672
+ }
673
+
674
+ const category = parseToolResult(categoryResult);
675
+
676
+ expect(category.data).toBeDefined();
677
+ expect(category.data.category).toBeDefined();
678
+ YNABAssertions.assertCategory(category.data.category);
679
+ expect(category.data.category.id).toBe(testCategoryId);
680
+
681
+ // Update category budget
682
+ const newBudgetAmount = TestData.generateAmount(50); // $50.00
683
+ const updateResult = await executeToolCall(server, 'ynab:update_category', {
684
+ budget_id: testBudgetId,
685
+ category_id: testCategoryId,
686
+ budgeted: newBudgetAmount,
687
+ });
688
+
689
+ // Validate update_category output schema
690
+ const updateValidation = validateOutputSchema(server, 'update_category', updateResult);
691
+ expect(updateValidation.valid).toBe(true);
692
+ if (!updateValidation.valid) {
693
+ console.error('update_category schema validation errors:', updateValidation.errors);
694
+ }
695
+
696
+ const updatedCategory = parseToolResult(updateResult);
697
+
698
+ expect(updatedCategory.data).toBeDefined();
699
+ expect(updatedCategory.data.category).toBeDefined();
700
+ expect(updatedCategory.data.category.budgeted).toBe(newBudgetAmount);
701
+ });
702
+ });
703
+
704
+ describe('Complete Payee Management Workflow', () => {
705
+ it('should list and retrieve payee information', async () => {
706
+ if (testConfig.skipE2ETests) return;
707
+
708
+ // List all payees
709
+ const payeesResult = await executeToolCall(server, 'ynab:list_payees', {
710
+ budget_id: testBudgetId,
711
+ });
712
+
713
+ // Validate list_payees output schema
714
+ const listValidation = validateOutputSchema(server, 'list_payees', payeesResult);
715
+ expect(listValidation.valid).toBe(true);
716
+ if (!listValidation.valid) {
717
+ console.error('list_payees schema validation errors:', listValidation.errors);
718
+ }
719
+
720
+ const payees = parseToolResult(payeesResult);
721
+
722
+ expect(payees.data).toBeDefined();
723
+ expect(payees.data.payees).toBeDefined();
724
+ expect(Array.isArray(payees.data.payees)).toBe(true);
725
+
726
+ if (payees.data.payees.length > 0) {
727
+ // Validate payee structures
728
+ payees.data.payees.forEach(YNABAssertions.assertPayee);
729
+
730
+ // Get specific payee details
731
+ const testPayeeId = payees.data.payees[0].id;
732
+ const payeeResult = await executeToolCall(server, 'ynab:get_payee', {
733
+ budget_id: testBudgetId,
734
+ payee_id: testPayeeId,
735
+ });
736
+
737
+ // Validate get_payee output schema
738
+ const getValidation = validateOutputSchema(server, 'get_payee', payeeResult);
739
+ expect(getValidation.valid).toBe(true);
740
+ if (!getValidation.valid) {
741
+ console.error('get_payee schema validation errors:', getValidation.errors);
742
+ }
743
+
744
+ const payee = parseToolResult(payeeResult);
745
+
746
+ expect(payee.data).toBeDefined();
747
+ expect(payee.data.payee).toBeDefined();
748
+ YNABAssertions.assertPayee(payee.data.payee);
749
+ expect(payee.data.payee.id).toBe(testPayeeId);
750
+ }
751
+ });
752
+ });
753
+
754
+ describe('Complete Monthly Data Workflow', () => {
755
+ it('should retrieve monthly budget data', async () => {
756
+ if (testConfig.skipE2ETests) return;
757
+
758
+ // List all months
759
+ const monthsResult = await executeToolCall(server, 'ynab:list_months', {
760
+ budget_id: testBudgetId,
761
+ });
762
+
763
+ // Validate list_months output schema
764
+ const listValidation = validateOutputSchema(server, 'list_months', monthsResult);
765
+ expect(listValidation.valid).toBe(true);
766
+ if (!listValidation.valid) {
767
+ console.error('list_months schema validation errors:', listValidation.errors);
768
+ }
769
+
770
+ const months = parseToolResult(monthsResult);
771
+
772
+ expect(months.data).toBeDefined();
773
+ expect(months.data.months).toBeDefined();
774
+ expect(Array.isArray(months.data.months)).toBe(true);
775
+ expect(months.data.months.length).toBeGreaterThan(0);
776
+
777
+ // Get current month data
778
+ const currentMonth = getCurrentMonth();
779
+ const monthResult = await executeToolCall(server, 'ynab:get_month', {
780
+ budget_id: testBudgetId,
781
+ month: currentMonth,
782
+ });
783
+
784
+ // Validate get_month output schema
785
+ const getValidation = validateOutputSchema(server, 'get_month', monthResult);
786
+ expect(getValidation.valid).toBe(true);
787
+ if (!getValidation.valid) {
788
+ console.error('get_month schema validation errors:', getValidation.errors);
789
+ }
790
+
791
+ const month = parseToolResult(monthResult);
792
+
793
+ expect(month.data).toBeDefined();
794
+ expect(month.data.month).toBeDefined();
795
+ expect(typeof month.data.month.month).toBe('string');
796
+ expect(typeof month.data.month.income).toBe('number');
797
+ expect(typeof month.data.month.budgeted).toBe('number');
798
+ expect(typeof month.data.month.activity).toBe('number');
799
+ expect(typeof month.data.month.to_be_budgeted).toBe('number');
800
+ });
801
+ });
802
+
803
+ describe('Utility Tools Workflow', () => {
804
+ it('should convert amounts between dollars and milliunits', async () => {
805
+ if (testConfig.skipE2ETests) return;
806
+
807
+ // Convert dollars to milliunits
808
+ const toMilliunitsResult = await executeToolCall(server, 'ynab:convert_amount', {
809
+ amount: 25.5,
810
+ to_milliunits: true,
811
+ });
812
+ const milliunits = parseToolResult(toMilliunitsResult);
813
+
814
+ expect(milliunits.data?.conversion?.converted_amount).toBe(25500);
815
+ expect(milliunits.data?.conversion?.description).toContain('25500');
816
+ expect(milliunits.data?.conversion?.to_milliunits).toBe(true);
817
+
818
+ // Convert milliunits to dollars
819
+ const toDollarsResult = await executeToolCall(server, 'ynab:convert_amount', {
820
+ amount: 25500,
821
+ to_milliunits: false,
822
+ });
823
+ const dollars = parseToolResult(toDollarsResult);
824
+
825
+ expect(dollars.data?.conversion?.converted_amount).toBe(25.5);
826
+ expect(dollars.data?.conversion?.description).toContain('$25.50');
827
+ expect(dollars.data?.conversion?.to_milliunits).toBe(false);
828
+ });
829
+ });
830
+
831
+ describe('v0.8.x Architecture Integration Tests', () => {
832
+ describe('Cache System Verification', () => {
833
+ it('should demonstrate cache warming after default budget set', async () => {
834
+ if (testConfig.skipE2ETests) return;
835
+
836
+ // Enable caching for this test
837
+ testEnv.enableCache();
838
+
839
+ try {
840
+ // Get initial cache stats
841
+ const initialStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
842
+ const initialStats = parseToolResult(initialStatsResult);
843
+ const initialCacheStats = initialStats.data?.cache;
844
+
845
+ // Set default budget (should trigger cache warming)
846
+ await executeToolCall(server, 'ynab:set_default_budget', {
847
+ budget_id: testBudgetId,
848
+ });
849
+
850
+ // Allow time for cache warming (fire-and-forget)
851
+ await new Promise((resolve) => setTimeout(resolve, 1000));
852
+
853
+ // Get updated cache stats
854
+ const finalStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
855
+ const finalStats = parseToolResult(finalStatsResult);
856
+ const finalCacheStats = finalStats.data?.cache;
857
+
858
+ // Verify cache warming occurred
859
+ expect(finalCacheStats?.entries).toBeGreaterThan(initialCacheStats?.entries || 0);
860
+ expect(finalCacheStats?.hits).toBeGreaterThanOrEqual(0);
861
+ } finally {
862
+ // Restore original NODE_ENV
863
+ testEnv.restoreEnv();
864
+ }
865
+ });
866
+
867
+ it('should demonstrate LRU eviction and observability metrics', async () => {
868
+ if (testConfig.skipE2ETests) return;
869
+
870
+ // Enable caching for this test (bypass NODE_ENV='test' check)
871
+ testEnv.enableCache();
872
+
873
+ try {
874
+ // Get initial cache stats
875
+ const initialStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
876
+ const initialStats = parseToolResult(initialStatsResult);
877
+ const initialCacheStats = initialStats.data?.cache;
878
+
879
+ // Perform operations that should hit cache
880
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
881
+ await executeToolCall(server, 'ynab:list_categories', { budget_id: testBudgetId });
882
+ await executeToolCall(server, 'ynab:list_payees', { budget_id: testBudgetId });
883
+
884
+ // Perform same operations again (should hit cache)
885
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
886
+ await executeToolCall(server, 'ynab:list_categories', { budget_id: testBudgetId });
887
+ await executeToolCall(server, 'ynab:list_payees', { budget_id: testBudgetId });
888
+
889
+ // Get final cache stats
890
+ const finalStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
891
+ const finalStats = parseToolResult(finalStatsResult);
892
+ const finalCacheStats = finalStats.data?.cache;
893
+
894
+ // Verify cache behavior
895
+ expect(finalCacheStats?.hits).toBeGreaterThan(initialCacheStats?.hits || 0);
896
+ expect(finalCacheStats?.misses).toBeGreaterThan(initialCacheStats?.misses || 0);
897
+ expect(finalCacheStats?.hits).toBeGreaterThan(0);
898
+ expect(finalCacheStats?.entries).toBeGreaterThan(0);
899
+ } finally {
900
+ // Restore original NODE_ENV
901
+ testEnv.restoreEnv();
902
+ }
903
+ });
904
+
905
+ it('should demonstrate cache invalidation on write operations', async () => {
906
+ if (testConfig.skipE2ETests) return;
907
+
908
+ // Enable caching for this test
909
+ testEnv.enableCache();
910
+
911
+ try {
912
+ // Prime cache by listing accounts
913
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
914
+
915
+ // Create new account (should invalidate accounts cache)
916
+ const accountName = TestData.generateAccountName();
917
+ const createResult = await executeToolCall(server, 'ynab:create_account', {
918
+ budget_id: testBudgetId,
919
+ name: accountName,
920
+ type: 'checking',
921
+ balance: 10000,
922
+ });
923
+
924
+ // Validate output schema
925
+ const createValidation = validateOutputSchema(server, 'create_account', createResult);
926
+ expect(createValidation.valid).toBe(true);
927
+ if (!createValidation.valid) {
928
+ console.error('create_account schema validation errors:', createValidation.errors);
929
+ }
930
+
931
+ const createdAccount = parseToolResult(createResult);
932
+ cleanup.trackAccount(createdAccount.data.account.id);
933
+
934
+ // List accounts again (should show new account due to cache invalidation)
935
+ const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
936
+ budget_id: testBudgetId,
937
+ });
938
+ const accounts = parseToolResult(accountsResult);
939
+
940
+ const foundAccount = accounts.data.accounts.find(
941
+ (acc: any) => acc.id === createdAccount.data.account.id,
942
+ );
943
+ expect(foundAccount).toBeDefined();
944
+ expect(foundAccount.name).toBe(accountName);
945
+ } finally {
946
+ // Restore original NODE_ENV
947
+ testEnv.restoreEnv();
948
+ }
949
+ });
950
+ });
951
+
952
+ describe('Budget Resolution Consistency', () => {
953
+ it('should provide consistent error messages for missing budget ID', async () => {
954
+ if (testConfig.skipE2ETests) return;
955
+
956
+ // Clear default budget first
957
+ server.clearDefaultBudget();
958
+
959
+ // Test multiple tools for consistent error handling
960
+ const toolsToTest = [
961
+ 'ynab:list_accounts',
962
+ 'ynab:list_categories',
963
+ 'ynab:list_payees',
964
+ 'ynab:list_transactions',
965
+ ];
966
+
967
+ for (const toolName of toolsToTest) {
968
+ const result = await executeToolCall(server, toolName, {});
969
+ expect(isErrorResult(result)).toBe(true);
970
+ const errorMessage = getErrorMessage(result);
971
+ expect(errorMessage).toContain('No budget ID provided and no default budget set');
972
+ expect(errorMessage).toContain('set_default_budget');
973
+ }
974
+
975
+ // Restore default budget for other tests
976
+ await executeToolCall(server, 'ynab:set_default_budget', { budget_id: testBudgetId });
977
+ });
978
+
979
+ it('should handle invalid budget ID format consistently', async () => {
980
+ if (testConfig.skipE2ETests) return;
981
+
982
+ const invalidBudgetId = 'invalid-format';
983
+ const toolsToTest = ['ynab:list_accounts', 'ynab:list_categories', 'ynab:list_payees'];
984
+
985
+ for (const toolName of toolsToTest) {
986
+ const result = await executeToolCall(server, toolName, { budget_id: invalidBudgetId });
987
+ expect(isErrorResult(result)).toBe(true);
988
+ // All tools should provide similar error handling
989
+ expect(result.content).toBeDefined();
990
+ expect(result.content.length).toBeGreaterThan(0);
991
+ }
992
+ });
993
+ });
994
+
995
+ describe('Month Data Integration', () => {
996
+ it('should execute month data tools', async () => {
997
+ if (testConfig.skipE2ETests) return;
998
+
999
+ // Test get_month tool
1000
+ const currentMonth = new Date().toISOString().substring(0, 8) + '01';
1001
+ const monthResult = await executeToolCall(server, 'ynab:get_month', {
1002
+ budget_id: testBudgetId,
1003
+ month: currentMonth,
1004
+ });
1005
+ const monthData = parseToolResult(monthResult);
1006
+
1007
+ expect(monthData.data, 'Month data should return data object').toBeDefined();
1008
+ expect(monthData.data.month || monthData.data, 'Should contain month info').toBeDefined();
1009
+
1010
+ // Test list_months tool
1011
+ const monthsResult = await executeToolCall(server, 'ynab:list_months', {
1012
+ budget_id: testBudgetId,
1013
+ });
1014
+ const monthsData = parseToolResult(monthsResult);
1015
+
1016
+ expect(monthsData.data).toBeDefined();
1017
+ expect(Array.isArray(monthsData.data.months), 'Should return months array').toBe(true);
1018
+ });
1019
+ });
1020
+
1021
+ describe('Tool Registry Integration', () => {
1022
+ it('should demonstrate tool registry functionality', async () => {
1023
+ if (testConfig.skipE2ETests) return;
1024
+
1025
+ // Test that tool listing includes all expected tools
1026
+ const toolsResult = await server.handleListTools();
1027
+ expect(toolsResult.tools).toBeDefined();
1028
+ expect(Array.isArray(toolsResult.tools)).toBe(true);
1029
+ expect(toolsResult.tools.length).toBeGreaterThan(20);
1030
+
1031
+ // Verify key v0.8.x tools are present (tools are registered without ynab: prefix)
1032
+ const toolNames = toolsResult.tools.map((tool: any) => tool.name);
1033
+ expect(toolNames, 'Should contain list_budgets tool').toContain('list_budgets');
1034
+ expect(toolNames, 'Should contain get_month tool').toContain('get_month');
1035
+ expect(toolNames, 'Should contain list_months tool').toContain('list_months');
1036
+ expect(toolNames, 'Should contain compare_transactions tool').toContain(
1037
+ 'compare_transactions',
1038
+ );
1039
+ expect(toolNames, 'Should contain diagnostic_info tool').toContain('diagnostic_info');
1040
+
1041
+ // Test that each tool has proper schema validation
1042
+ for (const tool of toolsResult.tools) {
1043
+ expect(tool.name).toBeDefined();
1044
+ expect(tool.description).toBeDefined();
1045
+ expect(tool.inputSchema).toBeDefined();
1046
+
1047
+ // Verify that all tools define outputSchema (as guaranteed by CHANGELOG.md and docs/reference/TOOLS.md)
1048
+ // Note: Some utility tools like diagnostic_info or clear_cache may not define structured outputs,
1049
+ // but most data-retrieval and CRUD tools should have output schemas.
1050
+ expect(
1051
+ tool.outputSchema,
1052
+ `Tool '${tool.name}' should define an outputSchema`,
1053
+ ).toBeDefined();
1054
+ }
1055
+ });
1056
+ });
1057
+
1058
+ describe('Module Integration Tests', () => {
1059
+ it('should verify resource manager integration', async () => {
1060
+ if (testConfig.skipE2ETests) return;
1061
+
1062
+ // Test resource listing
1063
+ const resourcesResult = await server.handleListResources();
1064
+ expect(resourcesResult.resources).toBeDefined();
1065
+ expect(Array.isArray(resourcesResult.resources)).toBe(true);
1066
+
1067
+ // Test reading a specific resource
1068
+ if (resourcesResult.resources.length > 0) {
1069
+ const resource = resourcesResult.resources[0];
1070
+ const readResult = await server.handleReadResource({
1071
+ uri: resource.uri,
1072
+ });
1073
+ expect(readResult.contents).toBeDefined();
1074
+ }
1075
+ });
1076
+
1077
+ it('should verify prompt manager integration', async () => {
1078
+ if (testConfig.skipE2ETests) return;
1079
+
1080
+ // Test prompt listing
1081
+ const promptsResult = await server.handleListPrompts();
1082
+ expect(promptsResult.prompts).toBeDefined();
1083
+ expect(Array.isArray(promptsResult.prompts)).toBe(true);
1084
+
1085
+ // Test getting a specific prompt
1086
+ if (promptsResult.prompts.length > 0) {
1087
+ const prompt = promptsResult.prompts[0];
1088
+ const getResult = await server.handleGetPrompt({
1089
+ name: prompt.name,
1090
+ arguments: {},
1091
+ });
1092
+ expect(getResult.messages).toBeDefined();
1093
+ }
1094
+ });
1095
+
1096
+ it('should verify diagnostic manager integration', async () => {
1097
+ if (testConfig.skipE2ETests) return;
1098
+
1099
+ // Test diagnostic info tool
1100
+ const diagnosticResult = await executeToolCall(server, 'ynab:diagnostic_info');
1101
+ const diagnostic = parseToolResult(diagnosticResult);
1102
+
1103
+ expect(diagnostic.data, 'Diagnostic should return data object').toBeDefined();
1104
+
1105
+ // The diagnostic data is in the root of data, not under diagnostics
1106
+ expect(diagnostic.data.timestamp, 'Should contain timestamp').toBeDefined();
1107
+ expect(diagnostic.data.server, 'Should contain server info').toBeDefined();
1108
+ expect(diagnostic.data.memory, 'Should contain memory info').toBeDefined();
1109
+ expect(diagnostic.data.environment, 'Should contain environment info').toBeDefined();
1110
+ expect(diagnostic.data.cache, 'Should contain cache info').toBeDefined();
1111
+ });
1112
+ });
1113
+
1114
+ describe('Backward Compatibility Verification', () => {
1115
+ it('should maintain v0.7.x API compatibility', async () => {
1116
+ if (testConfig.skipE2ETests) return;
1117
+
1118
+ // Test that all existing tool calls work identically
1119
+ const v7Tools = [
1120
+ { name: 'ynab:list_budgets', args: {} },
1121
+ { name: 'ynab:list_accounts', args: { budget_id: testBudgetId } },
1122
+ { name: 'ynab:list_categories', args: { budget_id: testBudgetId } },
1123
+ { name: 'ynab:list_payees', args: { budget_id: testBudgetId } },
1124
+ { name: 'ynab:get_user', args: {} },
1125
+ { name: 'ynab:convert_amount', args: { amount: 100, to_milliunits: true } },
1126
+ ];
1127
+
1128
+ for (const tool of v7Tools) {
1129
+ const result = await executeToolCall(server, tool.name, tool.args);
1130
+ const parsed = parseToolResult(result);
1131
+
1132
+ // Verify response structure is consistent with v0.7.x
1133
+ expect(parsed.data).toBeDefined();
1134
+ expect(parsed.success).toBe(true);
1135
+ }
1136
+ });
1137
+
1138
+ it('should maintain response format consistency', async () => {
1139
+ if (testConfig.skipE2ETests) return;
1140
+
1141
+ // Test that response formats match expected v0.7.x structure
1142
+ const budgetsResult = await executeToolCall(server, 'ynab:list_budgets');
1143
+ const budgets = parseToolResult(budgetsResult);
1144
+
1145
+ // Verify standard response wrapper
1146
+ expect(budgets).toHaveProperty('success');
1147
+ expect(budgets).toHaveProperty('data');
1148
+ expect(budgets.success).toBe(true);
1149
+ expect(budgets.data).toHaveProperty('budgets');
1150
+ });
1151
+ });
1152
+
1153
+ describe('Performance Regression Tests', () => {
1154
+ it('should not introduce performance regressions', async () => {
1155
+ if (testConfig.skipE2ETests) return;
1156
+
1157
+ // Test response times for common operations
1158
+ const operations = [
1159
+ { name: 'ynab:list_budgets', args: {} },
1160
+ { name: 'ynab:list_accounts', args: { budget_id: testBudgetId } },
1161
+ { name: 'ynab:list_categories', args: { budget_id: testBudgetId } },
1162
+ ];
1163
+
1164
+ for (const operation of operations) {
1165
+ const startTime = Date.now();
1166
+ await executeToolCall(server, operation.name, operation.args);
1167
+ const endTime = Date.now();
1168
+ const duration = endTime - startTime;
1169
+
1170
+ // Response should be reasonably fast (under 5 seconds for E2E)
1171
+ expect(duration).toBeLessThan(5000);
1172
+ }
1173
+ });
1174
+
1175
+ it('should demonstrate cache performance improvements', async () => {
1176
+ if (testConfig.skipE2ETests) return;
1177
+
1178
+ // Enable caching for this test
1179
+ testEnv.enableCache();
1180
+
1181
+ try {
1182
+ // First call (cache miss)
1183
+ const startTime1 = Date.now();
1184
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
1185
+ const duration1 = Date.now() - startTime1;
1186
+
1187
+ // Second call (cache hit)
1188
+ const startTime2 = Date.now();
1189
+ await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
1190
+ const duration2 = Date.now() - startTime2;
1191
+
1192
+ // Cached call should be faster (allowing for some variance in E2E environment)
1193
+ expect(duration2).toBeLessThanOrEqual(duration1 + 500); // Allow 500ms tolerance for E2E environment
1194
+ } finally {
1195
+ // Restore original NODE_ENV
1196
+ testEnv.restoreEnv();
1197
+ }
1198
+ });
1199
+ });
1200
+
1201
+ describe('Enhanced Error Handling', () => {
1202
+ it('should provide improved error messages with actionable suggestions', async () => {
1203
+ if (testConfig.skipE2ETests) return;
1204
+
1205
+ // Clear default budget
1206
+ server.clearDefaultBudget();
1207
+
1208
+ const result = await executeToolCall(server, 'ynab:list_accounts', {});
1209
+ expect(isErrorResult(result)).toBe(true);
1210
+ const errorMessage = getErrorMessage(result);
1211
+
1212
+ // Error should provide actionable guidance
1213
+ expect(errorMessage).toContain('No budget ID provided and no default budget set');
1214
+ expect(errorMessage).toContain('set_default_budget');
1215
+ expect(errorMessage).toContain('budget_id parameter');
1216
+
1217
+ // Restore default budget
1218
+ await executeToolCall(server, 'ynab:set_default_budget', { budget_id: testBudgetId });
1219
+ });
1220
+ });
1221
+ });
1222
+
1223
+ describe('Error Handling Workflow', () => {
1224
+ it('should handle invalid budget ID gracefully', async () => {
1225
+ if (testConfig.skipE2ETests) return;
1226
+
1227
+ const result = await executeToolCall(server, 'ynab:get_budget', {
1228
+ budget_id: 'invalid-budget-id',
1229
+ });
1230
+ expect(isErrorResult(result)).toBe(true);
1231
+ expect(result.content).toBeDefined();
1232
+ expect(result.content.length).toBeGreaterThan(0);
1233
+
1234
+ // Verify error response contract: error responses should not have success: true
1235
+ const textContent = result.content.find((c) => c.type === 'text');
1236
+ if (textContent && textContent.type === 'text') {
1237
+ const parsed = JSON.parse(textContent.text);
1238
+ expect(parsed).toHaveProperty('error');
1239
+ // If success property exists, it should be false for errors
1240
+ if ('success' in parsed) {
1241
+ expect(parsed.success).toBe(false);
1242
+ }
1243
+ }
1244
+ });
1245
+
1246
+ it('should handle invalid account ID gracefully', async () => {
1247
+ if (testConfig.skipE2ETests) return;
1248
+
1249
+ const result = await executeToolCall(server, 'ynab:get_account', {
1250
+ budget_id: testBudgetId,
1251
+ account_id: 'invalid-account-id',
1252
+ });
1253
+ expect(isErrorResult(result)).toBe(true);
1254
+ expect(result.content).toBeDefined();
1255
+ expect(result.content.length).toBeGreaterThan(0);
1256
+
1257
+ // Verify error response contract
1258
+ const textContent = result.content.find((c) => c.type === 'text');
1259
+ if (textContent && textContent.type === 'text') {
1260
+ const parsed = JSON.parse(textContent.text);
1261
+ expect(parsed).toHaveProperty('error');
1262
+ if ('success' in parsed) {
1263
+ expect(parsed.success).toBe(false);
1264
+ }
1265
+ }
1266
+ });
1267
+
1268
+ it('should handle invalid transaction ID gracefully', async () => {
1269
+ if (testConfig.skipE2ETests) return;
1270
+
1271
+ const result = await executeToolCall(server, 'ynab:get_transaction', {
1272
+ budget_id: testBudgetId,
1273
+ transaction_id: 'invalid-transaction-id',
1274
+ });
1275
+ expect(isErrorResult(result)).toBe(true);
1276
+ expect(result.content).toBeDefined();
1277
+ expect(result.content.length).toBeGreaterThan(0);
1278
+
1279
+ // Verify error response contract
1280
+ const textContent = result.content.find((c) => c.type === 'text');
1281
+ if (textContent && textContent.type === 'text') {
1282
+ const parsed = JSON.parse(textContent.text);
1283
+ expect(parsed).toHaveProperty('error');
1284
+ if ('success' in parsed) {
1285
+ expect(parsed.success).toBe(false);
1286
+ }
1287
+ }
1288
+ });
1289
+ });
1290
+
1291
+ describe('Output Schema Validation', () => {
1292
+ it('should validate list_budgets output schema', async () => {
1293
+ if (testConfig.skipE2ETests) return;
1294
+
1295
+ const result = await executeToolCall(server, 'ynab:list_budgets');
1296
+ const validation = validateOutputSchema(server, 'list_budgets', result);
1297
+ expect(validation.hasSchema).toBe(true);
1298
+ expect(validation.valid).toBe(true);
1299
+ if (!validation.valid) {
1300
+ console.error('Schema validation errors:', validation.errors);
1301
+ }
1302
+ });
1303
+
1304
+ it('should validate list_accounts output schema', async () => {
1305
+ if (testConfig.skipE2ETests) return;
1306
+
1307
+ const result = await executeToolCall(server, 'ynab:list_accounts', {
1308
+ budget_id: testBudgetId,
1309
+ });
1310
+ const validation = validateOutputSchema(server, 'list_accounts', result);
1311
+ expect(validation.hasSchema).toBe(true);
1312
+ expect(validation.valid).toBe(true);
1313
+ if (!validation.valid) {
1314
+ console.error('Schema validation errors:', validation.errors);
1315
+ }
1316
+ });
1317
+
1318
+ it('should validate list_transactions output schema', async () => {
1319
+ if (testConfig.skipE2ETests) return;
1320
+
1321
+ const result = await executeToolCall(server, 'ynab:list_transactions', {
1322
+ budget_id: testBudgetId,
1323
+ since_date: '2025-01-01',
1324
+ });
1325
+ const validation = validateOutputSchema(server, 'list_transactions', result);
1326
+ expect(validation.hasSchema).toBe(true);
1327
+ expect(validation.valid).toBe(true);
1328
+ if (!validation.valid) {
1329
+ console.error('Schema validation errors:', validation.errors);
1330
+ }
1331
+ });
1332
+
1333
+ it('should validate list_categories output schema', async () => {
1334
+ if (testConfig.skipE2ETests) return;
1335
+
1336
+ const result = await executeToolCall(server, 'ynab:list_categories', {
1337
+ budget_id: testBudgetId,
1338
+ });
1339
+ const validation = validateOutputSchema(server, 'list_categories', result);
1340
+ expect(validation.hasSchema).toBe(true);
1341
+ expect(validation.valid).toBe(true);
1342
+ if (!validation.valid) {
1343
+ console.error('Schema validation errors:', validation.errors);
1344
+ }
1345
+ });
1346
+
1347
+ it('should validate list_payees output schema', async () => {
1348
+ if (testConfig.skipE2ETests) return;
1349
+
1350
+ const result = await executeToolCall(server, 'ynab:list_payees', {
1351
+ budget_id: testBudgetId,
1352
+ });
1353
+ const validation = validateOutputSchema(server, 'list_payees', result);
1354
+ expect(validation.hasSchema).toBe(true);
1355
+ expect(validation.valid).toBe(true);
1356
+ if (!validation.valid) {
1357
+ console.error('Schema validation errors:', validation.errors);
1358
+ }
1359
+ });
1360
+
1361
+ it('should validate list_months output schema', async () => {
1362
+ if (testConfig.skipE2ETests) return;
1363
+
1364
+ const result = await executeToolCall(server, 'ynab:list_months', {
1365
+ budget_id: testBudgetId,
1366
+ });
1367
+ const validation = validateOutputSchema(server, 'list_months', result);
1368
+ expect(validation.hasSchema).toBe(true);
1369
+ expect(validation.valid).toBe(true);
1370
+ if (!validation.valid) {
1371
+ console.error('Schema validation errors:', validation.errors);
1372
+ }
1373
+ });
1374
+
1375
+ it('should validate get_month output schema', async () => {
1376
+ if (testConfig.skipE2ETests) return;
1377
+
1378
+ const currentMonth = getCurrentMonth();
1379
+ const result = await executeToolCall(server, 'ynab:get_month', {
1380
+ budget_id: testBudgetId,
1381
+ month: currentMonth,
1382
+ });
1383
+ const validation = validateOutputSchema(server, 'get_month', result);
1384
+ expect(validation.hasSchema).toBe(true);
1385
+ expect(validation.valid).toBe(true);
1386
+ if (!validation.valid) {
1387
+ console.error('Schema validation errors:', validation.errors);
1388
+ }
1389
+ });
1390
+
1391
+ it('should validate get_user output schema', async () => {
1392
+ if (testConfig.skipE2ETests) return;
1393
+
1394
+ const result = await executeToolCall(server, 'ynab:get_user');
1395
+ const validation = validateOutputSchema(server, 'get_user', result);
1396
+ expect(validation.hasSchema).toBe(true);
1397
+ expect(validation.valid).toBe(true);
1398
+ if (!validation.valid) {
1399
+ console.error('Schema validation errors:', validation.errors);
1400
+ }
1401
+ });
1402
+
1403
+ it('should validate diagnostic_info output schema', async () => {
1404
+ if (testConfig.skipE2ETests) return;
1405
+
1406
+ const result = await executeToolCall(server, 'ynab:diagnostic_info');
1407
+ const validation = validateOutputSchema(server, 'diagnostic_info', result);
1408
+ expect(validation.hasSchema).toBe(true);
1409
+ expect(validation.valid).toBe(true);
1410
+ if (!validation.valid) {
1411
+ console.error('Schema validation errors:', validation.errors);
1412
+ }
1413
+ });
1414
+
1415
+ it('should validate set_default_budget output schema', async () => {
1416
+ if (testConfig.skipE2ETests) return;
1417
+
1418
+ const result = await executeToolCall(server, 'ynab:set_default_budget', {
1419
+ budget_id: testBudgetId,
1420
+ });
1421
+ const validation = validateOutputSchema(server, 'set_default_budget', result);
1422
+ expect(validation.hasSchema).toBe(true);
1423
+ expect(validation.valid).toBe(true);
1424
+ if (!validation.valid) {
1425
+ console.error('Schema validation errors:', validation.errors);
1426
+ }
1427
+ });
1428
+
1429
+ it('should validate get_default_budget output schema', async () => {
1430
+ if (testConfig.skipE2ETests) return;
1431
+
1432
+ // Ensure default budget is set
1433
+ await executeToolCall(server, 'ynab:set_default_budget', {
1434
+ budget_id: testBudgetId,
1435
+ });
1436
+
1437
+ const result = await executeToolCall(server, 'ynab:get_default_budget');
1438
+ const validation = validateOutputSchema(server, 'get_default_budget', result);
1439
+ expect(validation.hasSchema).toBe(true);
1440
+ expect(validation.valid).toBe(true);
1441
+ if (!validation.valid) {
1442
+ console.error('Schema validation errors:', validation.errors);
1443
+ }
1444
+ });
1445
+
1446
+ it('should validate clear_cache output schema', async () => {
1447
+ if (testConfig.skipE2ETests) return;
1448
+
1449
+ const result = await executeToolCall(server, 'ynab:clear_cache');
1450
+ const validation = validateOutputSchema(server, 'clear_cache', result);
1451
+ expect(validation.hasSchema).toBe(true);
1452
+ expect(validation.valid).toBe(true);
1453
+ if (!validation.valid) {
1454
+ console.error('Schema validation errors:', validation.errors);
1455
+ }
1456
+ });
1457
+
1458
+ it('should validate set_output_format output schema', async () => {
1459
+ if (testConfig.skipE2ETests) return;
1460
+
1461
+ const result = await executeToolCall(server, 'ynab:set_output_format', {
1462
+ minify: false,
1463
+ });
1464
+ const validation = validateOutputSchema(server, 'set_output_format', result);
1465
+ expect(validation.hasSchema).toBe(true);
1466
+ expect(validation.valid).toBe(true);
1467
+ if (!validation.valid) {
1468
+ console.error('Schema validation errors:', validation.errors);
1469
+ }
1470
+
1471
+ // Reset to default
1472
+ await executeToolCall(server, 'ynab:set_output_format', {
1473
+ minify: true,
1474
+ });
1475
+ });
1476
+
1477
+ it('should validate reconcile_account output schema', async () => {
1478
+ if (testConfig.skipE2ETests) return;
1479
+
1480
+ const result = await executeToolCall(server, 'ynab:reconcile_account', {
1481
+ budget_id: testBudgetId,
1482
+ account_id: testAccountId,
1483
+ cleared_balance: 0,
1484
+ });
1485
+ const validation = validateOutputSchema(server, 'reconcile_account', result);
1486
+ expect(validation.hasSchema).toBe(true);
1487
+ expect(validation.valid).toBe(true);
1488
+ if (!validation.valid) {
1489
+ console.error('Schema validation errors:', validation.errors);
1490
+ }
1491
+ });
1492
+
1493
+ it('should validate create_transactions (bulk) output schema', async () => {
1494
+ if (testConfig.skipE2ETests) return;
1495
+
1496
+ // Create multiple transactions
1497
+ const transactions = [
1498
+ {
1499
+ account_id: testAccountId,
1500
+ date: new Date().toISOString().split('T')[0],
1501
+ amount: -1000,
1502
+ payee_name: `Test Payee 1 ${Date.now()}`,
1503
+ memo: 'Bulk test 1',
1504
+ cleared: 'uncleared' as const,
1505
+ },
1506
+ {
1507
+ account_id: testAccountId,
1508
+ date: new Date().toISOString().split('T')[0],
1509
+ amount: -2000,
1510
+ payee_name: `Test Payee 2 ${Date.now()}`,
1511
+ memo: 'Bulk test 2',
1512
+ cleared: 'uncleared' as const,
1513
+ },
1514
+ ];
1515
+
1516
+ const result = await executeToolCall(server, 'ynab:create_transactions', {
1517
+ budget_id: testBudgetId,
1518
+ transactions,
1519
+ });
1520
+ const validation = validateOutputSchema(server, 'create_transactions', result);
1521
+ expect(validation.hasSchema).toBe(true);
1522
+ expect(validation.valid).toBe(true);
1523
+ if (!validation.valid) {
1524
+ console.error('Schema validation errors:', validation.errors);
1525
+ }
1526
+
1527
+ // Track transactions for cleanup
1528
+ const parsed = parseToolResult(result);
1529
+ if (parsed.data?.transactions) {
1530
+ parsed.data.transactions.forEach((txn: any) => {
1531
+ cleanup.trackTransaction(txn.id);
1532
+ });
1533
+ }
1534
+ });
1535
+
1536
+ it('should validate update_transactions (bulk) output schema', async () => {
1537
+ if (testConfig.skipE2ETests) return;
1538
+
1539
+ // First create a transaction to update
1540
+ const createResult = await executeToolCall(server, 'ynab:create_transaction', {
1541
+ budget_id: testBudgetId,
1542
+ account_id: testAccountId,
1543
+ date: new Date().toISOString().split('T')[0],
1544
+ amount: -3000,
1545
+ payee_name: `Test Update Payee ${Date.now()}`,
1546
+ memo: 'Before update',
1547
+ cleared: 'uncleared',
1548
+ });
1549
+ const created = parseToolResult(createResult);
1550
+ const transactionId = created.data.transaction.id;
1551
+ cleanup.trackTransaction(transactionId);
1552
+
1553
+ // Update the transaction
1554
+ const result = await executeToolCall(server, 'ynab:update_transactions', {
1555
+ budget_id: testBudgetId,
1556
+ transactions: [
1557
+ {
1558
+ id: transactionId,
1559
+ memo: 'After update',
1560
+ },
1561
+ ],
1562
+ });
1563
+ const validation = validateOutputSchema(server, 'update_transactions', result);
1564
+ expect(validation.hasSchema).toBe(true);
1565
+ expect(validation.valid).toBe(true);
1566
+ if (!validation.valid) {
1567
+ console.error('Schema validation errors:', validation.errors);
1568
+ }
1569
+ });
1570
+
1571
+ it('should validate compare_transactions output schema', async () => {
1572
+ if (testConfig.skipE2ETests) return;
1573
+
1574
+ // Create a minimal CSV for comparison
1575
+ const csvData = `Date,Payee,Amount\n2025-01-15,Test Payee,-10.00`;
1576
+
1577
+ const result = await executeToolCall(server, 'ynab:compare_transactions', {
1578
+ budget_id: testBudgetId,
1579
+ account_id: testAccountId,
1580
+ csv_data: csvData,
1581
+ start_date: '2025-01-01',
1582
+ end_date: '2025-01-31',
1583
+ });
1584
+ const validation = validateOutputSchema(server, 'compare_transactions', result);
1585
+ expect(validation.hasSchema).toBe(true);
1586
+ expect(validation.valid).toBe(true);
1587
+ if (!validation.valid) {
1588
+ console.error('Schema validation errors:', validation.errors);
1589
+ }
1590
+ });
1591
+
1592
+ it('should validate convert_amount output schema', async () => {
1593
+ if (testConfig.skipE2ETests) return;
1594
+
1595
+ const result = await executeToolCall(server, 'ynab:convert_amount', {
1596
+ amount: 100,
1597
+ to_milliunits: true,
1598
+ });
1599
+ const validation = validateOutputSchema(server, 'convert_amount', result);
1600
+ expect(validation.hasSchema).toBe(true);
1601
+ expect(validation.valid).toBe(true);
1602
+ if (!validation.valid) {
1603
+ console.error('Schema validation errors:', validation.errors);
1604
+ }
1605
+ });
1606
+
1607
+ it('should validate export_transactions output schema', async () => {
1608
+ if (testConfig.skipE2ETests) return;
1609
+
1610
+ const result = await executeToolCall(server, 'ynab:export_transactions', {
1611
+ budget_id: testBudgetId,
1612
+ account_id: testAccountId,
1613
+ });
1614
+ const validation = validateOutputSchema(server, 'export_transactions', result);
1615
+ expect(validation.hasSchema).toBe(true);
1616
+ expect(validation.valid).toBe(true);
1617
+ if (!validation.valid) {
1618
+ console.error('Schema validation errors:', validation.errors);
1619
+ }
1620
+ });
1621
+
1622
+ it('should validate create_receipt_split_transaction output schema', async () => {
1623
+ if (testConfig.skipE2ETests) return;
1624
+
1625
+ // Get categories to find a valid one
1626
+ const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
1627
+ budget_id: testBudgetId,
1628
+ });
1629
+ const categories = parseToolResult(categoriesResult);
1630
+
1631
+ // Find a non-hidden category
1632
+ let testCategoryName: string | undefined;
1633
+ for (const group of categories.data.category_groups) {
1634
+ const availableCategory = group.categories?.find((cat: any) => !cat.hidden);
1635
+ if (availableCategory) {
1636
+ testCategoryName = availableCategory.name;
1637
+ break;
1638
+ }
1639
+ }
1640
+
1641
+ if (!testCategoryName) {
1642
+ console.warn('No available categories found for create_receipt_split_transaction test');
1643
+ return;
1644
+ }
1645
+
1646
+ // Create a minimal receipt split transaction
1647
+ const result = await executeToolCall(server, 'ynab:create_receipt_split_transaction', {
1648
+ budget_id: testBudgetId,
1649
+ account_id: testAccountId,
1650
+ date: new Date().toISOString().split('T')[0],
1651
+ payee_name: `Test Receipt ${Date.now()}`,
1652
+ tax_amount: 100,
1653
+ receipt_items: [
1654
+ {
1655
+ category_name: testCategoryName,
1656
+ amount: 1000,
1657
+ },
1658
+ ],
1659
+ });
1660
+
1661
+ const validation = validateOutputSchema(server, 'create_receipt_split_transaction', result);
1662
+ expect(validation.hasSchema).toBe(true);
1663
+ expect(validation.valid).toBe(true);
1664
+ if (!validation.valid) {
1665
+ console.error('Schema validation errors:', validation.errors);
1666
+ }
1667
+
1668
+ // Track the created transaction for cleanup
1669
+ const parsed = parseToolResult(result);
1670
+ if (parsed.data?.transaction?.id) {
1671
+ cleanup.trackTransaction(parsed.data.transaction.id);
1672
+ }
1673
+ });
1674
+ });
1675
+ });