@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.
- package/.chunkhound.json +11 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
- package/.dxtignore +57 -0
- package/.env.example +44 -0
- package/.gemini/settings.json +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
- package/.github/pull_request_template.md +41 -0
- package/.github/workflows/ci-tests.yml +41 -0
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/full-integration.yml +22 -0
- package/.github/workflows/pr-description-check.yml +88 -0
- package/.github/workflows/publish.yml +33 -0
- package/.github/workflows/release.yml +89 -0
- package/.mcpbignore +58 -0
- package/.prettierignore +10 -0
- package/.prettierrc.json +10 -0
- package/ADOS-2-Module-1-Complete-Manual.md +757 -0
- package/AGENTS.md +36 -0
- package/CHANGELOG.md +187 -0
- package/CLAUDE.md +414 -0
- package/CODEREVIEW_RESPONSE.md +128 -0
- package/LICENSE +17 -0
- package/NUL +1 -0
- package/README.md +222 -0
- package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
- package/TESTING_NOTES.md +217 -0
- package/WARP.md +245 -0
- package/accountactivity-merged.csv +149 -0
- package/bin/ynab-mcp-server.cjs +4 -0
- package/bin/ynab-mcp-server.js +8 -0
- package/bundle-analysis.html +13110 -0
- package/dist/bundle/index.cjs +124 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +85 -0
- package/dist/server/YNABMCPServer.d.ts +264 -0
- package/dist/server/YNABMCPServer.js +845 -0
- package/dist/server/budgetResolver.d.ts +15 -0
- package/dist/server/budgetResolver.js +99 -0
- package/dist/server/cacheManager.d.ts +74 -0
- package/dist/server/cacheManager.js +306 -0
- package/dist/server/config.d.ts +3 -0
- package/dist/server/config.js +19 -0
- package/dist/server/deltaCache.d.ts +61 -0
- package/dist/server/deltaCache.js +206 -0
- package/dist/server/deltaCache.merge.d.ts +9 -0
- package/dist/server/deltaCache.merge.js +111 -0
- package/dist/server/diagnostics.d.ts +90 -0
- package/dist/server/diagnostics.js +163 -0
- package/dist/server/errorHandler.d.ts +69 -0
- package/dist/server/errorHandler.js +524 -0
- package/dist/server/prompts.d.ts +31 -0
- package/dist/server/prompts.js +205 -0
- package/dist/server/rateLimiter.d.ts +27 -0
- package/dist/server/rateLimiter.js +82 -0
- package/dist/server/requestLogger.d.ts +62 -0
- package/dist/server/requestLogger.js +190 -0
- package/dist/server/resources.d.ts +39 -0
- package/dist/server/resources.js +85 -0
- package/dist/server/responseFormatter.d.ts +14 -0
- package/dist/server/responseFormatter.js +42 -0
- package/dist/server/securityMiddleware.d.ts +87 -0
- package/dist/server/securityMiddleware.js +117 -0
- package/dist/server/serverKnowledgeStore.d.ts +11 -0
- package/dist/server/serverKnowledgeStore.js +42 -0
- package/dist/server/toolRegistry.d.ts +85 -0
- package/dist/server/toolRegistry.js +272 -0
- package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
- package/dist/tools/__tests__/deltaTestUtils.js +26 -0
- package/dist/tools/accountTools.d.ts +37 -0
- package/dist/tools/accountTools.js +175 -0
- package/dist/tools/budgetTools.d.ts +10 -0
- package/dist/tools/budgetTools.js +68 -0
- package/dist/tools/categoryTools.d.ts +27 -0
- package/dist/tools/categoryTools.js +232 -0
- package/dist/tools/compareTransactions/formatter.d.ts +71 -0
- package/dist/tools/compareTransactions/formatter.js +97 -0
- package/dist/tools/compareTransactions/index.d.ts +30 -0
- package/dist/tools/compareTransactions/index.js +160 -0
- package/dist/tools/compareTransactions/matcher.d.ts +12 -0
- package/dist/tools/compareTransactions/matcher.js +140 -0
- package/dist/tools/compareTransactions/parser.d.ts +14 -0
- package/dist/tools/compareTransactions/parser.js +430 -0
- package/dist/tools/compareTransactions/types.d.ts +27 -0
- package/dist/tools/compareTransactions/types.js +1 -0
- package/dist/tools/compareTransactions.d.ts +1 -0
- package/dist/tools/compareTransactions.js +1 -0
- package/dist/tools/deltaFetcher.d.ts +22 -0
- package/dist/tools/deltaFetcher.js +137 -0
- package/dist/tools/deltaSupport.d.ts +20 -0
- package/dist/tools/deltaSupport.js +176 -0
- package/dist/tools/exportTransactions.d.ts +17 -0
- package/dist/tools/exportTransactions.js +191 -0
- package/dist/tools/monthTools.d.ts +16 -0
- package/dist/tools/monthTools.js +107 -0
- package/dist/tools/payeeTools.d.ts +17 -0
- package/dist/tools/payeeTools.js +82 -0
- package/dist/tools/reconcileAdapter.d.ts +25 -0
- package/dist/tools/reconcileAdapter.js +167 -0
- package/dist/tools/reconciliation/analyzer.d.ts +3 -0
- package/dist/tools/reconciliation/analyzer.js +567 -0
- package/dist/tools/reconciliation/executor.d.ts +94 -0
- package/dist/tools/reconciliation/executor.js +611 -0
- package/dist/tools/reconciliation/index.d.ts +54 -0
- package/dist/tools/reconciliation/index.js +249 -0
- package/dist/tools/reconciliation/matcher.d.ts +3 -0
- package/dist/tools/reconciliation/matcher.js +160 -0
- package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
- package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
- package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
- package/dist/tools/reconciliation/recommendationEngine.js +273 -0
- package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
- package/dist/tools/reconciliation/reportFormatter.js +214 -0
- package/dist/tools/reconciliation/types.d.ts +172 -0
- package/dist/tools/reconciliation/types.js +7 -0
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
- package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
- package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
- package/dist/tools/schemas/outputs/index.d.ts +14 -0
- package/dist/tools/schemas/outputs/index.js +14 -0
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
- package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
- package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
- package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
- package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
- package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
- package/dist/tools/schemas/shared/commonOutputs.js +27 -0
- package/dist/tools/toolCategories.d.ts +32 -0
- package/dist/tools/toolCategories.js +32 -0
- package/dist/tools/transactionTools.d.ts +315 -0
- package/dist/tools/transactionTools.js +1722 -0
- package/dist/tools/utilityTools.d.ts +10 -0
- package/dist/tools/utilityTools.js +56 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.js +16 -0
- package/dist/types/toolAnnotations.d.ts +7 -0
- package/dist/types/toolAnnotations.js +1 -0
- package/dist/utils/amountUtils.d.ts +3 -0
- package/dist/utils/amountUtils.js +10 -0
- package/dist/utils/dateUtils.d.ts +9 -0
- package/dist/utils/dateUtils.js +43 -0
- package/dist/utils/money.d.ts +21 -0
- package/dist/utils/money.js +51 -0
- package/docs/README.md +72 -0
- package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
- package/docs/assets/schemas/reconciliation-v2.json +338 -0
- package/docs/getting-started/CONFIGURATION.md +175 -0
- package/docs/getting-started/INSTALLATION.md +333 -0
- package/docs/getting-started/QUICKSTART.md +282 -0
- package/docs/guides/ARCHITECTURE.md +650 -0
- package/docs/guides/DEPLOYMENT.md +189 -0
- package/docs/guides/INTEGRATION_TESTING.md +730 -0
- package/docs/guides/TESTING.md +591 -0
- package/docs/reconciliation-flow.md +83 -0
- package/docs/reference/API.md +1450 -0
- package/docs/reference/EXAMPLES.md +946 -0
- package/docs/reference/TOOLS.md +348 -0
- package/docs/reference/TROUBLESHOOTING.md +481 -0
- package/esbuild.config.mjs +68 -0
- package/eslint.config.js +49 -0
- package/fix-types.sh +17 -0
- package/meta.json +12550 -0
- package/package.json +105 -0
- package/package.json.tmp +105 -0
- package/scripts/analyze-bundle.mjs +41 -0
- package/scripts/create-pr-description.js +203 -0
- package/scripts/generate-mcpb.ps1 +96 -0
- package/scripts/run-domain-integration-tests.js +33 -0
- package/scripts/run-generate-mcpb.js +29 -0
- package/scripts/run-throttled-integration-tests.js +116 -0
- package/scripts/test-delta-params.mjs +140 -0
- package/scripts/test-recommendations.ts +53 -0
- package/scripts/tmpTransaction.ts +48 -0
- package/scripts/validate-env.js +122 -0
- package/scripts/verify-build.js +105 -0
- package/scripts/watch-and-restart.ps1 +50 -0
- package/src/__tests__/comprehensive.integration.test.ts +1196 -0
- package/src/__tests__/delta.performance.test.ts +80 -0
- package/src/__tests__/performance.test.ts +725 -0
- package/src/__tests__/setup.ts +449 -0
- package/src/__tests__/testRunner.ts +444 -0
- package/src/__tests__/testUtils.ts +563 -0
- package/src/__tests__/workflows.e2e.test.ts +1675 -0
- package/src/index.ts +124 -0
- package/src/server/.gitkeep +1 -0
- package/src/server/YNABMCPServer.ts +1188 -0
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
- package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
- package/src/server/__tests__/budgetResolver.test.ts +425 -0
- package/src/server/__tests__/cacheManager.test.ts +880 -0
- package/src/server/__tests__/config.test.ts +166 -0
- package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
- package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
- package/src/server/__tests__/deltaCache.test.ts +774 -0
- package/src/server/__tests__/diagnostics.test.ts +823 -0
- package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
- package/src/server/__tests__/errorHandler.test.ts +416 -0
- package/src/server/__tests__/prompts.test.ts +354 -0
- package/src/server/__tests__/rateLimiter.test.ts +314 -0
- package/src/server/__tests__/requestLogger.test.ts +408 -0
- package/src/server/__tests__/resources.test.ts +299 -0
- package/src/server/__tests__/security.integration.test.ts +426 -0
- package/src/server/__tests__/securityMiddleware.test.ts +449 -0
- package/src/server/__tests__/server-startup.integration.test.ts +477 -0
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
- package/src/server/__tests__/toolRegistry.test.ts +855 -0
- package/src/server/budgetResolver.ts +235 -0
- package/src/server/cacheManager.ts +503 -0
- package/src/server/config.ts +41 -0
- package/src/server/deltaCache.merge.ts +149 -0
- package/src/server/deltaCache.ts +341 -0
- package/src/server/diagnostics.ts +338 -0
- package/src/server/errorHandler.ts +756 -0
- package/src/server/prompts.ts +291 -0
- package/src/server/rateLimiter.ts +156 -0
- package/src/server/requestLogger.ts +344 -0
- package/src/server/resources.ts +168 -0
- package/src/server/responseFormatter.ts +51 -0
- package/src/server/securityMiddleware.ts +236 -0
- package/src/server/serverKnowledgeStore.ts +91 -0
- package/src/server/toolRegistry.ts +489 -0
- package/src/tools/.gitkeep +1 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
- package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
- package/src/tools/__tests__/accountTools.test.ts +653 -0
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
- package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
- package/src/tools/__tests__/budgetTools.test.ts +423 -0
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
- package/src/tools/__tests__/categoryTools.test.ts +622 -0
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
- package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
- package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
- package/src/tools/__tests__/compareTransactions.test.ts +342 -0
- package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
- package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
- package/src/tools/__tests__/deltaSupport.test.ts +188 -0
- package/src/tools/__tests__/deltaTestUtils.ts +46 -0
- package/src/tools/__tests__/exportTransactions.test.ts +213 -0
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
- package/src/tools/__tests__/monthTools.test.ts +523 -0
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
- package/src/tools/__tests__/payeeTools.test.ts +445 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
- package/src/tools/__tests__/transactionTools.test.ts +3521 -0
- package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
- package/src/tools/__tests__/utilityTools.test.ts +205 -0
- package/src/tools/accountTools.ts +283 -0
- package/src/tools/budgetTools.ts +112 -0
- package/src/tools/categoryTools.ts +366 -0
- package/src/tools/compareTransactions/formatter.ts +163 -0
- package/src/tools/compareTransactions/index.ts +228 -0
- package/src/tools/compareTransactions/matcher.ts +240 -0
- package/src/tools/compareTransactions/parser.ts +557 -0
- package/src/tools/compareTransactions/types.ts +60 -0
- package/src/tools/compareTransactions.ts +3 -0
- package/src/tools/deltaFetcher.ts +278 -0
- package/src/tools/deltaSupport.ts +293 -0
- package/src/tools/exportTransactions.ts +273 -0
- package/src/tools/monthTools.ts +164 -0
- package/src/tools/payeeTools.ts +140 -0
- package/src/tools/reconcileAdapter.ts +312 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
- package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
- package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
- package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
- package/src/tools/reconciliation/analyzer.ts +824 -0
- package/src/tools/reconciliation/executor.ts +880 -0
- package/src/tools/reconciliation/index.ts +400 -0
- package/src/tools/reconciliation/matcher.ts +269 -0
- package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
- package/src/tools/reconciliation/recommendationEngine.ts +506 -0
- package/src/tools/reconciliation/reportFormatter.ts +363 -0
- package/src/tools/reconciliation/types.ts +314 -0
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
- package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
- package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
- package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
- package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
- package/src/tools/schemas/outputs/index.ts +270 -0
- package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
- package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
- package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
- package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
- package/src/tools/schemas/shared/commonOutputs.ts +140 -0
- package/src/tools/toolCategories.ts +140 -0
- package/src/tools/transactionTools.ts +2509 -0
- package/src/tools/utilityTools.ts +90 -0
- package/src/types/.gitkeep +1 -0
- package/src/types/__tests__/index.test.ts +52 -0
- package/src/types/index.ts +67 -0
- package/src/types/integration-tests.d.ts +35 -0
- package/src/types/toolAnnotations.ts +44 -0
- package/src/utils/__tests__/dateUtils.test.ts +170 -0
- package/src/utils/__tests__/money.test.ts +189 -0
- package/src/utils/amountUtils.ts +32 -0
- package/src/utils/dateUtils.ts +108 -0
- package/src/utils/money.ts +123 -0
- package/test-csv-sample.csv +28 -0
- package/test-exports/sample_bank_statement.csv +7 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
- package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
- package/test-reconcile-autodetect.js +40 -0
- package/test-reconcile-tool.js +152 -0
- package/test-reconcile-with-csv.cjs +89 -0
- package/test-statement.csv +8 -0
- package/test_debug.js +47 -0
- package/test_simple.mjs +16 -0
- package/tsconfig.json +31 -0
- package/tsconfig.prod.json +18 -0
- package/vitest-reporters/split-json-reporter.ts +211 -0
- package/vitest.config.ts +96 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities for comprehensive testing suite
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { expect } from 'vitest';
|
|
6
|
+
import { YNABMCPServer } from '../server/YNABMCPServer.js';
|
|
7
|
+
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test environment configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface TestConfig {
|
|
14
|
+
hasRealApiKey: boolean;
|
|
15
|
+
testBudgetId: string | undefined;
|
|
16
|
+
testAccountId: string | undefined;
|
|
17
|
+
skipE2ETests: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get test configuration from environment
|
|
22
|
+
*/
|
|
23
|
+
export function getTestConfig(): TestConfig {
|
|
24
|
+
const hasRealApiKey = !!process.env['YNAB_ACCESS_TOKEN'];
|
|
25
|
+
const skipE2ETests = process.env['SKIP_E2E_TESTS'] === 'true' || !hasRealApiKey;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
hasRealApiKey,
|
|
29
|
+
testBudgetId: process.env['TEST_BUDGET_ID'],
|
|
30
|
+
testAccountId: process.env['TEST_ACCOUNT_ID'],
|
|
31
|
+
skipE2ETests,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a test server instance
|
|
37
|
+
*/
|
|
38
|
+
export async function createTestServer(): Promise<YNABMCPServer> {
|
|
39
|
+
if (!process.env['YNAB_ACCESS_TOKEN']) {
|
|
40
|
+
throw new Error('YNAB_ACCESS_TOKEN is required for testing');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new YNABMCPServer();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Execute a named tool through the server's tool registry.
|
|
48
|
+
*
|
|
49
|
+
* @param toolName - The tool identifier to run; a leading `ynab:` prefix will be removed if present.
|
|
50
|
+
* @param args - Optional arguments to pass to the tool.
|
|
51
|
+
* @returns The tool's raw execution result as a `CallToolResult`.
|
|
52
|
+
* @throws Error if the `YNAB_ACCESS_TOKEN` environment variable is not set.
|
|
53
|
+
*/
|
|
54
|
+
export async function executeToolCall(
|
|
55
|
+
server: YNABMCPServer,
|
|
56
|
+
toolName: string,
|
|
57
|
+
args: Record<string, any> = {},
|
|
58
|
+
): Promise<CallToolResult> {
|
|
59
|
+
const accessToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
60
|
+
if (!accessToken) {
|
|
61
|
+
throw new Error('YNAB_ACCESS_TOKEN is required for tool execution');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const registry = server.getToolRegistry();
|
|
65
|
+
const normalizedName = toolName.startsWith('ynab:')
|
|
66
|
+
? toolName.slice(toolName.indexOf(':') + 1)
|
|
67
|
+
: toolName;
|
|
68
|
+
|
|
69
|
+
return await registry.executeTool({
|
|
70
|
+
name: normalizedName,
|
|
71
|
+
accessToken,
|
|
72
|
+
arguments: args,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Asserts that a CallToolResult contains a non-empty `content` array composed of text items.
|
|
78
|
+
*
|
|
79
|
+
* Verifies the result and its `content` are defined, that `content` is a non-empty array,
|
|
80
|
+
* and that every item in the array has `type` equal to `'text'` and a `text` property of type `string`.
|
|
81
|
+
*
|
|
82
|
+
* @param result - The CallToolResult to validate
|
|
83
|
+
*/
|
|
84
|
+
export function validateToolResult(result: CallToolResult): void {
|
|
85
|
+
expect(result).toBeDefined();
|
|
86
|
+
expect(result.content).toBeDefined();
|
|
87
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
88
|
+
expect(result.content.length).toBeGreaterThan(0);
|
|
89
|
+
|
|
90
|
+
for (const content of result.content) {
|
|
91
|
+
if (content.type === 'text') {
|
|
92
|
+
expect(typeof content.text).toBe('string');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Determines whether a tool call result represents an error.
|
|
99
|
+
*
|
|
100
|
+
* Inspects the first content item (must be of type `text`) and treats the result as an error
|
|
101
|
+
* if that text parses to a JSON object containing an `error` property.
|
|
102
|
+
*
|
|
103
|
+
* @returns `true` if the first text content parses as a JSON object with an `error` field, `false` otherwise.
|
|
104
|
+
*/
|
|
105
|
+
export function isErrorResult(result: CallToolResult): boolean {
|
|
106
|
+
if (!result.content || result.content.length === 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const content = result.content[0];
|
|
111
|
+
if (!content || content.type !== 'text') {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(content.text);
|
|
117
|
+
return parsed && typeof parsed === 'object' && 'error' in parsed;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extracts a human-readable error message from a CallToolResult when the result contains an error.
|
|
125
|
+
*
|
|
126
|
+
* @returns A human-readable error message extracted from `result` (falls back to the raw text), or an empty string if no error message is available.
|
|
127
|
+
*/
|
|
128
|
+
export function getErrorMessage(result: CallToolResult): string {
|
|
129
|
+
if (!isErrorResult(result)) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const content = result.content[0];
|
|
134
|
+
if (!content || content.type !== 'text') {
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(content.text);
|
|
140
|
+
const error = parsed?.error;
|
|
141
|
+
if (typeof error === 'string' && error.length > 0) {
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
if (error && typeof error === 'object') {
|
|
145
|
+
const { message, userMessage, details, suggestions, name } = error as Record<string, unknown>;
|
|
146
|
+
|
|
147
|
+
let errorMessage = '';
|
|
148
|
+
if (typeof message === 'string' && message.length > 0) {
|
|
149
|
+
errorMessage = message;
|
|
150
|
+
} else if (typeof userMessage === 'string' && userMessage.length > 0) {
|
|
151
|
+
errorMessage = userMessage;
|
|
152
|
+
} else if (typeof name === 'string' && name.length > 0) {
|
|
153
|
+
errorMessage = name;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Include details if available
|
|
157
|
+
if (typeof details === 'string' && details.length > 0) {
|
|
158
|
+
errorMessage += `\n\n${details}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Include suggestions if available
|
|
162
|
+
if (Array.isArray(suggestions) && suggestions.length > 0) {
|
|
163
|
+
const suggestionsText = suggestions
|
|
164
|
+
.filter((s) => typeof s === 'string')
|
|
165
|
+
.map((s, i) => `${i + 1}. ${s}`)
|
|
166
|
+
.join('\n');
|
|
167
|
+
if (suggestionsText) {
|
|
168
|
+
errorMessage += `\n\nSuggestions:\n${suggestionsText}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (errorMessage) return errorMessage;
|
|
173
|
+
}
|
|
174
|
+
return content.text;
|
|
175
|
+
} catch {
|
|
176
|
+
return content.text;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse and normalize JSON payload from a CallToolResult's text content.
|
|
182
|
+
*
|
|
183
|
+
* @param result - The tool call result whose first content item must be a text string containing JSON.
|
|
184
|
+
* @returns If the parsed JSON is an object with a `data` property, returns that object (adding `success: true` if missing). If the parsed JSON is an object without `data`, returns `{ success: true, data: <parsed> }`. If the parsed JSON is a non-object or array, returns the parsed value directly.
|
|
185
|
+
* @throws If the result has no text content, the text is not a string, or the text cannot be parsed as JSON.
|
|
186
|
+
*/
|
|
187
|
+
export function parseToolResult<T = any>(result: CallToolResult): T {
|
|
188
|
+
validateToolResult(result);
|
|
189
|
+
const content = result.content[0];
|
|
190
|
+
if (!content || content.type !== 'text') {
|
|
191
|
+
throw new Error('No text content in tool result');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const text = content.text;
|
|
195
|
+
if (typeof text !== 'string') {
|
|
196
|
+
throw new Error('Tool result text is not a string');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(text) as Record<string, unknown> | T;
|
|
201
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
202
|
+
const record = parsed as Record<string, unknown>;
|
|
203
|
+
|
|
204
|
+
// Handle backward compatibility - ensure both success and data properties exist
|
|
205
|
+
if ('data' in record) {
|
|
206
|
+
// Response already has data property, add success if missing
|
|
207
|
+
if (!('success' in record)) {
|
|
208
|
+
return { success: true, ...record } as T;
|
|
209
|
+
}
|
|
210
|
+
return parsed as T;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Response doesn't have data property, wrap it and add success
|
|
214
|
+
return { success: true, data: parsed } as T;
|
|
215
|
+
}
|
|
216
|
+
return parsed as T;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Failed to parse tool result as JSON: ${error}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validates a tool result against its registered output schema.
|
|
224
|
+
*
|
|
225
|
+
* This helper enables e2e tests to verify that tool responses match their
|
|
226
|
+
* declared output schemas without duplicating schema definitions.
|
|
227
|
+
*
|
|
228
|
+
* @param server - The YNAB MCP server instance
|
|
229
|
+
* @param toolName - Name of the tool to validate
|
|
230
|
+
* @param result - The CallToolResult from the tool execution
|
|
231
|
+
* @returns Validation result object containing valid flag, hasSchema flag, errors array, and parsed data
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* const result = await executeToolCall(server, 'list_budgets', {});
|
|
236
|
+
* const validation = validateOutputSchema(server, 'list_budgets', result);
|
|
237
|
+
* expect(validation.hasSchema).toBe(true);
|
|
238
|
+
* expect(validation.valid).toBe(true);
|
|
239
|
+
* if (!validation.valid) {
|
|
240
|
+
* console.error('Schema validation errors:', validation.errors);
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export function validateOutputSchema(
|
|
245
|
+
server: YNABMCPServer,
|
|
246
|
+
toolName: string,
|
|
247
|
+
result: CallToolResult,
|
|
248
|
+
): { valid: boolean; hasSchema: boolean; errors?: string[]; data?: unknown; note?: string } {
|
|
249
|
+
// Get tool definitions from registry
|
|
250
|
+
const registry = server.getToolRegistry();
|
|
251
|
+
const toolDefinitions = registry.getToolDefinitions();
|
|
252
|
+
const toolDef = toolDefinitions.find((t) => t.name === toolName);
|
|
253
|
+
|
|
254
|
+
if (!toolDef) {
|
|
255
|
+
return {
|
|
256
|
+
valid: false,
|
|
257
|
+
hasSchema: false,
|
|
258
|
+
errors: [`Tool '${toolName}' not found in registry for schema validation`],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!toolDef.outputSchema) {
|
|
263
|
+
return {
|
|
264
|
+
valid: true,
|
|
265
|
+
hasSchema: false,
|
|
266
|
+
note: `Tool '${toolName}' does not define an outputSchema (schemas are optional)`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Parse JSON response from result's text content
|
|
271
|
+
let parsedData: unknown;
|
|
272
|
+
try {
|
|
273
|
+
const textContent = result.content.find((c) => c.type === 'text');
|
|
274
|
+
if (!textContent || textContent.type !== 'text') {
|
|
275
|
+
return {
|
|
276
|
+
valid: false,
|
|
277
|
+
hasSchema: true,
|
|
278
|
+
errors: ['Result does not contain text content'],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
parsedData = JSON.parse(textContent.text);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return {
|
|
284
|
+
valid: false,
|
|
285
|
+
hasSchema: true,
|
|
286
|
+
errors: [`Failed to parse result as JSON: ${error}`],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Validate against output schema
|
|
291
|
+
const validationResult = toolDef.outputSchema.safeParse(parsedData);
|
|
292
|
+
|
|
293
|
+
if (!validationResult.success) {
|
|
294
|
+
// Extract detailed error messages from Zod errors
|
|
295
|
+
const zodError = validationResult.error as z.ZodError;
|
|
296
|
+
const errors = zodError.issues.map((err: z.ZodIssue) => {
|
|
297
|
+
const path = err.path.join('.');
|
|
298
|
+
return `${path ? path + ': ' : ''}${err.message}`;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
valid: false,
|
|
303
|
+
hasSchema: true,
|
|
304
|
+
errors,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
valid: true,
|
|
310
|
+
hasSchema: true,
|
|
311
|
+
data: validationResult.data,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Wait for a condition to be true
|
|
317
|
+
*/
|
|
318
|
+
export async function waitFor(
|
|
319
|
+
condition: () => boolean | Promise<boolean>,
|
|
320
|
+
timeout: number = 5000,
|
|
321
|
+
interval: number = 100,
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
const start = Date.now();
|
|
324
|
+
|
|
325
|
+
while (Date.now() - start < timeout) {
|
|
326
|
+
if (await condition()) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
throw new Error(`Condition not met within ${timeout}ms`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generate test data
|
|
337
|
+
*/
|
|
338
|
+
export const TestData = {
|
|
339
|
+
/**
|
|
340
|
+
* Generate a unique test account name
|
|
341
|
+
*/
|
|
342
|
+
generateAccountName(): string {
|
|
343
|
+
return `Test Account ${Date.now()}`;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Generate a test transaction
|
|
348
|
+
*/
|
|
349
|
+
generateTransaction(accountId: string, categoryId?: string) {
|
|
350
|
+
return {
|
|
351
|
+
account_id: accountId,
|
|
352
|
+
category_id: categoryId,
|
|
353
|
+
payee_name: `Test Payee ${Date.now()}`,
|
|
354
|
+
amount: -5000, // $5.00 outflow
|
|
355
|
+
memo: `Test transaction ${Date.now()}`,
|
|
356
|
+
date: new Date().toISOString().split('T')[0], // Today's date
|
|
357
|
+
cleared: 'uncleared' as const,
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Generate test amounts in milliunits
|
|
363
|
+
*/
|
|
364
|
+
generateAmount(dollars: number): number {
|
|
365
|
+
return Math.round(dollars * 1000);
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Test data cleanup utilities
|
|
371
|
+
*/
|
|
372
|
+
export class TestDataCleanup {
|
|
373
|
+
private createdAccountIds: string[] = [];
|
|
374
|
+
private createdTransactionIds: string[] = [];
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Track created account for cleanup
|
|
378
|
+
*/
|
|
379
|
+
trackAccount(accountId: string): void {
|
|
380
|
+
this.createdAccountIds.push(accountId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Track created transaction for cleanup
|
|
385
|
+
*/
|
|
386
|
+
trackTransaction(transactionId: string): void {
|
|
387
|
+
this.createdTransactionIds.push(transactionId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Clean up all tracked test data
|
|
392
|
+
*/
|
|
393
|
+
async cleanup(server: YNABMCPServer, budgetId: string): Promise<void> {
|
|
394
|
+
// Clean up transactions first (they depend on accounts)
|
|
395
|
+
for (const transactionId of this.createdTransactionIds) {
|
|
396
|
+
try {
|
|
397
|
+
await executeToolCall(server, 'ynab:delete_transaction', {
|
|
398
|
+
budget_id: budgetId,
|
|
399
|
+
transaction_id: transactionId,
|
|
400
|
+
});
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn(`Failed to cleanup transaction ${transactionId}:`, error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Note: YNAB API doesn't support deleting accounts via API
|
|
407
|
+
// Accounts created during testing will need manual cleanup
|
|
408
|
+
if (this.createdAccountIds.length > 0) {
|
|
409
|
+
console.warn(
|
|
410
|
+
`Created ${this.createdAccountIds.length} test accounts that need manual cleanup:`,
|
|
411
|
+
this.createdAccountIds,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.createdAccountIds = [];
|
|
416
|
+
this.createdTransactionIds = [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Assertion helpers for YNAB data
|
|
422
|
+
*/
|
|
423
|
+
export const YNABAssertions = {
|
|
424
|
+
/**
|
|
425
|
+
* Assert budget structure
|
|
426
|
+
*/
|
|
427
|
+
assertBudget(budget: any): void {
|
|
428
|
+
expect(budget).toBeDefined();
|
|
429
|
+
expect(typeof budget.id).toBe('string');
|
|
430
|
+
expect(typeof budget.name).toBe('string');
|
|
431
|
+
expect(typeof budget.last_modified_on).toBe('string');
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Assert account structure
|
|
436
|
+
*/
|
|
437
|
+
assertAccount(account: any): void {
|
|
438
|
+
expect(account).toBeDefined();
|
|
439
|
+
expect(typeof account.id).toBe('string');
|
|
440
|
+
expect(typeof account.name).toBe('string');
|
|
441
|
+
expect(typeof account.type).toBe('string');
|
|
442
|
+
expect(typeof account.on_budget).toBe('boolean');
|
|
443
|
+
expect(typeof account.closed).toBe('boolean');
|
|
444
|
+
expect(typeof account.balance).toBe('number');
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Assert transaction structure
|
|
449
|
+
*/
|
|
450
|
+
assertTransaction(transaction: any): void {
|
|
451
|
+
expect(transaction).toBeDefined();
|
|
452
|
+
expect(typeof transaction.id).toBe('string');
|
|
453
|
+
expect(typeof transaction.date).toBe('string');
|
|
454
|
+
expect(typeof transaction.amount).toBe('number');
|
|
455
|
+
expect(typeof transaction.account_id).toBe('string');
|
|
456
|
+
expect(['cleared', 'uncleared', 'reconciled']).toContain(transaction.cleared);
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Assert category structure
|
|
461
|
+
*/
|
|
462
|
+
assertCategory(category: any): void {
|
|
463
|
+
expect(category).toBeDefined();
|
|
464
|
+
expect(typeof category.id).toBe('string');
|
|
465
|
+
expect(typeof category.name).toBe('string');
|
|
466
|
+
expect(typeof category.category_group_id).toBe('string');
|
|
467
|
+
expect(typeof category.budgeted).toBe('number');
|
|
468
|
+
expect(typeof category.activity).toBe('number');
|
|
469
|
+
expect(typeof category.balance).toBe('number');
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Assert payee structure
|
|
474
|
+
*/
|
|
475
|
+
assertPayee(payee: any): void {
|
|
476
|
+
expect(payee).toBeDefined();
|
|
477
|
+
expect(typeof payee.id).toBe('string');
|
|
478
|
+
expect(typeof payee.name).toBe('string');
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Determine whether a value represents a rate-limit (HTTP 429 / "too many requests") error.
|
|
483
|
+
*
|
|
484
|
+
* Inspects common error shapes and messages to identify rate-limit responses.
|
|
485
|
+
*
|
|
486
|
+
* @param error - The error value to inspect (may be a string, Error, or an object with status/statusCode/error fields)
|
|
487
|
+
* @returns `true` if the provided value represents a rate limit error, `false` otherwise.
|
|
488
|
+
*/
|
|
489
|
+
export function isRateLimitError(error: any): boolean {
|
|
490
|
+
if (!error) return false;
|
|
491
|
+
|
|
492
|
+
// Check various ways rate limit errors can appear
|
|
493
|
+
const errorString = error.toString ? error.toString().toLowerCase() : String(error).toLowerCase();
|
|
494
|
+
const hasRateLimitMessage =
|
|
495
|
+
errorString.includes('rate limit') ||
|
|
496
|
+
errorString.includes('too many requests') ||
|
|
497
|
+
errorString.includes('429');
|
|
498
|
+
|
|
499
|
+
// Check for HTML responses (YNAB API returns HTML when rate limited or down)
|
|
500
|
+
// This manifests as JSON parsing errors with messages like:
|
|
501
|
+
// "SyntaxError: Unexpected token '<', "<style>..." is not valid JSON"
|
|
502
|
+
const isHTMLResponse =
|
|
503
|
+
(errorString.includes('syntaxerror') || errorString.includes('unexpected token')) &&
|
|
504
|
+
(errorString.includes("'<'") ||
|
|
505
|
+
errorString.includes('"<"') ||
|
|
506
|
+
errorString.includes('<style') ||
|
|
507
|
+
errorString.includes('not valid json'));
|
|
508
|
+
|
|
509
|
+
// Check for VALIDATION_ERROR from output schema validation failures
|
|
510
|
+
// These occur when YNAB API returns error responses instead of data during rate limiting
|
|
511
|
+
// Example: {"code":"VALIDATION_ERROR","message":"Output validation failed for list_budgets",...}
|
|
512
|
+
const isValidationError =
|
|
513
|
+
errorString.includes('validation_error') || errorString.includes('output validation failed');
|
|
514
|
+
|
|
515
|
+
// Check error object properties
|
|
516
|
+
if (error && typeof error === 'object') {
|
|
517
|
+
const statusCode = error.status || error.statusCode || error.error?.id;
|
|
518
|
+
if (statusCode === 429 || statusCode === '429') return true;
|
|
519
|
+
|
|
520
|
+
const errorName = error.name || error.error?.name || '';
|
|
521
|
+
if (errorName.toLowerCase().includes('too_many_requests')) return true;
|
|
522
|
+
|
|
523
|
+
// Check nested error objects
|
|
524
|
+
if (error.error && typeof error.error === 'object') {
|
|
525
|
+
const nestedId = error.error.id;
|
|
526
|
+
const nestedName = error.error.name;
|
|
527
|
+
if (nestedId === '429' || nestedName === 'too_many_requests') return true;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return hasRateLimitMessage || isHTMLResponse || isValidationError;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Runs a test function and skips the test if a YNAB API rate limit error occurs.
|
|
536
|
+
*
|
|
537
|
+
* @param testFn - The test code to execute.
|
|
538
|
+
* @param context - Optional test context providing a `skip()` method; if present, it will be called when a rate limit is detected.
|
|
539
|
+
* @returns The value returned by `testFn` or `undefined` if the test was skipped due to a rate limit.
|
|
540
|
+
*/
|
|
541
|
+
export async function skipOnRateLimit<T>(
|
|
542
|
+
testFn: () => Promise<T>,
|
|
543
|
+
context?: { skip: () => void },
|
|
544
|
+
): Promise<T | undefined> {
|
|
545
|
+
try {
|
|
546
|
+
return await testFn();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (isRateLimitError(error)) {
|
|
549
|
+
// Log the skip reason
|
|
550
|
+
console.warn('⏭️ Skipping test due to YNAB API rate limit');
|
|
551
|
+
|
|
552
|
+
// Skip the test if context is provided
|
|
553
|
+
if (context?.skip) {
|
|
554
|
+
context.skip();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Return void to satisfy type system
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// Re-throw non-rate-limit errors
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
}
|