@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,878 +3,895 @@
|
|
|
3
3
|
* Tests all new functionality including observability, LRU eviction, and concurrent deduplication
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
7
|
-
import { CacheManager } from
|
|
8
|
-
|
|
9
|
-
describe(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { CacheManager } from "../cacheManager.js";
|
|
8
|
+
|
|
9
|
+
describe("CacheManager", () => {
|
|
10
|
+
let cache: CacheManager;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
vi.useFakeTimers({ now: 0 }); // Start fake timers at timestamp 0
|
|
15
|
+
// Clear environment variables
|
|
16
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = undefined;
|
|
17
|
+
process.env.YNAB_MCP_CACHE_STALE_MS = undefined;
|
|
18
|
+
process.env.YNAB_MCP_CACHE_DEFAULT_TTL_MS = undefined;
|
|
19
|
+
cache = new CacheManager();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("Basic Functionality", () => {
|
|
27
|
+
it("should store and retrieve data", () => {
|
|
28
|
+
cache.set("key1", "value1");
|
|
29
|
+
expect(cache.get("key1")).toBe("value1");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return null for non-existent keys", () => {
|
|
33
|
+
expect(cache.get("nonexistent")).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should delete entries", () => {
|
|
37
|
+
cache.set("key1", "value1");
|
|
38
|
+
expect(cache.delete("key1")).toBe(true);
|
|
39
|
+
expect(cache.get("key1")).toBeNull();
|
|
40
|
+
expect(cache.delete("nonexistent")).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should clear all entries", () => {
|
|
44
|
+
cache.set("key1", "value1");
|
|
45
|
+
cache.set("key2", "value2");
|
|
46
|
+
cache.clear();
|
|
47
|
+
expect(cache.get("key1")).toBeNull();
|
|
48
|
+
expect(cache.get("key2")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should handle TTL expiration", () => {
|
|
52
|
+
cache.set("key1", "value1", 1000); // 1 second TTL
|
|
53
|
+
expect(cache.get("key1")).toBe("value1");
|
|
54
|
+
|
|
55
|
+
vi.advanceTimersByTime(1100);
|
|
56
|
+
expect(cache.get("key1")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should generate consistent cache keys", () => {
|
|
60
|
+
const key1 = CacheManager.generateKey("prefix", "param1", 2, true);
|
|
61
|
+
const key2 = CacheManager.generateKey("prefix", "param1", 2, true);
|
|
62
|
+
expect(key1).toBe(key2);
|
|
63
|
+
expect(key1).toBe("prefix:param1:2:true");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should filter undefined parameters in key generation", () => {
|
|
67
|
+
const key = CacheManager.generateKey(
|
|
68
|
+
"prefix",
|
|
69
|
+
"param1",
|
|
70
|
+
undefined,
|
|
71
|
+
"param3",
|
|
72
|
+
);
|
|
73
|
+
expect(key).toBe("prefix:param1:param3");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("Hit/Miss Counters", () => {
|
|
78
|
+
it("should track cache hits", () => {
|
|
79
|
+
cache.set("key1", "value1");
|
|
80
|
+
cache.get("key1");
|
|
81
|
+
cache.get("key1");
|
|
82
|
+
|
|
83
|
+
const stats = cache.getStats();
|
|
84
|
+
expect(stats.hits).toBe(2);
|
|
85
|
+
expect(stats.misses).toBe(0);
|
|
86
|
+
expect(stats.hitRate).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should track cache misses", () => {
|
|
90
|
+
cache.get("nonexistent1");
|
|
91
|
+
cache.get("nonexistent2");
|
|
92
|
+
|
|
93
|
+
const stats = cache.getStats();
|
|
94
|
+
expect(stats.hits).toBe(0);
|
|
95
|
+
expect(stats.misses).toBe(2);
|
|
96
|
+
expect(stats.hitRate).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should track expired entries as misses", () => {
|
|
100
|
+
cache.set("key1", "value1", 1000);
|
|
101
|
+
vi.advanceTimersByTime(1100);
|
|
102
|
+
cache.get("key1");
|
|
103
|
+
|
|
104
|
+
const stats = cache.getStats();
|
|
105
|
+
expect(stats.hits).toBe(0);
|
|
106
|
+
expect(stats.misses).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should calculate hit rate correctly", () => {
|
|
110
|
+
cache.set("key1", "value1");
|
|
111
|
+
cache.get("key1"); // hit
|
|
112
|
+
cache.get("key1"); // hit
|
|
113
|
+
cache.get("nonexistent"); // miss
|
|
114
|
+
|
|
115
|
+
const stats = cache.getStats();
|
|
116
|
+
expect(stats.hits).toBe(2);
|
|
117
|
+
expect(stats.misses).toBe(1);
|
|
118
|
+
expect(stats.hitRate).toBeCloseTo(2 / 3);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should reset counters on clear", () => {
|
|
122
|
+
cache.set("key1", "value1");
|
|
123
|
+
cache.get("key1");
|
|
124
|
+
cache.get("nonexistent");
|
|
125
|
+
|
|
126
|
+
cache.clear();
|
|
127
|
+
const stats = cache.getStats();
|
|
128
|
+
expect(stats.hits).toBe(0);
|
|
129
|
+
expect(stats.misses).toBe(0);
|
|
130
|
+
expect(stats.hitRate).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should handle zero requests for hit rate", () => {
|
|
134
|
+
const stats = cache.getStats();
|
|
135
|
+
expect(stats.hitRate).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("LRU Eviction", () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "3";
|
|
142
|
+
cache = new CacheManager();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should not evict when under limit", () => {
|
|
146
|
+
cache.set("key1", "value1");
|
|
147
|
+
cache.set("key2", "value2");
|
|
148
|
+
|
|
149
|
+
const stats = cache.getStats();
|
|
150
|
+
expect(stats.size).toBe(2);
|
|
151
|
+
expect(stats.evictions).toBe(0);
|
|
152
|
+
expect(cache.get("key1")).toBe("value1");
|
|
153
|
+
expect(cache.get("key2")).toBe("value2");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should evict LRU entry when maxEntries is exceeded", () => {
|
|
157
|
+
cache.set("key1", "value1");
|
|
158
|
+
cache.set("key2", "value2");
|
|
159
|
+
cache.set("key3", "value3");
|
|
160
|
+
cache.set("key4", "value4"); // Should evict key1
|
|
161
|
+
|
|
162
|
+
const stats = cache.getStats();
|
|
163
|
+
expect(stats.size).toBe(3);
|
|
164
|
+
expect(stats.evictions).toBe(1);
|
|
165
|
+
expect(cache.get("key1")).toBeNull(); // Evicted
|
|
166
|
+
expect(cache.get("key2")).toBe("value2");
|
|
167
|
+
expect(cache.get("key3")).toBe("value3");
|
|
168
|
+
expect(cache.get("key4")).toBe("value4");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should update access order on get", () => {
|
|
172
|
+
cache.set("key1", "value1");
|
|
173
|
+
cache.set("key2", "value2");
|
|
174
|
+
cache.set("key3", "value3");
|
|
175
|
+
|
|
176
|
+
// Access key1 to make it most recently used
|
|
177
|
+
cache.get("key1");
|
|
178
|
+
|
|
179
|
+
cache.set("key4", "value4"); // Should evict key2 (oldest)
|
|
180
|
+
|
|
181
|
+
expect(cache.get("key1")).toBe("value1"); // Still there
|
|
182
|
+
expect(cache.get("key2")).toBeNull(); // Evicted
|
|
183
|
+
expect(cache.get("key3")).toBe("value3");
|
|
184
|
+
expect(cache.get("key4")).toBe("value4");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should handle zero maxEntries (no caching)", () => {
|
|
188
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "0";
|
|
189
|
+
cache = new CacheManager();
|
|
190
|
+
|
|
191
|
+
cache.set("key1", "value1");
|
|
192
|
+
expect(cache.get("key1")).toBeNull();
|
|
193
|
+
expect(cache.getStats().size).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should evict multiple entries if needed", () => {
|
|
197
|
+
// Fill cache
|
|
198
|
+
cache.set("key1", "value1");
|
|
199
|
+
cache.set("key2", "value2");
|
|
200
|
+
cache.set("key3", "value3");
|
|
201
|
+
|
|
202
|
+
// Change maxEntries to 1 by creating a new cache manager
|
|
203
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "1";
|
|
204
|
+
const smallCache = new CacheManager();
|
|
205
|
+
|
|
206
|
+
// Add entries that should trigger multiple evictions
|
|
207
|
+
smallCache.set("key1", "value1");
|
|
208
|
+
smallCache.set("key2", "value2");
|
|
209
|
+
|
|
210
|
+
expect(smallCache.getStats().size).toBe(1);
|
|
211
|
+
expect(smallCache.getStats().evictions).toBe(1);
|
|
212
|
+
expect(smallCache.get("key2")).toBe("value2"); // Most recent
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should not evict when updating existing key at maxEntries limit", () => {
|
|
216
|
+
// Fill cache to capacity
|
|
217
|
+
cache.set("key1", "value1");
|
|
218
|
+
cache.set("key2", "value2");
|
|
219
|
+
cache.set("key3", "value3");
|
|
220
|
+
|
|
221
|
+
const initialStats = cache.getStats();
|
|
222
|
+
expect(initialStats.size).toBe(3);
|
|
223
|
+
expect(initialStats.evictions).toBe(0);
|
|
224
|
+
|
|
225
|
+
// Update an existing key - should not trigger eviction
|
|
226
|
+
cache.set("key2", "updated-value2");
|
|
227
|
+
|
|
228
|
+
const updatedStats = cache.getStats();
|
|
229
|
+
expect(updatedStats.size).toBe(3); // Same size
|
|
230
|
+
expect(updatedStats.evictions).toBe(0); // No evictions
|
|
231
|
+
expect(cache.get("key1")).toBe("value1"); // Other keys still present
|
|
232
|
+
expect(cache.get("key2")).toBe("updated-value2"); // Updated value
|
|
233
|
+
expect(cache.get("key3")).toBe("value3"); // Other keys still present
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("Per-Entry Options", () => {
|
|
238
|
+
it("should use custom TTL from options", () => {
|
|
239
|
+
cache.set("key1", "value1", { ttl: 500, staleWhileRevalidate: 0 });
|
|
240
|
+
cache.set("key2", "value2", { ttl: 1500, staleWhileRevalidate: 0 });
|
|
241
|
+
|
|
242
|
+
vi.advanceTimersByTime(1000);
|
|
243
|
+
expect(cache.get("key1")).toBeNull(); // Expired
|
|
244
|
+
expect(cache.get("key2")).toBe("value2"); // Still valid
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should use default TTL when no options provided", () => {
|
|
248
|
+
cache.set("key1", "value1");
|
|
249
|
+
|
|
250
|
+
// Advance to just before expiration (5 minutes is default TTL)
|
|
251
|
+
vi.advanceTimersByTime(299000); // Just under 5 minutes - should still be valid
|
|
252
|
+
expect(cache.get("key1")).toBe("value1");
|
|
253
|
+
|
|
254
|
+
// Advance past the TTL (using simple set should have NO stale window)
|
|
255
|
+
vi.advanceTimersByTime(2000); // Total ~5 minutes - should be expired
|
|
256
|
+
expect(cache.get("key1")).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should support staleWhileRevalidate", () => {
|
|
260
|
+
cache.set("key1", "value1", { ttl: 1000, staleWhileRevalidate: 2000 });
|
|
261
|
+
|
|
262
|
+
vi.advanceTimersByTime(1500); // Within stale window
|
|
263
|
+
const result = cache.get("key1");
|
|
264
|
+
|
|
265
|
+
expect(result).toBe("value1"); // Should return stale data
|
|
266
|
+
const stats = cache.getStats();
|
|
267
|
+
expect(stats.hits).toBe(1); // Counted as hit
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should not return data outside stale window", () => {
|
|
271
|
+
cache.set("key1", "value1", { ttl: 1000, staleWhileRevalidate: 2000 });
|
|
272
|
+
|
|
273
|
+
vi.advanceTimersByTime(3500); // Outside stale window
|
|
274
|
+
const result = cache.get("key1");
|
|
275
|
+
|
|
276
|
+
expect(result).toBeNull();
|
|
277
|
+
const stats = cache.getStats();
|
|
278
|
+
expect(stats.misses).toBe(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should maintain backward compatibility with number TTL", () => {
|
|
282
|
+
cache.set("key1", "value1", 2000);
|
|
283
|
+
vi.advanceTimersByTime(1000);
|
|
284
|
+
expect(cache.get("key1")).toBe("value1");
|
|
285
|
+
|
|
286
|
+
vi.advanceTimersByTime(1500);
|
|
287
|
+
expect(cache.get("key1")).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("wrap() Helper", () => {
|
|
292
|
+
it("should return cached data immediately on hit", async () => {
|
|
293
|
+
const loader = vi.fn().mockResolvedValue("loaded-value");
|
|
294
|
+
cache.set("key1", "cached-value");
|
|
295
|
+
|
|
296
|
+
const result = await cache.wrap("key1", { loader });
|
|
297
|
+
|
|
298
|
+
expect(result).toBe("cached-value");
|
|
299
|
+
expect(loader).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should call loader and cache result on miss", async () => {
|
|
303
|
+
const loader = vi.fn().mockResolvedValue("loaded-value");
|
|
304
|
+
|
|
305
|
+
const result = await cache.wrap("key1", { loader });
|
|
306
|
+
|
|
307
|
+
expect(result).toBe("loaded-value");
|
|
308
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(cache.get("key1")).toBe("loaded-value");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should deduplicate concurrent requests", async () => {
|
|
313
|
+
const loader = vi
|
|
314
|
+
.fn()
|
|
315
|
+
.mockImplementation(
|
|
316
|
+
() =>
|
|
317
|
+
new Promise((resolve) =>
|
|
318
|
+
setTimeout(() => resolve("loaded-value"), 100),
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Start two concurrent requests
|
|
323
|
+
const promise1 = cache.wrap("key1", { loader });
|
|
324
|
+
const promise2 = cache.wrap("key1", { loader });
|
|
325
|
+
|
|
326
|
+
// Advance time to resolve promises
|
|
327
|
+
vi.advanceTimersByTime(100);
|
|
328
|
+
|
|
329
|
+
const [result1, result2] = await Promise.all([promise1, promise2]);
|
|
330
|
+
|
|
331
|
+
expect(result1).toBe("loaded-value");
|
|
332
|
+
expect(result2).toBe("loaded-value");
|
|
333
|
+
expect(loader).toHaveBeenCalledTimes(1); // Only called once
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should handle loader errors gracefully", async () => {
|
|
337
|
+
const loader = vi.fn().mockRejectedValue(new Error("Load failed"));
|
|
338
|
+
|
|
339
|
+
await expect(cache.wrap("key1", { loader })).rejects.toThrow(
|
|
340
|
+
"Load failed",
|
|
341
|
+
);
|
|
342
|
+
expect(cache.get("key1")).toBeNull(); // Should not cache error
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should serve stale data and trigger background refresh", async () => {
|
|
346
|
+
const loader1 = vi.fn().mockResolvedValue("initial-value");
|
|
347
|
+
const loader2 = vi.fn().mockResolvedValue("refreshed-value");
|
|
348
|
+
|
|
349
|
+
// Initial load
|
|
350
|
+
await cache.wrap("key1", {
|
|
351
|
+
loader: loader1,
|
|
352
|
+
ttl: 1000,
|
|
353
|
+
staleWhileRevalidate: 2000,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Move to stale period
|
|
357
|
+
vi.advanceTimersByTime(1500);
|
|
358
|
+
|
|
359
|
+
// Second call should return stale data immediately and refresh in background
|
|
360
|
+
const result = await cache.wrap("key1", { loader: loader2 });
|
|
361
|
+
expect(result).toBe("initial-value"); // Stale data returned
|
|
362
|
+
|
|
363
|
+
// Advance time to allow background refresh
|
|
364
|
+
vi.advanceTimersByTime(100);
|
|
365
|
+
await vi.runAllTimersAsync();
|
|
366
|
+
|
|
367
|
+
expect(loader2).toHaveBeenCalledTimes(1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should apply cache options from wrap call", async () => {
|
|
371
|
+
const loader = vi.fn().mockResolvedValue("loaded-value");
|
|
372
|
+
|
|
373
|
+
await cache.wrap("key1", {
|
|
374
|
+
loader,
|
|
375
|
+
ttl: 500,
|
|
376
|
+
staleWhileRevalidate: 1000,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Verify custom TTL
|
|
380
|
+
vi.advanceTimersByTime(400);
|
|
381
|
+
expect(cache.get("key1")).toBe("loaded-value");
|
|
382
|
+
|
|
383
|
+
vi.advanceTimersByTime(200); // Total 600ms, past TTL but within stale window
|
|
384
|
+
const staleResult = cache.get("key1");
|
|
385
|
+
expect(staleResult).toBe("loaded-value"); // Should return stale data
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("should clean up pending operations on completion", async () => {
|
|
389
|
+
const loader = vi.fn().mockResolvedValue("loaded-value");
|
|
390
|
+
|
|
391
|
+
await cache.wrap("key1", { loader });
|
|
392
|
+
|
|
393
|
+
// Start another request after first completes
|
|
394
|
+
const loader2 = vi.fn().mockResolvedValue("loaded-value-2");
|
|
395
|
+
await cache.wrap("key1", { loader: loader2 });
|
|
396
|
+
|
|
397
|
+
// Should use cached value, not call loader2
|
|
398
|
+
expect(loader2).not.toHaveBeenCalled();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("should preserve existing TTL/SWR when options omitted in background refresh", async () => {
|
|
402
|
+
const loader1 = vi.fn().mockResolvedValue("initial-value");
|
|
403
|
+
const loader2 = vi.fn().mockResolvedValue("refreshed-value");
|
|
404
|
+
|
|
405
|
+
// Initial load with specific TTL/SWR
|
|
406
|
+
await cache.wrap("key1", {
|
|
407
|
+
loader: loader1,
|
|
408
|
+
ttl: 2000,
|
|
409
|
+
staleWhileRevalidate: 3000,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Move to stale period
|
|
413
|
+
vi.advanceTimersByTime(2500);
|
|
414
|
+
|
|
415
|
+
// Background refresh with no TTL/SWR specified - should preserve original values
|
|
416
|
+
await cache.wrap("key1", { loader: loader2 });
|
|
417
|
+
|
|
418
|
+
// Advance time to allow background refresh
|
|
419
|
+
vi.advanceTimersByTime(100);
|
|
420
|
+
await vi.runAllTimersAsync();
|
|
421
|
+
|
|
422
|
+
// Now check if the refreshed entry still has the original TTL
|
|
423
|
+
vi.advanceTimersByTime(1800); // Should still be within original TTL (2000ms)
|
|
424
|
+
const result = cache.get("key1");
|
|
425
|
+
expect(result).toBe("refreshed-value");
|
|
426
|
+
|
|
427
|
+
// Move past original TTL but within stale window
|
|
428
|
+
vi.advanceTimersByTime(300); // Total 2300ms past refresh, should be in stale window
|
|
429
|
+
const staleResult = cache.get("key1");
|
|
430
|
+
expect(staleResult).toBe("refreshed-value"); // Should still be available due to preserved SWR
|
|
431
|
+
|
|
432
|
+
// Move beyond original TTL + stale window (5000ms) from the initial load to ensure expiry
|
|
433
|
+
vi.advanceTimersByTime(3000); // Total elapsed time ~7700ms from first load
|
|
434
|
+
await vi.runAllTimersAsync();
|
|
435
|
+
expect(cache.get("key1")).toBeNull(); // Entry should be expired after preserved TTL/SWR window
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("Cleanup Enhancement", () => {
|
|
440
|
+
it("should update lastCleanup timestamp", () => {
|
|
441
|
+
const startTime = Date.now();
|
|
442
|
+
vi.advanceTimersByTime(1000);
|
|
443
|
+
|
|
444
|
+
cache.set("key1", "value1", 500);
|
|
445
|
+
vi.advanceTimersByTime(600);
|
|
446
|
+
cache.cleanup();
|
|
447
|
+
|
|
448
|
+
const stats = cache.getStats();
|
|
449
|
+
expect(stats.lastCleanup).toBeGreaterThan(startTime);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should include cleanup removals in eviction count", () => {
|
|
453
|
+
cache.set("key1", "value1", 500);
|
|
454
|
+
cache.set("key2", "value2", 1000);
|
|
455
|
+
|
|
456
|
+
vi.advanceTimersByTime(600);
|
|
457
|
+
const cleaned = cache.cleanup();
|
|
458
|
+
|
|
459
|
+
expect(cleaned).toBe(1);
|
|
460
|
+
const stats = cache.getStats();
|
|
461
|
+
expect(stats.evictions).toBe(1);
|
|
462
|
+
expect(cache.get("key1")).toBeNull();
|
|
463
|
+
expect(cache.get("key2")).toBe("value2");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("should return zero when no cleanup needed", () => {
|
|
467
|
+
cache.set("key1", "value1", 5000);
|
|
468
|
+
const cleaned = cache.cleanup();
|
|
469
|
+
|
|
470
|
+
expect(cleaned).toBe(0);
|
|
471
|
+
const stats = cache.getStats();
|
|
472
|
+
expect(stats.evictions).toBe(0);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("should provide detailed cleanup information", () => {
|
|
476
|
+
cache.set("key1", "value1", 500);
|
|
477
|
+
cache.set("key2", "value2", 1000);
|
|
478
|
+
cache.set("key3", "value3", 5000);
|
|
479
|
+
|
|
480
|
+
vi.advanceTimersByTime(600);
|
|
481
|
+
const result = cache.cleanupDetailed();
|
|
482
|
+
|
|
483
|
+
expect(result.cleaned).toBe(1);
|
|
484
|
+
expect(result.evictions).toBe(1);
|
|
485
|
+
expect(cache.get("key1")).toBeNull();
|
|
486
|
+
expect(cache.get("key2")).toBe("value2");
|
|
487
|
+
expect(cache.get("key3")).toBe("value3");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should maintain backward compatibility with cleanup() method", () => {
|
|
491
|
+
cache.set("key1", "value1", 500);
|
|
492
|
+
cache.set("key2", "value2", 1000);
|
|
493
|
+
|
|
494
|
+
vi.advanceTimersByTime(600);
|
|
495
|
+
const cleaned = cache.cleanup();
|
|
496
|
+
|
|
497
|
+
expect(cleaned).toBe(1); // Should still return number of cleaned entries
|
|
498
|
+
const stats = cache.getStats();
|
|
499
|
+
expect(stats.evictions).toBe(1);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe("Environment Variable Configuration", () => {
|
|
504
|
+
it("should use environment variable for maxEntries", () => {
|
|
505
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "5";
|
|
506
|
+
const configuredCache = new CacheManager();
|
|
507
|
+
|
|
508
|
+
const stats = configuredCache.getStats();
|
|
509
|
+
expect(stats.maxEntries).toBe(5);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("should use environment variable for stale window", () => {
|
|
513
|
+
process.env.YNAB_MCP_CACHE_STALE_MS = "30000";
|
|
514
|
+
const configuredCache = new CacheManager();
|
|
515
|
+
|
|
516
|
+
configuredCache.set("key1", "value1", {
|
|
517
|
+
ttl: 1000,
|
|
518
|
+
staleWhileRevalidate: undefined,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// The default stale window should be used from env var
|
|
522
|
+
vi.advanceTimersByTime(15000); // Within default stale window from env
|
|
523
|
+
expect(configuredCache.get("key1")).toBe("value1"); // Served as stale data
|
|
524
|
+
|
|
525
|
+
vi.advanceTimersByTime(16100); // Beyond stale window now (total 31100ms > 31000ms)
|
|
526
|
+
expect(configuredCache.get("key1")).toBeNull();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("should fall back to defaults for invalid environment values", () => {
|
|
530
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "invalid";
|
|
531
|
+
process.env.YNAB_MCP_CACHE_STALE_MS = "not-a-number";
|
|
532
|
+
process.env.YNAB_MCP_CACHE_DEFAULT_TTL_MS = "invalid-ttl";
|
|
533
|
+
|
|
534
|
+
// Reset timers for new cache instance
|
|
535
|
+
vi.useRealTimers();
|
|
536
|
+
vi.useFakeTimers({ now: 0 });
|
|
537
|
+
|
|
538
|
+
const configuredCache = new CacheManager();
|
|
539
|
+
const stats = configuredCache.getStats();
|
|
540
|
+
|
|
541
|
+
expect(stats.maxEntries).toBe(1000); // Default value
|
|
542
|
+
|
|
543
|
+
// Test that invalid default TTL falls back to 300000ms (5 minutes)
|
|
544
|
+
configuredCache.set("key1", "value1");
|
|
545
|
+
vi.advanceTimersByTime(299000); // Just under 5 minutes - should be valid
|
|
546
|
+
expect(configuredCache.get("key1")).toBe("value1");
|
|
547
|
+
|
|
548
|
+
vi.advanceTimersByTime(2000); // ~5 minutes total - should expire
|
|
549
|
+
expect(configuredCache.get("key1")).toBeNull();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("should use environment variable for default TTL", () => {
|
|
553
|
+
process.env.YNAB_MCP_CACHE_DEFAULT_TTL_MS = "60000"; // 1 minute
|
|
554
|
+
|
|
555
|
+
// Reset timers for new cache instance
|
|
556
|
+
vi.useRealTimers();
|
|
557
|
+
vi.useFakeTimers({ now: 0 });
|
|
558
|
+
|
|
559
|
+
const configuredCache = new CacheManager();
|
|
560
|
+
|
|
561
|
+
configuredCache.set("key1", "value1"); // Use default TTL
|
|
562
|
+
vi.advanceTimersByTime(59000); // Just under 1 minute - should be valid
|
|
563
|
+
expect(configuredCache.get("key1")).toBe("value1");
|
|
564
|
+
|
|
565
|
+
vi.advanceTimersByTime(2000); // ~1 minute total - should expire
|
|
566
|
+
expect(configuredCache.get("key1")).toBeNull();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("should fall back to defaults when environment variables are missing", () => {
|
|
570
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = undefined;
|
|
571
|
+
process.env.YNAB_MCP_CACHE_STALE_MS = undefined;
|
|
572
|
+
process.env.YNAB_MCP_CACHE_DEFAULT_TTL_MS = undefined;
|
|
573
|
+
|
|
574
|
+
// Reset timers for new cache instance
|
|
575
|
+
vi.useRealTimers();
|
|
576
|
+
vi.useFakeTimers({ now: 0 });
|
|
577
|
+
|
|
578
|
+
const configuredCache = new CacheManager();
|
|
579
|
+
const stats = configuredCache.getStats();
|
|
580
|
+
|
|
581
|
+
expect(stats.maxEntries).toBe(1000); // Default value
|
|
582
|
+
|
|
583
|
+
// Test default TTL (300000ms = 5 minutes)
|
|
584
|
+
configuredCache.set("key1", "value1");
|
|
585
|
+
vi.advanceTimersByTime(299000); // Just under 5 minutes - should be valid
|
|
586
|
+
expect(configuredCache.get("key1")).toBe("value1");
|
|
587
|
+
|
|
588
|
+
vi.advanceTimersByTime(2000); // ~5 minutes total - should expire
|
|
589
|
+
expect(configuredCache.get("key1")).toBeNull();
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe("Enhanced Statistics", () => {
|
|
594
|
+
it("should return comprehensive stats", () => {
|
|
595
|
+
cache.set("key1", "value1");
|
|
596
|
+
cache.get("key1");
|
|
597
|
+
cache.get("nonexistent");
|
|
598
|
+
|
|
599
|
+
const stats = cache.getStats();
|
|
600
|
+
|
|
601
|
+
expect(stats).toEqual({
|
|
602
|
+
size: 1,
|
|
603
|
+
keys: ["key1"],
|
|
604
|
+
hits: 1,
|
|
605
|
+
misses: 1,
|
|
606
|
+
evictions: 0,
|
|
607
|
+
lastCleanup: null,
|
|
608
|
+
maxEntries: 1000,
|
|
609
|
+
hitRate: 0.5,
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("should maintain backward compatibility with basic stats", () => {
|
|
614
|
+
cache.set("key1", "value1");
|
|
615
|
+
cache.set("key2", "value2");
|
|
616
|
+
|
|
617
|
+
const stats = cache.getStats();
|
|
618
|
+
|
|
619
|
+
// Basic fields should always be present
|
|
620
|
+
expect(stats).toHaveProperty("size", 2);
|
|
621
|
+
expect(stats).toHaveProperty("keys");
|
|
622
|
+
expect(stats.keys).toEqual(["key1", "key2"]);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("should handle getEntriesForSizeEstimation correctly", () => {
|
|
626
|
+
cache.set("key1", "value1", 1000);
|
|
627
|
+
cache.set("key2", "value2", 2000);
|
|
628
|
+
|
|
629
|
+
vi.advanceTimersByTime(1500);
|
|
630
|
+
|
|
631
|
+
const entries = cache.getEntriesForSizeEstimation();
|
|
632
|
+
expect(entries).toHaveLength(1); // Only non-expired entry
|
|
633
|
+
expect(entries[0][0]).toBe("key2");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("should provide lightweight cache metadata without full entry data", () => {
|
|
637
|
+
cache.set("key1", "string-value", 1000);
|
|
638
|
+
cache.set("key2", { prop: "object" }, 2000);
|
|
639
|
+
cache.set("key3", 42, { ttl: 3000, staleWhileRevalidate: 1000 });
|
|
640
|
+
|
|
641
|
+
vi.advanceTimersByTime(1500); // key1 should be expired
|
|
642
|
+
|
|
643
|
+
const metadata = cache.getCacheMetadata();
|
|
644
|
+
expect(metadata).toHaveLength(3);
|
|
645
|
+
|
|
646
|
+
// Check expired entry
|
|
647
|
+
const key1Meta = metadata.find((m) => m.key === "key1");
|
|
648
|
+
expect(key1Meta).toEqual({
|
|
649
|
+
key: "key1",
|
|
650
|
+
timestamp: expect.any(Number),
|
|
651
|
+
ttl: 1000,
|
|
652
|
+
staleWhileRevalidate: undefined,
|
|
653
|
+
dataType: "string",
|
|
654
|
+
isExpired: true,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Check non-expired entry
|
|
658
|
+
const key2Meta = metadata.find((m) => m.key === "key2");
|
|
659
|
+
expect(key2Meta).toEqual({
|
|
660
|
+
key: "key2",
|
|
661
|
+
timestamp: expect.any(Number),
|
|
662
|
+
ttl: 2000,
|
|
663
|
+
staleWhileRevalidate: undefined,
|
|
664
|
+
dataType: "object",
|
|
665
|
+
isExpired: false,
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Check entry with staleWhileRevalidate
|
|
669
|
+
const key3Meta = metadata.find((m) => m.key === "key3");
|
|
670
|
+
expect(key3Meta).toEqual({
|
|
671
|
+
key: "key3",
|
|
672
|
+
timestamp: expect.any(Number),
|
|
673
|
+
ttl: 3000,
|
|
674
|
+
staleWhileRevalidate: 1000,
|
|
675
|
+
dataType: "number",
|
|
676
|
+
isExpired: false,
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe("Edge Cases and Error Handling", () => {
|
|
682
|
+
it("should handle circular references in cache values", () => {
|
|
683
|
+
const circular: any = { name: "test" };
|
|
684
|
+
circular.self = circular;
|
|
685
|
+
|
|
686
|
+
expect(() => cache.set("key1", circular)).not.toThrow();
|
|
687
|
+
expect(cache.get("key1")).toBe(circular);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("should handle very large cache sizes", () => {
|
|
691
|
+
process.env.YNAB_MCP_CACHE_MAX_ENTRIES = "10000";
|
|
692
|
+
const largeCache = new CacheManager();
|
|
693
|
+
|
|
694
|
+
// Add many entries
|
|
695
|
+
for (let i = 0; i < 5000; i++) {
|
|
696
|
+
largeCache.set(`key${i}`, `value${i}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
expect(largeCache.getStats().size).toBe(5000);
|
|
700
|
+
expect(largeCache.get("key0")).toBe("value0");
|
|
701
|
+
expect(largeCache.get("key4999")).toBe("value4999");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("should handle concurrent wrap calls with different keys independently", async () => {
|
|
705
|
+
const loader1 = vi.fn().mockResolvedValue("value1");
|
|
706
|
+
const loader2 = vi.fn().mockResolvedValue("value2");
|
|
707
|
+
|
|
708
|
+
const [result1, result2] = await Promise.all([
|
|
709
|
+
cache.wrap("key1", { loader: loader1 }),
|
|
710
|
+
cache.wrap("key2", { loader: loader2 }),
|
|
711
|
+
]);
|
|
712
|
+
|
|
713
|
+
expect(result1).toBe("value1");
|
|
714
|
+
expect(result2).toBe("value2");
|
|
715
|
+
expect(loader1).toHaveBeenCalledTimes(1);
|
|
716
|
+
expect(loader2).toHaveBeenCalledTimes(1);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("should clean up failed operations", async () => {
|
|
720
|
+
const loader = vi.fn().mockRejectedValue(new Error("Failed"));
|
|
721
|
+
|
|
722
|
+
await expect(cache.wrap("key1", { loader })).rejects.toThrow("Failed");
|
|
723
|
+
|
|
724
|
+
// Subsequent call should try again
|
|
725
|
+
const loader2 = vi.fn().mockResolvedValue("success");
|
|
726
|
+
const result = await cache.wrap("key1", { loader: loader2 });
|
|
727
|
+
|
|
728
|
+
expect(result).toBe("success");
|
|
729
|
+
expect(loader2).toHaveBeenCalledTimes(1);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe("Prefix and Budget-based Deletion", () => {
|
|
734
|
+
describe("deleteByPrefix", () => {
|
|
735
|
+
it("should delete entries matching prefix and return count", () => {
|
|
736
|
+
cache.set("transactions:list:budget-123", "list");
|
|
737
|
+
cache.set("transactions:get:budget-123", "detail");
|
|
738
|
+
cache.set("accounts:list:budget-123", "accounts");
|
|
739
|
+
|
|
740
|
+
const removed = cache.deleteByPrefix("transactions:");
|
|
741
|
+
expect(removed).toBe(2);
|
|
742
|
+
expect(cache.getKeys()).toEqual(["accounts:list:budget-123"]);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("should return 0 when no matches found", () => {
|
|
746
|
+
cache.set("accounts:list:budget-123", "accounts");
|
|
747
|
+
const removed = cache.deleteByPrefix("payments:");
|
|
748
|
+
expect(removed).toBe(0);
|
|
749
|
+
expect(cache.getKeys()).toEqual(["accounts:list:budget-123"]);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it("should handle empty prefix safely", () => {
|
|
753
|
+
cache.set("transactions:list:budget-123", "list");
|
|
754
|
+
cache.set("accounts:list:budget-123", "accounts");
|
|
755
|
+
|
|
756
|
+
const removed = cache.deleteByPrefix("");
|
|
757
|
+
expect(removed).toBe(0);
|
|
758
|
+
expect(cache.getKeys()).toEqual([
|
|
759
|
+
"transactions:list:budget-123",
|
|
760
|
+
"accounts:list:budget-123",
|
|
761
|
+
]);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("should not delete when prefix only partially matches a resource's namespace", () => {
|
|
765
|
+
cache.set("transactions:list:budget-123", "list");
|
|
766
|
+
cache.set("accounts:list:budget-123", "accounts");
|
|
767
|
+
|
|
768
|
+
const removed = cache.deleteByPrefix("trans");
|
|
769
|
+
expect(removed).toBe(0);
|
|
770
|
+
expect(cache.getKeys()).toEqual([
|
|
771
|
+
"transactions:list:budget-123",
|
|
772
|
+
"accounts:list:budget-123",
|
|
773
|
+
]);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("should not affect cache hit or miss counters", () => {
|
|
777
|
+
cache.set("transactions:list:budget-123", "list");
|
|
778
|
+
cache.set("transactions:list:budget-456", "list");
|
|
779
|
+
|
|
780
|
+
const before = cache.getStats();
|
|
781
|
+
cache.deleteByPrefix("transactions:");
|
|
782
|
+
const after = cache.getStats();
|
|
783
|
+
|
|
784
|
+
expect(after.hits).toBe(before.hits);
|
|
785
|
+
expect(after.misses).toBe(before.misses);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
describe("deleteByBudgetId", () => {
|
|
790
|
+
it("should delete entries containing the provided budget ID", () => {
|
|
791
|
+
cache.set("transactions:list:budget-123", "txn");
|
|
792
|
+
cache.set("accounts:list:budget-123", "acct");
|
|
793
|
+
cache.set("transactions:list:budget-456", "other");
|
|
794
|
+
|
|
795
|
+
const removed = cache.deleteByBudgetId("budget-123");
|
|
796
|
+
expect(removed).toBe(2);
|
|
797
|
+
expect(cache.getKeys()).toEqual(["transactions:list:budget-456"]);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("should return 0 when budget ID does not exist in cache", () => {
|
|
801
|
+
cache.set("transactions:list:budget-123", "txn");
|
|
802
|
+
|
|
803
|
+
const removed = cache.deleteByBudgetId("budget-999");
|
|
804
|
+
expect(removed).toBe(0);
|
|
805
|
+
expect(cache.getKeys()).toEqual(["transactions:list:budget-123"]);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("should not match budget IDs that are substrings of other IDs", () => {
|
|
809
|
+
cache.set("transactions:list:budget-123", "txn");
|
|
810
|
+
cache.set("transactions:list:budget-1234", "txn2");
|
|
811
|
+
|
|
812
|
+
const removed = cache.deleteByBudgetId("budget-1");
|
|
813
|
+
expect(removed).toBe(0);
|
|
814
|
+
expect(cache.getKeys()).toEqual([
|
|
815
|
+
"transactions:list:budget-123",
|
|
816
|
+
"transactions:list:budget-1234",
|
|
817
|
+
]);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("should handle UUID formatted budget identifiers", () => {
|
|
821
|
+
const uuid = "123e4567-e89b-12d3-a456-426614174000";
|
|
822
|
+
cache.set(`transactions:list:${uuid}`, "txn");
|
|
823
|
+
cache.set(`accounts:list:${uuid}`, "acct");
|
|
824
|
+
cache.set("transactions:list:budget-456", "other");
|
|
825
|
+
|
|
826
|
+
const removed = cache.deleteByBudgetId(uuid);
|
|
827
|
+
expect(removed).toBe(2);
|
|
828
|
+
expect(cache.getKeys()).toEqual(["transactions:list:budget-456"]);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("should not affect cache stats when deleting by budget ID", () => {
|
|
832
|
+
cache.set("transactions:list:budget-123", "txn");
|
|
833
|
+
cache.set("transactions:list:budget-456", "other");
|
|
834
|
+
|
|
835
|
+
const before = cache.getStats();
|
|
836
|
+
cache.deleteByBudgetId("budget-123");
|
|
837
|
+
const after = cache.getStats();
|
|
838
|
+
|
|
839
|
+
expect(after.hits).toBe(before.hits);
|
|
840
|
+
expect(after.misses).toBe(before.misses);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
describe("getKeys", () => {
|
|
845
|
+
it("should return an empty array when cache is empty", () => {
|
|
846
|
+
expect(cache.getKeys()).toEqual([]);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it("should return all cache keys", () => {
|
|
850
|
+
cache.set("accounts:list:budget-123", "acct");
|
|
851
|
+
cache.set("transactions:list:budget-123", "txn");
|
|
852
|
+
cache.set("payees:list:budget-123", "payees");
|
|
853
|
+
|
|
854
|
+
expect(cache.getKeys()).toEqual([
|
|
855
|
+
"accounts:list:budget-123",
|
|
856
|
+
"transactions:list:budget-123",
|
|
857
|
+
"payees:list:budget-123",
|
|
858
|
+
]);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("should preserve insertion order of cache keys", () => {
|
|
862
|
+
cache.set("key-a", "a");
|
|
863
|
+
cache.set("key-b", "b");
|
|
864
|
+
cache.set("key-c", "c");
|
|
865
|
+
|
|
866
|
+
expect(cache.getKeys()).toEqual(["key-a", "key-b", "key-c"]);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
describe("Integration with Existing Patterns", () => {
|
|
872
|
+
it("should work with existing tool usage patterns", () => {
|
|
873
|
+
// Simulate existing usage pattern from tools
|
|
874
|
+
const key = CacheManager.generateKey("budgets", "user123");
|
|
875
|
+
cache.set(key, { budgets: ["budget1", "budget2"] }, 10 * 60 * 1000);
|
|
876
|
+
|
|
877
|
+
const cached = cache.get(key);
|
|
878
|
+
expect(cached).toEqual({ budgets: ["budget1", "budget2"] });
|
|
879
|
+
|
|
880
|
+
const stats = cache.getStats();
|
|
881
|
+
expect(stats.hits).toBe(1);
|
|
882
|
+
expect(stats.size).toBe(1);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("should maintain singleton behavior", async () => {
|
|
886
|
+
// The imported singleton should work consistently
|
|
887
|
+
const { cacheManager } = await import("../cacheManager.js");
|
|
888
|
+
|
|
889
|
+
cacheManager.set("singleton-test", "value");
|
|
890
|
+
expect(cacheManager.get("singleton-test")).toBe("value");
|
|
891
|
+
|
|
892
|
+
const stats = cacheManager.getStats();
|
|
893
|
+
expect(stats).toHaveProperty("hits");
|
|
894
|
+
expect(stats).toHaveProperty("misses");
|
|
895
|
+
});
|
|
896
|
+
});
|
|
880
897
|
});
|