@dizzlkheinz/ynab-mcpb 0.18.4 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +87 -8
- package/bin/ynab-mcp-server.cjs +2 -2
- package/bin/ynab-mcp-server.js +3 -3
- package/biome.json +39 -0
- package/dist/bundle/index.cjs +67 -67
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -27
- package/dist/server/YNABMCPServer.d.ts +3 -4
- package/dist/server/YNABMCPServer.js +111 -116
- package/dist/server/budgetResolver.d.ts +6 -5
- package/dist/server/budgetResolver.js +46 -36
- package/dist/server/cacheKeys.js +6 -6
- package/dist/server/cacheManager.js +14 -11
- package/dist/server/completions.d.ts +2 -2
- package/dist/server/completions.js +20 -15
- package/dist/server/config.d.ts +10 -5
- package/dist/server/config.js +24 -7
- package/dist/server/deltaCache.d.ts +2 -2
- package/dist/server/deltaCache.js +22 -16
- package/dist/server/deltaCache.merge.d.ts +2 -2
- package/dist/server/diagnostics.d.ts +4 -4
- package/dist/server/diagnostics.js +38 -32
- package/dist/server/errorHandler.d.ts +5 -12
- package/dist/server/errorHandler.js +219 -217
- package/dist/server/prompts.d.ts +2 -2
- package/dist/server/prompts.js +45 -45
- package/dist/server/rateLimiter.js +4 -4
- package/dist/server/requestLogger.d.ts +1 -1
- package/dist/server/requestLogger.js +40 -35
- package/dist/server/resources.d.ts +3 -3
- package/dist/server/resources.js +55 -52
- package/dist/server/responseFormatter.js +6 -6
- package/dist/server/securityMiddleware.d.ts +2 -2
- package/dist/server/securityMiddleware.js +22 -20
- package/dist/server/serverKnowledgeStore.js +1 -1
- package/dist/server/toolRegistry.d.ts +3 -3
- package/dist/server/toolRegistry.js +47 -40
- package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
- package/dist/tools/__tests__/deltaTestUtils.js +2 -2
- package/dist/tools/accountTools.d.ts +9 -8
- package/dist/tools/accountTools.js +47 -47
- package/dist/tools/adapters.d.ts +13 -8
- package/dist/tools/adapters.js +21 -11
- package/dist/tools/budgetTools.d.ts +8 -7
- package/dist/tools/budgetTools.js +22 -22
- package/dist/tools/categoryTools.d.ts +9 -8
- package/dist/tools/categoryTools.js +68 -59
- package/dist/tools/compareTransactions/formatter.d.ts +3 -3
- package/dist/tools/compareTransactions/formatter.js +9 -9
- package/dist/tools/compareTransactions/index.d.ts +6 -6
- package/dist/tools/compareTransactions/index.js +58 -43
- package/dist/tools/compareTransactions/matcher.d.ts +1 -1
- package/dist/tools/compareTransactions/matcher.js +28 -15
- package/dist/tools/compareTransactions/parser.d.ts +2 -2
- package/dist/tools/compareTransactions/parser.js +144 -138
- package/dist/tools/compareTransactions/types.d.ts +4 -4
- package/dist/tools/compareTransactions.d.ts +1 -1
- package/dist/tools/compareTransactions.js +1 -1
- package/dist/tools/deltaFetcher.d.ts +2 -2
- package/dist/tools/deltaFetcher.js +16 -15
- package/dist/tools/deltaSupport.d.ts +4 -4
- package/dist/tools/deltaSupport.js +35 -41
- package/dist/tools/exportTransactions.d.ts +5 -4
- package/dist/tools/exportTransactions.js +61 -59
- package/dist/tools/monthTools.d.ts +7 -6
- package/dist/tools/monthTools.js +31 -29
- package/dist/tools/payeeTools.d.ts +7 -6
- package/dist/tools/payeeTools.js +28 -28
- package/dist/tools/reconcileAdapter.d.ts +2 -2
- package/dist/tools/reconcileAdapter.js +19 -12
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +73 -59
- package/dist/tools/reconciliation/csvParser.d.ts +3 -3
- package/dist/tools/reconciliation/csvParser.js +128 -104
- package/dist/tools/reconciliation/executor.d.ts +4 -4
- package/dist/tools/reconciliation/executor.js +148 -109
- package/dist/tools/reconciliation/index.d.ts +10 -10
- package/dist/tools/reconciliation/index.js +96 -83
- package/dist/tools/reconciliation/matcher.d.ts +3 -3
- package/dist/tools/reconciliation/matcher.js +17 -16
- package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
- package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
- package/dist/tools/reconciliation/recommendationEngine.js +40 -40
- package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
- package/dist/tools/reconciliation/reportFormatter.js +59 -58
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +16 -16
- package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
- package/dist/tools/schemas/common.d.ts +1 -1
- package/dist/tools/schemas/common.js +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
- package/dist/tools/schemas/outputs/index.d.ts +14 -14
- package/dist/tools/schemas/outputs/index.js +14 -14
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
- package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
- package/dist/tools/schemas/shared/commonOutputs.js +15 -9
- package/dist/tools/transactionReadTools.d.ts +11 -0
- package/dist/tools/transactionReadTools.js +202 -0
- package/dist/tools/transactionSchemas.d.ts +7 -7
- package/dist/tools/transactionSchemas.js +77 -57
- package/dist/tools/transactionTools.d.ts +6 -24
- package/dist/tools/transactionTools.js +7 -1499
- package/dist/tools/transactionUtils.d.ts +6 -6
- package/dist/tools/transactionUtils.js +78 -63
- package/dist/tools/transactionWriteTools.d.ts +20 -0
- package/dist/tools/transactionWriteTools.js +1342 -0
- package/dist/tools/utilityTools.d.ts +5 -4
- package/dist/tools/utilityTools.js +11 -11
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +6 -6
- package/dist/types/reconciliation.d.ts +1 -1
- package/dist/types/toolRegistration.d.ts +14 -12
- package/dist/utils/amountUtils.js +1 -1
- package/dist/utils/dateUtils.js +4 -4
- package/dist/utils/errors.d.ts +3 -3
- package/dist/utils/errors.js +4 -4
- package/dist/utils/money.d.ts +2 -2
- package/dist/utils/money.js +8 -8
- package/dist/utils/validationError.d.ts +1 -1
- package/dist/utils/validationError.js +1 -1
- package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
- package/docs/assets/schemas/reconciliation-v2.json +360 -336
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -111
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +178 -169
- package/scripts/run-domain-integration-tests.js +28 -18
- package/scripts/run-generate-mcpb.js +19 -17
- package/scripts/run-throttled-integration-tests.js +92 -83
- package/scripts/test-delta-params.mjs +149 -120
- package/scripts/test-recommendations.ts +36 -32
- package/scripts/tmpTransaction.ts +80 -43
- package/scripts/validate-env.js +98 -91
- package/scripts/verify-build.js +78 -76
- package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
- package/src/__tests__/performance.test.ts +723 -671
- package/src/__tests__/setup.ts +442 -395
- package/src/__tests__/smoke.e2e.test.ts +41 -39
- package/src/__tests__/testRunner.ts +314 -295
- package/src/__tests__/testUtils.ts +456 -364
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
- package/src/index.ts +68 -59
- package/src/server/CLAUDE.md +480 -0
- package/src/server/YNABMCPServer.ts +821 -794
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
- package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
- package/src/server/__tests__/budgetResolver.test.ts +466 -423
- package/src/server/__tests__/cacheManager.test.ts +891 -874
- package/src/server/__tests__/completions.integration.test.ts +115 -106
- package/src/server/__tests__/completions.test.ts +334 -313
- package/src/server/__tests__/config.test.ts +98 -86
- package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
- package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
- package/src/server/__tests__/deltaCache.test.ts +946 -759
- package/src/server/__tests__/diagnostics.test.ts +825 -792
- package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
- package/src/server/__tests__/errorHandler.test.ts +402 -397
- package/src/server/__tests__/prompts.test.ts +424 -347
- package/src/server/__tests__/rateLimiter.test.ts +313 -309
- package/src/server/__tests__/requestLogger.test.ts +443 -403
- package/src/server/__tests__/resources.template.test.ts +196 -185
- package/src/server/__tests__/resources.test.ts +294 -288
- package/src/server/__tests__/security.integration.test.ts +487 -421
- package/src/server/__tests__/securityMiddleware.test.ts +519 -444
- package/src/server/__tests__/server-startup.integration.test.ts +509 -490
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
- package/src/server/__tests__/toolRegistration.test.ts +239 -210
- package/src/server/__tests__/toolRegistry.test.ts +907 -845
- package/src/server/budgetResolver.ts +221 -181
- package/src/server/cacheKeys.ts +6 -6
- package/src/server/cacheManager.ts +498 -484
- package/src/server/completions.ts +267 -243
- package/src/server/config.ts +35 -14
- package/src/server/deltaCache.merge.ts +146 -128
- package/src/server/deltaCache.ts +352 -309
- package/src/server/diagnostics.ts +257 -242
- package/src/server/errorHandler.ts +747 -744
- package/src/server/prompts.ts +181 -176
- package/src/server/rateLimiter.ts +131 -129
- package/src/server/requestLogger.ts +350 -322
- package/src/server/resources.ts +442 -374
- package/src/server/responseFormatter.ts +41 -37
- package/src/server/securityMiddleware.ts +223 -205
- package/src/server/serverKnowledgeStore.ts +67 -67
- package/src/server/toolRegistry.ts +508 -474
- package/src/tools/CLAUDE.md +604 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
- package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
- package/src/tools/__tests__/accountTools.test.ts +685 -638
- package/src/tools/__tests__/adapters.test.ts +142 -108
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
- package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
- package/src/tools/__tests__/budgetTools.test.ts +442 -413
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
- package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
- package/src/tools/__tests__/categoryTools.test.ts +656 -625
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
- package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
- package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
- package/src/tools/__tests__/compareTransactions.test.ts +352 -332
- package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
- package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
- package/src/tools/__tests__/deltaSupport.test.ts +211 -184
- package/src/tools/__tests__/deltaTestUtils.ts +37 -33
- package/src/tools/__tests__/exportTransactions.test.ts +205 -200
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
- package/src/tools/__tests__/monthTools.test.ts +561 -512
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
- package/src/tools/__tests__/payeeTools.test.ts +486 -434
- package/src/tools/__tests__/transactionSchemas.test.ts +1202 -1186
- package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
- package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
- package/src/tools/__tests__/transactionUtils.test.ts +1004 -977
- package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
- package/src/tools/__tests__/utilityTools.test.ts +68 -58
- package/src/tools/accountTools.ts +293 -271
- package/src/tools/adapters.ts +120 -63
- package/src/tools/budgetTools.ts +121 -116
- package/src/tools/categoryTools.ts +379 -339
- package/src/tools/compareTransactions/formatter.ts +131 -119
- package/src/tools/compareTransactions/index.ts +249 -214
- package/src/tools/compareTransactions/matcher.ts +259 -209
- package/src/tools/compareTransactions/parser.ts +517 -487
- package/src/tools/compareTransactions/types.ts +38 -38
- package/src/tools/compareTransactions.ts +1 -1
- package/src/tools/deltaFetcher.ts +281 -260
- package/src/tools/deltaSupport.ts +264 -259
- package/src/tools/exportTransactions.ts +230 -218
- package/src/tools/monthTools.ts +180 -165
- package/src/tools/payeeTools.ts +152 -140
- package/src/tools/reconcileAdapter.ts +297 -252
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +133 -124
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -230
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -400
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
- package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
- package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -989
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -533
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -74
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -62
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +56 -55
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
- package/src/tools/reconciliation/analyzer.ts +564 -504
- package/src/tools/reconciliation/csvParser.ts +656 -609
- package/src/tools/reconciliation/executor.ts +1290 -1128
- package/src/tools/reconciliation/index.ts +580 -528
- package/src/tools/reconciliation/matcher.ts +256 -240
- package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
- package/src/tools/reconciliation/recommendationEngine.ts +357 -345
- package/src/tools/reconciliation/reportFormatter.ts +343 -307
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -159
- package/src/tools/reconciliation/ynabAdapter.ts +17 -15
- package/src/tools/schemas/CLAUDE.md +546 -0
- package/src/tools/schemas/common.ts +1 -1
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
- package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
- package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
- package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
- package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
- package/src/tools/schemas/outputs/index.ts +163 -163
- package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
- package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
- package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
- package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
- package/src/tools/schemas/shared/commonOutputs.ts +27 -19
- package/src/tools/toolCategories.ts +114 -114
- package/src/tools/transactionReadTools.ts +327 -0
- package/src/tools/transactionSchemas.ts +322 -291
- package/src/tools/transactionTools.ts +84 -2246
- package/src/tools/transactionUtils.ts +507 -422
- package/src/tools/transactionWriteTools.ts +2110 -0
- package/src/tools/utilityTools.ts +46 -41
- package/src/types/CLAUDE.md +477 -0
- package/src/types/__tests__/index.test.ts +51 -51
- package/src/types/index.ts +43 -39
- package/src/types/integration-tests.d.ts +26 -26
- package/src/types/reconciliation.ts +29 -29
- package/src/types/toolAnnotations.ts +30 -30
- package/src/types/toolRegistration.ts +43 -32
- package/src/utils/CLAUDE.md +508 -0
- package/src/utils/__tests__/dateUtils.test.ts +174 -168
- package/src/utils/__tests__/money.test.ts +193 -187
- package/src/utils/amountUtils.ts +5 -5
- package/src/utils/baseError.ts +5 -5
- package/src/utils/dateUtils.ts +29 -26
- package/src/utils/errors.ts +14 -14
- package/src/utils/money.ts +66 -52
- package/src/utils/validationError.ts +1 -1
- package/tsconfig.json +29 -29
- package/tsconfig.prod.json +16 -16
- package/vitest-reporters/split-json-reporter.ts +247 -204
- package/vitest.config.ts +99 -95
- package/.prettierignore +0 -10
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -49
|
@@ -3,1166 +3,1293 @@
|
|
|
3
3
|
* These tests use mocked YNAB API responses to test complete workflows
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import {
|
|
7
|
+
afterAll,
|
|
8
|
+
beforeAll,
|
|
9
|
+
beforeEach,
|
|
10
|
+
describe,
|
|
11
|
+
expect,
|
|
12
|
+
it,
|
|
13
|
+
vi,
|
|
14
|
+
} from "vitest";
|
|
15
|
+
import { YNABMCPServer } from "../server/YNABMCPServer.js";
|
|
16
|
+
import { cacheManager } from "../server/cacheManager.js";
|
|
17
|
+
import {
|
|
18
|
+
executeToolCall,
|
|
19
|
+
parseToolResult,
|
|
20
|
+
validateToolResult,
|
|
21
|
+
waitFor,
|
|
22
|
+
} from "./testUtils.js";
|
|
10
23
|
|
|
11
24
|
// Mock the YNAB SDK
|
|
12
|
-
vi.mock(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
25
|
+
vi.mock("ynab", () => {
|
|
26
|
+
const mockAPI = {
|
|
27
|
+
budgets: {
|
|
28
|
+
getBudgets: vi.fn(),
|
|
29
|
+
getBudgetById: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
accounts: {
|
|
32
|
+
getAccounts: vi.fn(),
|
|
33
|
+
getAccountById: vi.fn(),
|
|
34
|
+
createAccount: vi.fn(),
|
|
35
|
+
},
|
|
36
|
+
transactions: {
|
|
37
|
+
getTransactions: vi.fn(),
|
|
38
|
+
getTransactionsByAccount: vi.fn(),
|
|
39
|
+
getTransactionsByCategory: vi.fn(),
|
|
40
|
+
getTransactionById: vi.fn(),
|
|
41
|
+
createTransaction: vi.fn(),
|
|
42
|
+
updateTransaction: vi.fn(),
|
|
43
|
+
deleteTransaction: vi.fn(),
|
|
44
|
+
},
|
|
45
|
+
categories: {
|
|
46
|
+
getCategories: vi.fn(),
|
|
47
|
+
getCategoryById: vi.fn(),
|
|
48
|
+
updateMonthCategory: vi.fn(),
|
|
49
|
+
},
|
|
50
|
+
payees: {
|
|
51
|
+
getPayees: vi.fn(),
|
|
52
|
+
getPayeeById: vi.fn(),
|
|
53
|
+
},
|
|
54
|
+
months: {
|
|
55
|
+
getBudgetMonth: vi.fn(),
|
|
56
|
+
getBudgetMonths: vi.fn(),
|
|
57
|
+
},
|
|
58
|
+
user: {
|
|
59
|
+
getUser: vi.fn(),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
API: vi.fn(() => mockAPI),
|
|
65
|
+
utils: {
|
|
66
|
+
convertMilliUnitsToCurrencyAmount: vi.fn(
|
|
67
|
+
(milliunits: number, currencyDecimalDigits = 2) => {
|
|
68
|
+
const amount = milliunits / 1000;
|
|
69
|
+
return Number(amount.toFixed(currencyDecimalDigits));
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
convertCurrencyAmountToMilliUnits: vi.fn((amount: number) =>
|
|
73
|
+
Math.round(amount * 1000),
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
62
77
|
});
|
|
63
78
|
|
|
64
|
-
const TEST_BUDGET_UUID =
|
|
79
|
+
const TEST_BUDGET_UUID = "00000000-0000-0000-0000-000000000001";
|
|
65
80
|
|
|
66
|
-
describe(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
describe("YNAB utils mock", () => {
|
|
82
|
+
it(
|
|
83
|
+
"converts milliunits using SDK rounding rules",
|
|
84
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
85
|
+
async () => {
|
|
86
|
+
const { utils } = await import("ynab");
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
expect(utils.convertMilliUnitsToCurrencyAmount(123456, 2)).toBe(123.46);
|
|
89
|
+
expect(utils.convertMilliUnitsToCurrencyAmount(123456, 3)).toBe(123.456);
|
|
90
|
+
expect(utils.convertMilliUnitsToCurrencyAmount(-98765, 2)).toBe(-98.77);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
78
93
|
});
|
|
79
94
|
|
|
80
|
-
describe(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
95
|
+
describe("YNAB MCP Server - Comprehensive Integration Tests", () => {
|
|
96
|
+
let server: YNABMCPServer;
|
|
97
|
+
let mockYnabAPI: any;
|
|
98
|
+
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
// Set up environment
|
|
101
|
+
process.env.YNAB_ACCESS_TOKEN = "test-token";
|
|
102
|
+
|
|
103
|
+
// Create server instance
|
|
104
|
+
server = new YNABMCPServer();
|
|
105
|
+
|
|
106
|
+
// Get the mocked YNAB API instance
|
|
107
|
+
const { API } = await import("ynab");
|
|
108
|
+
mockYnabAPI = new (API as any)();
|
|
109
|
+
|
|
110
|
+
// Reset all mocks
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("Complete Budget Management Integration", () => {
|
|
115
|
+
it(
|
|
116
|
+
"should handle complete budget listing and retrieval workflow",
|
|
117
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
118
|
+
async () => {
|
|
119
|
+
// Mock budget list response
|
|
120
|
+
const mockBudgets = {
|
|
121
|
+
data: {
|
|
122
|
+
budgets: [
|
|
123
|
+
{
|
|
124
|
+
id: "budget-1",
|
|
125
|
+
name: "Test Budget 1",
|
|
126
|
+
last_modified_on: "2024-01-01T00:00:00Z",
|
|
127
|
+
first_month: "2024-01-01",
|
|
128
|
+
last_month: "2024-12-01",
|
|
129
|
+
date_format: { format: "MM/DD/YYYY" },
|
|
130
|
+
currency_format: {
|
|
131
|
+
iso_code: "USD",
|
|
132
|
+
example_format: "$123.45",
|
|
133
|
+
decimal_digits: 2,
|
|
134
|
+
decimal_separator: ".",
|
|
135
|
+
symbol_first: true,
|
|
136
|
+
group_separator: ",",
|
|
137
|
+
currency_symbol: "$",
|
|
138
|
+
display_symbol: true,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "budget-2",
|
|
143
|
+
name: "Test Budget 2",
|
|
144
|
+
last_modified_on: "2024-01-02T00:00:00Z",
|
|
145
|
+
first_month: "2024-01-01",
|
|
146
|
+
last_month: "2024-12-01",
|
|
147
|
+
date_format: { format: "MM/DD/YYYY" },
|
|
148
|
+
currency_format: {
|
|
149
|
+
iso_code: "USD",
|
|
150
|
+
example_format: "$123.45",
|
|
151
|
+
decimal_digits: 2,
|
|
152
|
+
decimal_separator: ".",
|
|
153
|
+
symbol_first: true,
|
|
154
|
+
group_separator: ",",
|
|
155
|
+
currency_symbol: "$",
|
|
156
|
+
display_symbol: true,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
mockYnabAPI.budgets.getBudgets.mockResolvedValue(mockBudgets);
|
|
164
|
+
|
|
165
|
+
// Test budget listing
|
|
166
|
+
const listResult = await executeToolCall(server, "ynab:list_budgets");
|
|
167
|
+
validateToolResult(listResult);
|
|
168
|
+
|
|
169
|
+
const budgets = parseToolResult(listResult);
|
|
170
|
+
expect(budgets.data.budgets).toHaveLength(2);
|
|
171
|
+
expect(budgets.data.budgets[0].name).toBe("Test Budget 1");
|
|
172
|
+
expect(budgets.data.budgets[1].name).toBe("Test Budget 2");
|
|
173
|
+
|
|
174
|
+
// Mock specific budget response
|
|
175
|
+
const mockBudget = {
|
|
176
|
+
data: {
|
|
177
|
+
budget: {
|
|
178
|
+
id: "budget-1",
|
|
179
|
+
name: "Test Budget 1",
|
|
180
|
+
last_modified_on: "2024-01-01T00:00:00Z",
|
|
181
|
+
first_month: "2024-01-01",
|
|
182
|
+
last_month: "2024-12-01",
|
|
183
|
+
date_format: { format: "MM/DD/YYYY" },
|
|
184
|
+
currency_format: {
|
|
185
|
+
iso_code: "USD",
|
|
186
|
+
example_format: "$123.45",
|
|
187
|
+
decimal_digits: 2,
|
|
188
|
+
decimal_separator: ".",
|
|
189
|
+
symbol_first: true,
|
|
190
|
+
group_separator: ",",
|
|
191
|
+
currency_symbol: "$",
|
|
192
|
+
display_symbol: true,
|
|
193
|
+
},
|
|
194
|
+
accounts: [],
|
|
195
|
+
payees: [],
|
|
196
|
+
category_groups: [],
|
|
197
|
+
months: [],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
mockYnabAPI.budgets.getBudgetById.mockResolvedValue(mockBudget);
|
|
203
|
+
|
|
204
|
+
// Test specific budget retrieval
|
|
205
|
+
const getResult = await executeToolCall(server, "ynab:get_budget", {
|
|
206
|
+
budget_id: "budget-1",
|
|
207
|
+
});
|
|
208
|
+
validateToolResult(getResult);
|
|
209
|
+
|
|
210
|
+
const budget = parseToolResult(getResult);
|
|
211
|
+
expect(budget.data.budget.id).toBe("budget-1");
|
|
212
|
+
expect(budget.data.budget.name).toBe("Test Budget 1");
|
|
213
|
+
|
|
214
|
+
// Verify API calls
|
|
215
|
+
expect(mockYnabAPI.budgets.getBudgets).toHaveBeenCalledTimes(1);
|
|
216
|
+
expect(mockYnabAPI.budgets.getBudgetById).toHaveBeenCalledWith(
|
|
217
|
+
"budget-1",
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
it(
|
|
223
|
+
"should handle budget retrieval errors gracefully",
|
|
224
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
225
|
+
async () => {
|
|
226
|
+
// Mock API error
|
|
227
|
+
const apiError = new Error("Budget not found");
|
|
228
|
+
(apiError as any).error = {
|
|
229
|
+
id: "404.2",
|
|
230
|
+
name: "not_found",
|
|
231
|
+
description: "Budget not found",
|
|
232
|
+
};
|
|
233
|
+
mockYnabAPI.budgets.getBudgetById.mockRejectedValue(apiError);
|
|
234
|
+
|
|
235
|
+
// Test error handling
|
|
236
|
+
try {
|
|
237
|
+
await executeToolCall(server, "ynab:get_budget", {
|
|
238
|
+
budget_id: "invalid-budget",
|
|
239
|
+
});
|
|
240
|
+
expect.fail("Should have thrown an error");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
expect(error).toBeDefined();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
expect(mockYnabAPI.budgets.getBudgetById).toHaveBeenCalledWith(
|
|
246
|
+
"invalid-budget",
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("Complete Account Management Integration", () => {
|
|
253
|
+
it(
|
|
254
|
+
"should handle complete account workflow",
|
|
255
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
256
|
+
async () => {
|
|
257
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
258
|
+
|
|
259
|
+
// Mock accounts list
|
|
260
|
+
const mockAccounts = {
|
|
261
|
+
data: {
|
|
262
|
+
accounts: [
|
|
263
|
+
{
|
|
264
|
+
id: "account-1",
|
|
265
|
+
name: "Checking Account",
|
|
266
|
+
type: "checking",
|
|
267
|
+
on_budget: true,
|
|
268
|
+
closed: false,
|
|
269
|
+
balance: 100000, // $100.00
|
|
270
|
+
cleared_balance: 95000,
|
|
271
|
+
uncleared_balance: 5000,
|
|
272
|
+
transfer_payee_id: "payee-transfer-1",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "account-2",
|
|
276
|
+
name: "Savings Account",
|
|
277
|
+
type: "savings",
|
|
278
|
+
on_budget: true,
|
|
279
|
+
closed: false,
|
|
280
|
+
note: "Emergency fund",
|
|
281
|
+
balance: 500000, // $500.00
|
|
282
|
+
cleared_balance: 500000,
|
|
283
|
+
uncleared_balance: 0,
|
|
284
|
+
transfer_payee_id: "payee-transfer-2",
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
mockYnabAPI.accounts.getAccounts.mockResolvedValue(mockAccounts);
|
|
291
|
+
|
|
292
|
+
// Test account listing
|
|
293
|
+
const listResult = await executeToolCall(server, "ynab:list_accounts", {
|
|
294
|
+
budget_id: budgetId,
|
|
295
|
+
});
|
|
296
|
+
validateToolResult(listResult);
|
|
297
|
+
|
|
298
|
+
const accounts = parseToolResult(listResult);
|
|
299
|
+
expect(accounts.data.accounts).toHaveLength(2);
|
|
300
|
+
expect(accounts.data.accounts[0].name).toBe("Checking Account");
|
|
301
|
+
expect(accounts.data.accounts[1].name).toBe("Savings Account");
|
|
302
|
+
|
|
303
|
+
// Mock specific account response
|
|
304
|
+
const mockAccount = {
|
|
305
|
+
data: {
|
|
306
|
+
account: mockAccounts.data.accounts[0],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
mockYnabAPI.accounts.getAccountById.mockResolvedValue(mockAccount);
|
|
311
|
+
|
|
312
|
+
// Test specific account retrieval
|
|
313
|
+
const getResult = await executeToolCall(server, "ynab:get_account", {
|
|
314
|
+
budget_id: budgetId,
|
|
315
|
+
account_id: "account-1",
|
|
316
|
+
});
|
|
317
|
+
validateToolResult(getResult);
|
|
318
|
+
|
|
319
|
+
const account = parseToolResult(getResult);
|
|
320
|
+
expect(account.data.account.id).toBe("account-1");
|
|
321
|
+
expect(account.data.account.name).toBe("Checking Account");
|
|
322
|
+
expect(account.data.account.balance).toBe(100);
|
|
323
|
+
|
|
324
|
+
// Mock account creation
|
|
325
|
+
const newAccount = {
|
|
326
|
+
id: "account-3",
|
|
327
|
+
name: "New Test Account",
|
|
328
|
+
type: "checking",
|
|
329
|
+
on_budget: true,
|
|
330
|
+
closed: false,
|
|
331
|
+
balance: 0,
|
|
332
|
+
cleared_balance: 0,
|
|
333
|
+
uncleared_balance: 0,
|
|
334
|
+
transfer_payee_id: "payee-transfer-3",
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const mockCreateResponse = {
|
|
338
|
+
data: {
|
|
339
|
+
account: newAccount,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
mockYnabAPI.accounts.createAccount.mockResolvedValue(
|
|
344
|
+
mockCreateResponse,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Test account creation
|
|
348
|
+
const createResult = await executeToolCall(
|
|
349
|
+
server,
|
|
350
|
+
"ynab:create_account",
|
|
351
|
+
{
|
|
352
|
+
budget_id: budgetId,
|
|
353
|
+
name: "New Test Account",
|
|
354
|
+
type: "checking",
|
|
355
|
+
balance: 0,
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
validateToolResult(createResult);
|
|
359
|
+
|
|
360
|
+
const createdAccount = parseToolResult(createResult);
|
|
361
|
+
expect(createdAccount.data.account.name).toBe("New Test Account");
|
|
362
|
+
expect(createdAccount.data.account.type).toBe("checking");
|
|
363
|
+
|
|
364
|
+
// Verify API calls
|
|
365
|
+
expect(mockYnabAPI.accounts.getAccounts).toHaveBeenCalledWith(budgetId);
|
|
366
|
+
expect(mockYnabAPI.accounts.getAccountById).toHaveBeenCalledWith(
|
|
367
|
+
budgetId,
|
|
368
|
+
"account-1",
|
|
369
|
+
);
|
|
370
|
+
expect(mockYnabAPI.accounts.createAccount).toHaveBeenCalledWith(
|
|
371
|
+
budgetId,
|
|
372
|
+
{
|
|
373
|
+
account: {
|
|
374
|
+
name: "New Test Account",
|
|
375
|
+
type: "checking",
|
|
376
|
+
balance: 0,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("Complete Transaction Management Integration", () => {
|
|
385
|
+
// TODO: Re-enable after DeltaFetcher cache integration alignment.
|
|
386
|
+
it.skip(
|
|
387
|
+
"should handle complete transaction workflow",
|
|
388
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
389
|
+
async () => {
|
|
390
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
391
|
+
const accountId = "test-account";
|
|
392
|
+
|
|
393
|
+
// Mock transactions list
|
|
394
|
+
const mockTransactions = {
|
|
395
|
+
data: {
|
|
396
|
+
transactions: [
|
|
397
|
+
{
|
|
398
|
+
id: "transaction-1",
|
|
399
|
+
date: "2024-01-15",
|
|
400
|
+
amount: -5000, // $5.00 outflow
|
|
401
|
+
memo: "Coffee shop",
|
|
402
|
+
cleared: "cleared",
|
|
403
|
+
approved: true,
|
|
404
|
+
flag_color: null,
|
|
405
|
+
account_id: accountId,
|
|
406
|
+
payee_id: "payee-1",
|
|
407
|
+
category_id: "category-1",
|
|
408
|
+
transfer_account_id: null,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: "transaction-2",
|
|
412
|
+
date: "2024-01-16",
|
|
413
|
+
amount: 100000, // $100.00 inflow
|
|
414
|
+
memo: "Salary",
|
|
415
|
+
cleared: "cleared",
|
|
416
|
+
approved: true,
|
|
417
|
+
flag_color: null,
|
|
418
|
+
account_id: accountId,
|
|
419
|
+
payee_id: "payee-2",
|
|
420
|
+
category_id: null,
|
|
421
|
+
transfer_account_id: null,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
mockYnabAPI.transactions.getTransactionsByAccount.mockResolvedValue(
|
|
428
|
+
mockTransactions,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Test transaction listing
|
|
432
|
+
const listResult = await executeToolCall(
|
|
433
|
+
server,
|
|
434
|
+
"ynab:list_transactions",
|
|
435
|
+
{
|
|
436
|
+
budget_id: budgetId,
|
|
437
|
+
account_id: accountId,
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
validateToolResult(listResult);
|
|
441
|
+
|
|
442
|
+
const transactions = parseToolResult(listResult);
|
|
443
|
+
expect(transactions.data.transactions).toHaveLength(2);
|
|
444
|
+
expect(transactions.data.transactions[0].memo).toBe("Coffee shop");
|
|
445
|
+
expect(transactions.data.transactions[1].memo).toBe("Salary");
|
|
446
|
+
|
|
447
|
+
// Mock specific transaction response
|
|
448
|
+
const mockTransaction = {
|
|
449
|
+
data: {
|
|
450
|
+
transaction: mockTransactions.data.transactions[0],
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
mockYnabAPI.transactions.getTransactionById.mockResolvedValue(
|
|
455
|
+
mockTransaction,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Test specific transaction retrieval
|
|
459
|
+
const getResult = await executeToolCall(
|
|
460
|
+
server,
|
|
461
|
+
"ynab:get_transaction",
|
|
462
|
+
{
|
|
463
|
+
budget_id: budgetId,
|
|
464
|
+
transaction_id: "transaction-1",
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
validateToolResult(getResult);
|
|
468
|
+
|
|
469
|
+
const transaction = parseToolResult(getResult);
|
|
470
|
+
expect(transaction.data.transaction.id).toBe("transaction-1");
|
|
471
|
+
expect(transaction.data.transaction.memo).toBe("Coffee shop");
|
|
472
|
+
expect(transaction.data.transaction.amount).toBe(-5);
|
|
473
|
+
|
|
474
|
+
// Mock transaction creation
|
|
475
|
+
const newTransaction = {
|
|
476
|
+
id: "transaction-3",
|
|
477
|
+
date: "2024-01-17",
|
|
478
|
+
amount: -2500,
|
|
479
|
+
memo: "Test transaction",
|
|
480
|
+
cleared: "uncleared",
|
|
481
|
+
approved: true,
|
|
482
|
+
flag_color: null,
|
|
483
|
+
account_id: accountId,
|
|
484
|
+
payee_id: null,
|
|
485
|
+
category_id: "category-1",
|
|
486
|
+
transfer_account_id: null,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const mockCreateResponse = {
|
|
490
|
+
data: {
|
|
491
|
+
transaction: newTransaction,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
mockYnabAPI.transactions.createTransaction.mockResolvedValue(
|
|
496
|
+
mockCreateResponse,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// Test transaction creation
|
|
500
|
+
const createResult = await executeToolCall(
|
|
501
|
+
server,
|
|
502
|
+
"ynab:create_transaction",
|
|
503
|
+
{
|
|
504
|
+
budget_id: budgetId,
|
|
505
|
+
account_id: accountId,
|
|
506
|
+
category_id: "category-1",
|
|
507
|
+
payee_name: "Test Payee",
|
|
508
|
+
amount: -2500,
|
|
509
|
+
memo: "Test transaction",
|
|
510
|
+
date: "2024-01-17",
|
|
511
|
+
cleared: "uncleared",
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
validateToolResult(createResult);
|
|
515
|
+
|
|
516
|
+
const createdTransaction = parseToolResult(createResult);
|
|
517
|
+
expect(createdTransaction.data.transaction.memo).toBe(
|
|
518
|
+
"Test transaction",
|
|
519
|
+
);
|
|
520
|
+
expect(createdTransaction.data.transaction.amount).toBe(-2.5);
|
|
521
|
+
|
|
522
|
+
// Mock transaction update
|
|
523
|
+
const updatedTransaction = { ...newTransaction, memo: "Updated memo" };
|
|
524
|
+
const mockUpdateResponse = {
|
|
525
|
+
data: {
|
|
526
|
+
transaction: updatedTransaction,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
mockYnabAPI.transactions.updateTransaction.mockResolvedValue(
|
|
531
|
+
mockUpdateResponse,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Test transaction update
|
|
535
|
+
const updateResult = await executeToolCall(
|
|
536
|
+
server,
|
|
537
|
+
"ynab:update_transaction",
|
|
538
|
+
{
|
|
539
|
+
budget_id: budgetId,
|
|
540
|
+
transaction_id: "transaction-3",
|
|
541
|
+
memo: "Updated memo",
|
|
542
|
+
},
|
|
543
|
+
);
|
|
544
|
+
validateToolResult(updateResult);
|
|
545
|
+
|
|
546
|
+
const updated = parseToolResult(updateResult);
|
|
547
|
+
expect(updated.data.transaction.memo).toBe("Updated memo");
|
|
548
|
+
|
|
549
|
+
// Mock transaction deletion
|
|
550
|
+
mockYnabAPI.transactions.deleteTransaction.mockResolvedValue({
|
|
551
|
+
data: {
|
|
552
|
+
transaction: { ...updatedTransaction, deleted: true },
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Test transaction deletion
|
|
557
|
+
const deleteResult = await executeToolCall(
|
|
558
|
+
server,
|
|
559
|
+
"ynab:delete_transaction",
|
|
560
|
+
{
|
|
561
|
+
budget_id: budgetId,
|
|
562
|
+
transaction_id: "transaction-3",
|
|
563
|
+
},
|
|
564
|
+
);
|
|
565
|
+
validateToolResult(deleteResult);
|
|
566
|
+
|
|
567
|
+
// Verify API calls
|
|
568
|
+
expect(
|
|
569
|
+
mockYnabAPI.transactions.getTransactionsByAccount,
|
|
570
|
+
).toHaveBeenCalledWith(budgetId, accountId, undefined);
|
|
571
|
+
expect(
|
|
572
|
+
mockYnabAPI.transactions.getTransactionById,
|
|
573
|
+
).toHaveBeenCalledWith(budgetId, "transaction-1");
|
|
574
|
+
expect(mockYnabAPI.transactions.createTransaction).toHaveBeenCalled();
|
|
575
|
+
expect(mockYnabAPI.transactions.updateTransaction).toHaveBeenCalled();
|
|
576
|
+
expect(mockYnabAPI.transactions.deleteTransaction).toHaveBeenCalledWith(
|
|
577
|
+
budgetId,
|
|
578
|
+
"transaction-3",
|
|
579
|
+
);
|
|
580
|
+
},
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
it(
|
|
584
|
+
"should handle transaction filtering",
|
|
585
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
586
|
+
async () => {
|
|
587
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
588
|
+
|
|
589
|
+
// Mock filtered transactions
|
|
590
|
+
mockYnabAPI.transactions.getTransactions.mockResolvedValue({
|
|
591
|
+
data: {
|
|
592
|
+
transactions: [
|
|
593
|
+
{
|
|
594
|
+
id: "filtered-transaction",
|
|
595
|
+
date: "2024-01-15",
|
|
596
|
+
amount: -1000,
|
|
597
|
+
memo: "Filtered transaction",
|
|
598
|
+
cleared: "cleared",
|
|
599
|
+
approved: true,
|
|
600
|
+
account_id: "account-1",
|
|
601
|
+
category_id: "category-1",
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Test filtering by date
|
|
608
|
+
const dateFilterResult = await executeToolCall(
|
|
609
|
+
server,
|
|
610
|
+
"ynab:list_transactions",
|
|
611
|
+
{
|
|
612
|
+
budget_id: budgetId,
|
|
613
|
+
since_date: "2024-01-01",
|
|
614
|
+
},
|
|
615
|
+
);
|
|
616
|
+
validateToolResult(dateFilterResult);
|
|
617
|
+
|
|
618
|
+
// Also mock account/category specific endpoints
|
|
619
|
+
mockYnabAPI.transactions.getTransactionsByAccount.mockResolvedValue({
|
|
620
|
+
data: {
|
|
621
|
+
transactions: [
|
|
622
|
+
{
|
|
623
|
+
id: "filtered-transaction",
|
|
624
|
+
date: "2024-01-15",
|
|
625
|
+
amount: -1000,
|
|
626
|
+
memo: "Filtered transaction",
|
|
627
|
+
cleared: "cleared",
|
|
628
|
+
approved: true,
|
|
629
|
+
account_id: "account-1",
|
|
630
|
+
category_id: "category-1",
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
mockYnabAPI.transactions.getTransactionsByCategory.mockResolvedValue({
|
|
636
|
+
data: {
|
|
637
|
+
transactions: [
|
|
638
|
+
{
|
|
639
|
+
id: "filtered-transaction",
|
|
640
|
+
date: "2024-01-15",
|
|
641
|
+
amount: -1000,
|
|
642
|
+
memo: "Filtered transaction",
|
|
643
|
+
cleared: "cleared",
|
|
644
|
+
approved: true,
|
|
645
|
+
account_id: "account-1",
|
|
646
|
+
category_id: "category-1",
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Test filtering by account
|
|
653
|
+
const accountFilterResult = await executeToolCall(
|
|
654
|
+
server,
|
|
655
|
+
"ynab:list_transactions",
|
|
656
|
+
{
|
|
657
|
+
budget_id: budgetId,
|
|
658
|
+
account_id: "account-1",
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
validateToolResult(accountFilterResult);
|
|
662
|
+
|
|
663
|
+
// Test filtering by category
|
|
664
|
+
const categoryFilterResult = await executeToolCall(
|
|
665
|
+
server,
|
|
666
|
+
"ynab:list_transactions",
|
|
667
|
+
{
|
|
668
|
+
budget_id: budgetId,
|
|
669
|
+
category_id: "category-1",
|
|
670
|
+
},
|
|
671
|
+
);
|
|
672
|
+
validateToolResult(categoryFilterResult);
|
|
673
|
+
|
|
674
|
+
// Verify API calls with different parameters
|
|
675
|
+
expect(mockYnabAPI.transactions.getTransactions).toHaveBeenCalledTimes(
|
|
676
|
+
1,
|
|
677
|
+
);
|
|
678
|
+
expect(
|
|
679
|
+
mockYnabAPI.transactions.getTransactionsByAccount,
|
|
680
|
+
).toHaveBeenCalledTimes(1);
|
|
681
|
+
expect(
|
|
682
|
+
mockYnabAPI.transactions.getTransactionsByCategory,
|
|
683
|
+
).toHaveBeenCalledTimes(1);
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
describe("Complete Category Management Integration", () => {
|
|
689
|
+
it(
|
|
690
|
+
"should handle complete category workflow",
|
|
691
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
692
|
+
async () => {
|
|
693
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
694
|
+
|
|
695
|
+
// Mock categories response
|
|
696
|
+
const mockCategories = {
|
|
697
|
+
data: {
|
|
698
|
+
category_groups: [
|
|
699
|
+
{
|
|
700
|
+
id: "group-1",
|
|
701
|
+
name: "Immediate Obligations",
|
|
702
|
+
hidden: false,
|
|
703
|
+
deleted: false,
|
|
704
|
+
categories: [
|
|
705
|
+
{
|
|
706
|
+
id: "category-1",
|
|
707
|
+
category_group_id: "group-1",
|
|
708
|
+
name: "Rent/Mortgage",
|
|
709
|
+
hidden: false,
|
|
710
|
+
budgeted: 150000, // $150.00
|
|
711
|
+
activity: -150000,
|
|
712
|
+
balance: 0,
|
|
713
|
+
// goal_type omitted (undefined, not null)
|
|
714
|
+
deleted: false,
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
id: "category-2",
|
|
718
|
+
category_group_id: "group-1",
|
|
719
|
+
name: "Utilities",
|
|
720
|
+
hidden: false,
|
|
721
|
+
budgeted: 10000, // $10.00
|
|
722
|
+
activity: -8500,
|
|
723
|
+
balance: 1500,
|
|
724
|
+
// goal_type omitted (undefined, not null)
|
|
725
|
+
deleted: false,
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
mockYnabAPI.categories.getCategories.mockResolvedValue(mockCategories);
|
|
734
|
+
|
|
735
|
+
// Test category listing
|
|
736
|
+
const listResult = await executeToolCall(
|
|
737
|
+
server,
|
|
738
|
+
"ynab:list_categories",
|
|
739
|
+
{
|
|
740
|
+
budget_id: budgetId,
|
|
741
|
+
},
|
|
742
|
+
);
|
|
743
|
+
validateToolResult(listResult);
|
|
744
|
+
|
|
745
|
+
const categories = parseToolResult(listResult);
|
|
746
|
+
expect(categories.data.category_groups).toHaveLength(1);
|
|
747
|
+
expect(categories.data.categories).toHaveLength(2);
|
|
748
|
+
expect(categories.data.categories[0].name).toBe("Rent/Mortgage");
|
|
749
|
+
|
|
750
|
+
// Mock specific category response
|
|
751
|
+
const mockCategory = {
|
|
752
|
+
data: {
|
|
753
|
+
category: mockCategories.data.category_groups[0].categories[0],
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
mockYnabAPI.categories.getCategoryById.mockResolvedValue(mockCategory);
|
|
758
|
+
|
|
759
|
+
// Test specific category retrieval
|
|
760
|
+
const getResult = await executeToolCall(server, "ynab:get_category", {
|
|
761
|
+
budget_id: budgetId,
|
|
762
|
+
category_id: "category-1",
|
|
763
|
+
});
|
|
764
|
+
validateToolResult(getResult);
|
|
765
|
+
|
|
766
|
+
const category = parseToolResult(getResult);
|
|
767
|
+
expect(category.data.category.id).toBe("category-1");
|
|
768
|
+
expect(category.data.category.name).toBe("Rent/Mortgage");
|
|
769
|
+
expect(category.data.category.budgeted).toBe(150);
|
|
770
|
+
|
|
771
|
+
// Mock category update
|
|
772
|
+
const updatedCategory = {
|
|
773
|
+
...mockCategories.data.category_groups[0].categories[0],
|
|
774
|
+
budgeted: 160000, // $160.00
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const mockUpdateResponse = {
|
|
778
|
+
data: {
|
|
779
|
+
category: updatedCategory,
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
mockYnabAPI.categories.updateMonthCategory.mockResolvedValue(
|
|
784
|
+
mockUpdateResponse,
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
// Test category budget update
|
|
788
|
+
const updateResult = await executeToolCall(
|
|
789
|
+
server,
|
|
790
|
+
"ynab:update_category",
|
|
791
|
+
{
|
|
792
|
+
budget_id: budgetId,
|
|
793
|
+
category_id: "category-1",
|
|
794
|
+
budgeted: 160000,
|
|
795
|
+
},
|
|
796
|
+
);
|
|
797
|
+
validateToolResult(updateResult);
|
|
798
|
+
|
|
799
|
+
const updated = parseToolResult(updateResult);
|
|
800
|
+
expect(updated.data.category.budgeted).toBe(160);
|
|
801
|
+
|
|
802
|
+
// Verify API calls
|
|
803
|
+
expect(mockYnabAPI.categories.getCategories).toHaveBeenCalledWith(
|
|
804
|
+
budgetId,
|
|
805
|
+
);
|
|
806
|
+
expect(mockYnabAPI.categories.getCategoryById).toHaveBeenCalledWith(
|
|
807
|
+
budgetId,
|
|
808
|
+
"category-1",
|
|
809
|
+
);
|
|
810
|
+
expect(mockYnabAPI.categories.updateMonthCategory).toHaveBeenCalled();
|
|
811
|
+
},
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
describe("Complete Utility Tools Integration", () => {
|
|
816
|
+
it(
|
|
817
|
+
"should handle user information retrieval",
|
|
818
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
819
|
+
async () => {
|
|
820
|
+
// Mock user response
|
|
821
|
+
const mockUser = {
|
|
822
|
+
data: {
|
|
823
|
+
user: {
|
|
824
|
+
id: "user-123",
|
|
825
|
+
email: "test@example.com",
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
mockYnabAPI.user.getUser.mockResolvedValue(mockUser);
|
|
831
|
+
|
|
832
|
+
// Test user retrieval
|
|
833
|
+
const userResult = await executeToolCall(server, "ynab:get_user");
|
|
834
|
+
validateToolResult(userResult);
|
|
835
|
+
|
|
836
|
+
const user = parseToolResult(userResult);
|
|
837
|
+
expect(user.data.user.id).toBe("user-123");
|
|
838
|
+
|
|
839
|
+
expect(mockYnabAPI.user.getUser).toHaveBeenCalledTimes(1);
|
|
840
|
+
},
|
|
841
|
+
);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
describe("Error Handling Integration", () => {
|
|
845
|
+
it(
|
|
846
|
+
"should handle various API error scenarios",
|
|
847
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
848
|
+
async () => {
|
|
849
|
+
// Test 401 Unauthorized
|
|
850
|
+
const authError = new Error("Unauthorized");
|
|
851
|
+
(authError as any).error = {
|
|
852
|
+
id: "401",
|
|
853
|
+
name: "unauthorized",
|
|
854
|
+
description: "Unauthorized",
|
|
855
|
+
};
|
|
856
|
+
mockYnabAPI.budgets.getBudgets.mockRejectedValue(authError);
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
await executeToolCall(server, "ynab:list_budgets");
|
|
860
|
+
expect.fail("Should have thrown an error");
|
|
861
|
+
} catch (error) {
|
|
862
|
+
expect(error).toBeDefined();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Test 404 Not Found
|
|
866
|
+
const notFoundError = new Error("Not Found");
|
|
867
|
+
(notFoundError as any).error = {
|
|
868
|
+
id: "404.2",
|
|
869
|
+
name: "not_found",
|
|
870
|
+
description: "Budget not found",
|
|
871
|
+
};
|
|
872
|
+
mockYnabAPI.budgets.getBudgetById.mockRejectedValue(notFoundError);
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
await executeToolCall(server, "ynab:get_budget", {
|
|
876
|
+
budget_id: "invalid",
|
|
877
|
+
});
|
|
878
|
+
expect.fail("Should have thrown an error");
|
|
879
|
+
} catch (error) {
|
|
880
|
+
expect(error).toBeDefined();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Test 429 Rate Limit
|
|
884
|
+
const rateLimitError = new Error("Too Many Requests");
|
|
885
|
+
(rateLimitError as any).error = {
|
|
886
|
+
id: "429",
|
|
887
|
+
name: "rate_limit",
|
|
888
|
+
description: "Rate limit exceeded",
|
|
889
|
+
};
|
|
890
|
+
mockYnabAPI.accounts.getAccounts.mockRejectedValue(rateLimitError);
|
|
891
|
+
|
|
892
|
+
try {
|
|
893
|
+
await executeToolCall(server, "ynab:list_accounts", {
|
|
894
|
+
budget_id: "test",
|
|
895
|
+
});
|
|
896
|
+
expect.fail("Should have thrown an error");
|
|
897
|
+
} catch (error) {
|
|
898
|
+
expect(error).toBeDefined();
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
it(
|
|
904
|
+
"should validate input parameters",
|
|
905
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
906
|
+
async () => {
|
|
907
|
+
// Test missing required parameters
|
|
908
|
+
try {
|
|
909
|
+
await executeToolCall(server, "ynab:get_budget", {});
|
|
910
|
+
expect.fail("Should have thrown validation error");
|
|
911
|
+
} catch (error) {
|
|
912
|
+
expect(error).toBeDefined();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Test invalid parameter types
|
|
916
|
+
try {
|
|
917
|
+
await executeToolCall(server, "ynab:create_transaction", {
|
|
918
|
+
budget_id: "test",
|
|
919
|
+
account_id: "test",
|
|
920
|
+
amount: "invalid-amount", // Should be number
|
|
921
|
+
date: "2024-01-01",
|
|
922
|
+
});
|
|
923
|
+
expect.fail("Should have thrown validation error");
|
|
924
|
+
} catch (error) {
|
|
925
|
+
expect(error).toBeDefined();
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
describe("Caching Integration Tests", () => {
|
|
932
|
+
let previousNodeEnv: string | undefined;
|
|
933
|
+
|
|
934
|
+
beforeAll(() => {
|
|
935
|
+
previousNodeEnv = process.env.NODE_ENV;
|
|
936
|
+
process.env.NODE_ENV = "development";
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
beforeEach(() => {
|
|
940
|
+
cacheManager.clear();
|
|
941
|
+
process.env.NODE_ENV = "development";
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
afterAll(() => {
|
|
945
|
+
// Restore NODE_ENV after all caching tests complete
|
|
946
|
+
if (previousNodeEnv === undefined) {
|
|
947
|
+
process.env.NODE_ENV = undefined;
|
|
948
|
+
} else {
|
|
949
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// TODO: Re-enable after DeltaFetcher cache integration alignment.
|
|
954
|
+
it.skip(
|
|
955
|
+
"should cache budget list requests and improve performance on subsequent calls",
|
|
956
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
957
|
+
async () => {
|
|
958
|
+
const mockBudgets = {
|
|
959
|
+
data: {
|
|
960
|
+
budgets: [
|
|
961
|
+
{
|
|
962
|
+
id: "budget-1",
|
|
963
|
+
name: "Test Budget",
|
|
964
|
+
last_modified_on: "2024-01-01T00:00:00Z",
|
|
965
|
+
first_month: "2024-01-01",
|
|
966
|
+
last_month: "2024-12-01",
|
|
967
|
+
date_format: { format: "MM/DD/YYYY" },
|
|
968
|
+
currency_format: {
|
|
969
|
+
iso_code: "USD",
|
|
970
|
+
example_format: "$123.45",
|
|
971
|
+
decimal_digits: 2,
|
|
972
|
+
decimal_separator: ".",
|
|
973
|
+
symbol_first: true,
|
|
974
|
+
group_separator: ",",
|
|
975
|
+
currency_symbol: "$",
|
|
976
|
+
display_symbol: true,
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
mockYnabAPI.budgets.getBudgets.mockResolvedValue(mockBudgets);
|
|
984
|
+
|
|
985
|
+
const statsBeforeFirstCall = cacheManager.getStats();
|
|
986
|
+
const initialSize = statsBeforeFirstCall.size;
|
|
987
|
+
|
|
988
|
+
// First call - should hit API and cache result
|
|
989
|
+
const firstResult = await executeToolCall(server, "ynab:list_budgets");
|
|
990
|
+
validateToolResult(firstResult);
|
|
991
|
+
|
|
992
|
+
const firstParsed = parseToolResult(firstResult);
|
|
993
|
+
expect(firstParsed.data.cached).toBe(false);
|
|
994
|
+
expect(firstParsed.data.cache_info).toBe(
|
|
995
|
+
"Fresh data retrieved from YNAB API",
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
// Cache should have grown
|
|
999
|
+
const statsAfterFirstCall = cacheManager.getStats();
|
|
1000
|
+
expect(statsAfterFirstCall.size).toBeGreaterThan(initialSize);
|
|
1001
|
+
|
|
1002
|
+
// Second call - should hit cache
|
|
1003
|
+
const secondResult = await executeToolCall(server, "ynab:list_budgets");
|
|
1004
|
+
validateToolResult(secondResult);
|
|
1005
|
+
|
|
1006
|
+
const secondParsed = parseToolResult(secondResult);
|
|
1007
|
+
expect(secondParsed.data.cached).toBe(true);
|
|
1008
|
+
expect(secondParsed.data.cache_info).toBe(
|
|
1009
|
+
"Data retrieved from cache for improved performance",
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
// API should only have been called once
|
|
1013
|
+
expect(mockYnabAPI.budgets.getBudgets).toHaveBeenCalledTimes(1);
|
|
1014
|
+
|
|
1015
|
+
// Cache hit count should have increased
|
|
1016
|
+
const finalStats = cacheManager.getStats();
|
|
1017
|
+
expect(finalStats.hits).toBeGreaterThan(statsBeforeFirstCall.hits);
|
|
1018
|
+
},
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
it(
|
|
1022
|
+
"should invalidate cache on write operations",
|
|
1023
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
1024
|
+
async () => {
|
|
1025
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
1026
|
+
|
|
1027
|
+
// Mock responses
|
|
1028
|
+
const mockAccounts = {
|
|
1029
|
+
data: {
|
|
1030
|
+
accounts: [
|
|
1031
|
+
{
|
|
1032
|
+
id: "account-1",
|
|
1033
|
+
name: "Test Account",
|
|
1034
|
+
type: "checking",
|
|
1035
|
+
on_budget: true,
|
|
1036
|
+
closed: false,
|
|
1037
|
+
balance: 100000,
|
|
1038
|
+
cleared_balance: 95000,
|
|
1039
|
+
uncleared_balance: 5000,
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const mockCreatedAccount = {
|
|
1046
|
+
data: {
|
|
1047
|
+
account: {
|
|
1048
|
+
id: "account-2",
|
|
1049
|
+
name: "New Account",
|
|
1050
|
+
type: "savings",
|
|
1051
|
+
on_budget: true,
|
|
1052
|
+
closed: false,
|
|
1053
|
+
balance: 0,
|
|
1054
|
+
cleared_balance: 0,
|
|
1055
|
+
uncleared_balance: 0,
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
mockYnabAPI.accounts.getAccounts.mockResolvedValue(mockAccounts);
|
|
1061
|
+
mockYnabAPI.accounts.createAccount.mockResolvedValue(
|
|
1062
|
+
mockCreatedAccount,
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
// First, populate cache with account list
|
|
1066
|
+
await executeToolCall(server, "ynab:list_accounts", {
|
|
1067
|
+
budget_id: budgetId,
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// Verify cache has entries
|
|
1071
|
+
const statsAfterRead = cacheManager.getStats();
|
|
1072
|
+
expect(statsAfterRead.size).toBeGreaterThan(0);
|
|
1073
|
+
|
|
1074
|
+
// Create a new account (write operation)
|
|
1075
|
+
await executeToolCall(server, "ynab:create_account", {
|
|
1076
|
+
budget_id: budgetId,
|
|
1077
|
+
name: "New Account",
|
|
1078
|
+
type: "savings",
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// Next call to list accounts should hit API again (cache was invalidated)
|
|
1082
|
+
mockYnabAPI.accounts.getAccounts.mockClear();
|
|
1083
|
+
await executeToolCall(server, "ynab:list_accounts", {
|
|
1084
|
+
budget_id: budgetId,
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// Verify API was called again after cache invalidation
|
|
1088
|
+
expect(mockYnabAPI.accounts.getAccounts).toHaveBeenCalledTimes(1);
|
|
1089
|
+
},
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
// TODO: Re-enable after DeltaFetcher cache integration alignment.
|
|
1093
|
+
it.skip(
|
|
1094
|
+
"should not cache filtered transaction requests",
|
|
1095
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
1096
|
+
async () => {
|
|
1097
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
1098
|
+
|
|
1099
|
+
const mockTransactions = {
|
|
1100
|
+
data: {
|
|
1101
|
+
transactions: [
|
|
1102
|
+
{
|
|
1103
|
+
id: "transaction-1",
|
|
1104
|
+
date: "2024-01-15",
|
|
1105
|
+
amount: -5000,
|
|
1106
|
+
memo: "Test transaction",
|
|
1107
|
+
cleared: "cleared",
|
|
1108
|
+
approved: true,
|
|
1109
|
+
account_id: "account-1",
|
|
1110
|
+
category_id: "category-1",
|
|
1111
|
+
},
|
|
1112
|
+
],
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
mockYnabAPI.transactions.getTransactions.mockResolvedValue(
|
|
1117
|
+
mockTransactions,
|
|
1118
|
+
);
|
|
1119
|
+
mockYnabAPI.transactions.getTransactionsByAccount.mockResolvedValue(
|
|
1120
|
+
mockTransactions,
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
const statsBeforeUnfiltered = cacheManager.getStats();
|
|
1124
|
+
|
|
1125
|
+
// Unfiltered request - should be cached
|
|
1126
|
+
const unfilteredResult = await executeToolCall(
|
|
1127
|
+
server,
|
|
1128
|
+
"ynab:list_transactions",
|
|
1129
|
+
{
|
|
1130
|
+
budget_id: budgetId,
|
|
1131
|
+
},
|
|
1132
|
+
);
|
|
1133
|
+
const unfilteredParsed = parseToolResult(unfilteredResult);
|
|
1134
|
+
expect(unfilteredParsed.data.cached).toBe(false); // First call, cache miss
|
|
1135
|
+
|
|
1136
|
+
const statsAfterUnfiltered = cacheManager.getStats();
|
|
1137
|
+
expect(statsAfterUnfiltered.size).toBeGreaterThan(
|
|
1138
|
+
statsBeforeUnfiltered.size,
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
// Filtered request - should NOT be cached
|
|
1142
|
+
const filteredResult = await executeToolCall(
|
|
1143
|
+
server,
|
|
1144
|
+
"ynab:list_transactions",
|
|
1145
|
+
{
|
|
1146
|
+
budget_id: budgetId,
|
|
1147
|
+
account_id: "account-1",
|
|
1148
|
+
},
|
|
1149
|
+
);
|
|
1150
|
+
const filteredParsed = parseToolResult(filteredResult);
|
|
1151
|
+
expect(filteredParsed.data.cached).toBe(false);
|
|
1152
|
+
expect(filteredParsed.data.cache_info).toBe(
|
|
1153
|
+
"Fresh data retrieved from YNAB API",
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
// Cache size should not have increased for filtered request
|
|
1157
|
+
const statsAfterFiltered = cacheManager.getStats();
|
|
1158
|
+
expect(statsAfterFiltered.size).toBe(statsAfterUnfiltered.size);
|
|
1159
|
+
|
|
1160
|
+
// Both API methods should have been called
|
|
1161
|
+
expect(mockYnabAPI.transactions.getTransactions).toHaveBeenCalledTimes(
|
|
1162
|
+
1,
|
|
1163
|
+
);
|
|
1164
|
+
expect(
|
|
1165
|
+
mockYnabAPI.transactions.getTransactionsByAccount,
|
|
1166
|
+
).toHaveBeenCalledTimes(1);
|
|
1167
|
+
},
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
it(
|
|
1171
|
+
"should handle cache warming after setting default budget",
|
|
1172
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
1173
|
+
async () => {
|
|
1174
|
+
const budgetId = TEST_BUDGET_UUID;
|
|
1175
|
+
|
|
1176
|
+
// Mock all the responses for cache warming
|
|
1177
|
+
mockYnabAPI.accounts.getAccounts.mockResolvedValue({
|
|
1178
|
+
data: { accounts: [] },
|
|
1179
|
+
});
|
|
1180
|
+
mockYnabAPI.budgets.getBudgetById.mockResolvedValue({
|
|
1181
|
+
data: { budget: { id: budgetId, name: "Warm Cache Budget" } },
|
|
1182
|
+
});
|
|
1183
|
+
mockYnabAPI.categories.getCategories.mockResolvedValue({
|
|
1184
|
+
data: { category_groups: [] },
|
|
1185
|
+
});
|
|
1186
|
+
mockYnabAPI.payees.getPayees.mockResolvedValue({
|
|
1187
|
+
data: { payees: [] },
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
const statsBeforeSet = cacheManager.getStats();
|
|
1191
|
+
const initialSize = statsBeforeSet.size;
|
|
1192
|
+
|
|
1193
|
+
// Set default budget (should trigger cache warming)
|
|
1194
|
+
await executeToolCall(server, "ynab:set_default_budget", {
|
|
1195
|
+
budget_id: budgetId,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
// Wait for cache warming to populate entries (fire-and-forget process)
|
|
1199
|
+
let statsAfterSet = cacheManager.getStats();
|
|
1200
|
+
await waitFor(
|
|
1201
|
+
() => {
|
|
1202
|
+
statsAfterSet = cacheManager.getStats();
|
|
1203
|
+
return statsAfterSet.size > initialSize;
|
|
1204
|
+
},
|
|
1205
|
+
1000,
|
|
1206
|
+
50,
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
// Cache should have more entries due to warming
|
|
1210
|
+
expect(statsAfterSet.size).toBeGreaterThan(initialSize);
|
|
1211
|
+
|
|
1212
|
+
// Verify that cache warming API calls were made
|
|
1213
|
+
expect(mockYnabAPI.accounts.getAccounts).toHaveBeenCalled();
|
|
1214
|
+
expect(mockYnabAPI.categories.getCategories).toHaveBeenCalled();
|
|
1215
|
+
expect(mockYnabAPI.payees.getPayees).toHaveBeenCalled();
|
|
1216
|
+
},
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
it(
|
|
1220
|
+
"should handle cache clear operation",
|
|
1221
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
1222
|
+
async () => {
|
|
1223
|
+
// Populate cache with some data
|
|
1224
|
+
mockYnabAPI.budgets.getBudgets.mockResolvedValue({
|
|
1225
|
+
data: { budgets: [] },
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
await executeToolCall(server, "ynab:list_budgets");
|
|
1229
|
+
|
|
1230
|
+
// Verify cache has entries
|
|
1231
|
+
const statsAfterPopulation = cacheManager.getStats();
|
|
1232
|
+
expect(statsAfterPopulation.size).toBeGreaterThan(0);
|
|
1233
|
+
|
|
1234
|
+
// Clear cache
|
|
1235
|
+
await executeToolCall(server, "ynab:clear_cache");
|
|
1236
|
+
|
|
1237
|
+
// Verify cache is empty
|
|
1238
|
+
const statsAfterClear = cacheManager.getStats();
|
|
1239
|
+
expect(statsAfterClear.size).toBe(0);
|
|
1240
|
+
expect(statsAfterClear.hits).toBe(0);
|
|
1241
|
+
expect(statsAfterClear.misses).toBe(0);
|
|
1242
|
+
},
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
// TODO: Re-enable after DeltaFetcher cache integration alignment.
|
|
1246
|
+
it.skip(
|
|
1247
|
+
"should respect cache TTL and return fresh data after expiration",
|
|
1248
|
+
{ meta: { tier: "domain", domain: "workflows" } },
|
|
1249
|
+
async () => {
|
|
1250
|
+
// Note: This test is conceptual since TTL testing requires time manipulation
|
|
1251
|
+
// In a real scenario, we would mock the Date.now() or use a test clock
|
|
1252
|
+
|
|
1253
|
+
const mockBudgets = {
|
|
1254
|
+
data: {
|
|
1255
|
+
budgets: [
|
|
1256
|
+
{
|
|
1257
|
+
id: "budget-1",
|
|
1258
|
+
name: "Test Budget",
|
|
1259
|
+
last_modified_on: "2024-01-01T00:00:00Z",
|
|
1260
|
+
first_month: "2024-01-01",
|
|
1261
|
+
last_month: "2024-12-01",
|
|
1262
|
+
date_format: { format: "MM/DD/YYYY" },
|
|
1263
|
+
currency_format: {
|
|
1264
|
+
iso_code: "USD",
|
|
1265
|
+
example_format: "$123.45",
|
|
1266
|
+
decimal_digits: 2,
|
|
1267
|
+
decimal_separator: ".",
|
|
1268
|
+
symbol_first: true,
|
|
1269
|
+
group_separator: ",",
|
|
1270
|
+
currency_symbol: "$",
|
|
1271
|
+
display_symbol: true,
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
],
|
|
1275
|
+
},
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
mockYnabAPI.budgets.getBudgets.mockResolvedValue(mockBudgets);
|
|
1279
|
+
|
|
1280
|
+
// First call - cache miss
|
|
1281
|
+
const firstResult = await executeToolCall(server, "ynab:list_budgets");
|
|
1282
|
+
const firstParsed = parseToolResult(firstResult);
|
|
1283
|
+
expect(firstParsed.data.cached).toBe(false);
|
|
1284
|
+
|
|
1285
|
+
// Second call - cache hit
|
|
1286
|
+
const secondResult = await executeToolCall(server, "ynab:list_budgets");
|
|
1287
|
+
const secondParsed = parseToolResult(secondResult);
|
|
1288
|
+
expect(secondParsed.data.cached).toBe(true);
|
|
1289
|
+
|
|
1290
|
+
// Verify API was only called once (second call used cache)
|
|
1291
|
+
expect(mockYnabAPI.budgets.getBudgets).toHaveBeenCalledTimes(1);
|
|
1292
|
+
},
|
|
1293
|
+
);
|
|
1294
|
+
});
|
|
1168
1295
|
});
|