@dizzlkheinz/ynab-mcpb 0.17.0 → 0.18.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.
Files changed (182) hide show
  1. package/.env.example +33 -33
  2. package/.github/workflows/ci-tests.yml +45 -45
  3. package/.github/workflows/claude-code-review.yml +57 -57
  4. package/.github/workflows/claude.yml +50 -50
  5. package/.github/workflows/full-integration.yml +22 -22
  6. package/.github/workflows/publish.yml +12 -3
  7. package/.github/workflows/release.yml +2 -2
  8. package/CHANGELOG.md +10 -1
  9. package/CLAUDE.md +16 -12
  10. package/README.md +6 -1
  11. package/dist/bundle/index.cjs +49 -49
  12. package/dist/server/YNABMCPServer.d.ts +125 -54
  13. package/dist/server/YNABMCPServer.js +42 -11
  14. package/dist/server/cacheManager.js +6 -5
  15. package/dist/server/completions.d.ts +25 -0
  16. package/dist/server/completions.js +160 -0
  17. package/dist/server/config.d.ts +2 -2
  18. package/dist/server/errorHandler.js +1 -0
  19. package/dist/server/rateLimiter.js +3 -1
  20. package/dist/server/resources.d.ts +1 -0
  21. package/dist/server/resources.js +33 -16
  22. package/dist/server/securityMiddleware.d.ts +38 -8
  23. package/dist/server/securityMiddleware.js +1 -0
  24. package/dist/server/toolRegistry.d.ts +9 -0
  25. package/dist/server/toolRegistry.js +11 -0
  26. package/dist/tools/adapters.d.ts +3 -1
  27. package/dist/tools/adapters.js +1 -0
  28. package/dist/tools/reconciliation/executor.d.ts +2 -0
  29. package/dist/tools/reconciliation/executor.js +26 -1
  30. package/dist/tools/reconciliation/index.d.ts +3 -2
  31. package/dist/tools/reconciliation/index.js +4 -3
  32. package/dist/tools/schemas/outputs/index.d.ts +2 -2
  33. package/dist/tools/schemas/outputs/index.js +2 -2
  34. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +0 -15
  35. package/dist/tools/schemas/outputs/utilityOutputs.js +0 -9
  36. package/dist/tools/utilityTools.d.ts +0 -7
  37. package/dist/tools/utilityTools.js +1 -50
  38. package/docs/maintainers/npm-publishing.md +27 -0
  39. package/docs/reference/API.md +83 -97
  40. package/docs/technical/reconciliation-system-architecture.md +2251 -2251
  41. package/package.json +6 -6
  42. package/scripts/analyze-bundle.mjs +41 -41
  43. package/scripts/generate-mcpb.ps1 +95 -95
  44. package/scripts/watch-and-restart.ps1 +49 -49
  45. package/src/__tests__/comprehensive.integration.test.ts +4 -32
  46. package/src/__tests__/performance.test.ts +5 -14
  47. package/src/__tests__/setup.ts +45 -14
  48. package/src/__tests__/smoke.e2e.test.ts +70 -0
  49. package/src/__tests__/testUtils.ts +2 -113
  50. package/src/server/YNABMCPServer.ts +64 -10
  51. package/src/server/__tests__/YNABMCPServer.test.ts +0 -1
  52. package/src/server/__tests__/completions.integration.test.ts +117 -0
  53. package/src/server/__tests__/completions.test.ts +319 -0
  54. package/src/server/__tests__/resources.template.test.ts +3 -3
  55. package/src/server/__tests__/resources.test.ts +3 -3
  56. package/src/server/__tests__/toolRegistration.test.ts +3 -3
  57. package/src/server/cacheManager.ts +7 -6
  58. package/src/server/completions.ts +279 -0
  59. package/src/server/errorHandler.ts +1 -0
  60. package/src/server/rateLimiter.ts +4 -1
  61. package/src/server/resources.ts +49 -13
  62. package/src/server/securityMiddleware.ts +1 -0
  63. package/src/server/toolRegistry.ts +42 -0
  64. package/src/tools/__tests__/transactionTools.integration.test.ts +63 -3
  65. package/src/tools/__tests__/utilityTools.integration.test.ts +1 -85
  66. package/src/tools/__tests__/utilityTools.test.ts +1 -123
  67. package/src/tools/adapters.ts +22 -1
  68. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +462 -0
  69. package/src/tools/reconciliation/executor.ts +55 -1
  70. package/src/tools/reconciliation/index.ts +7 -3
  71. package/src/tools/schemas/outputs/index.ts +0 -3
  72. package/src/tools/schemas/outputs/utilityOutputs.ts +2 -43
  73. package/src/tools/toolCategories.ts +0 -1
  74. package/src/tools/utilityTools.ts +5 -76
  75. package/vitest.config.ts +4 -1
  76. package/.chunkhound.json +0 -11
  77. package/.code/agents/0098661e-0fa3-4990-beb9-c0cbf3f123aa/status.txt +0 -1
  78. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +0 -3
  79. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +0 -3
  80. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +0 -1
  81. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +0 -5
  82. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +0 -1569
  83. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +0 -3
  84. package/.code/agents/1324/exec-call_tIpx9uV1TpARbAMZonRQm8AO.txt +0 -757
  85. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +0 -2832
  86. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +0 -2709
  87. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +0 -2832
  88. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +0 -2832
  89. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +0 -2709
  90. package/.code/agents/1572/exec-call_GjVFBFOWcY7lE0idc5nWlLNh.txt +0 -781
  91. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +0 -1
  92. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +0 -5217
  93. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +0 -2594
  94. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +0 -2594
  95. package/.code/agents/1846/exec-call_1YNAVD18RjrMN7JnfkkQhUP3.txt +0 -766
  96. package/.code/agents/1846/exec-call_lh3lDzE4WJAh1lFiomiiZ73D.txt +0 -766
  97. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +0 -231
  98. package/.code/agents/2038/exec-call_DYwOukaYsL8VCONWmV2rUW5u.txt +0 -766
  99. package/.code/agents/2038/exec-call_c7fOQ7UrpVcTtvdfGBRM146V.txt +0 -652
  100. package/.code/agents/2038/exec-call_ySNyq9Mm55jWE480s54r5QcA.txt +0 -766
  101. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +0 -2590
  102. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +0 -5195
  103. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +0 -286
  104. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +0 -218
  105. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +0 -180
  106. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +0 -3
  107. package/.code/agents/2256/exec-call_AtPcRWPmFPMcmX6qOFm1fCEY.txt +0 -766
  108. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +0 -1
  109. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +0 -747
  110. package/.code/agents/2454/exec-call_aFJpupwjfZeOBm7ixI5Vc8z2.txt +0 -766
  111. package/.code/agents/2454/exec-call_wogZ4HfXTodTEXvdgXlVUBpv.txt +0 -766
  112. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +0 -1
  113. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +0 -2594
  114. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +0 -2594
  115. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +0 -144
  116. package/.code/agents/2e905864-aa07-4314-bcf9-c5b32277e4ac/result.txt +0 -36
  117. package/.code/agents/3073/exec-call_Peeagc9DxGYLgE6pNdMZhqIE.txt +0 -766
  118. package/.code/agents/3073/exec-call_d2YSE3hXF08KRSoUM3qd8Z3x.txt +0 -766
  119. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +0 -416
  120. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +0 -2590
  121. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +0 -2590
  122. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +0 -2590
  123. package/.code/agents/335aa031-466d-4fb7-925f-3cd864e264d0/result.txt +0 -191
  124. package/.code/agents/3364/exec-call_NbhIrsM5HhyDZDmJZG5CuCYL.txt +0 -766
  125. package/.code/agents/3364/exec-call_cKtJg0NrXiwXEFwlsE3uPZRA.txt +0 -766
  126. package/.code/agents/36d98414-5cde-4d9d-9a67-a240a18c1f07/result.txt +0 -189
  127. package/.code/agents/4604e866-b7b8-44f5-992f-2f683b0a523b/status.txt +0 -1
  128. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +0 -790
  129. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +0 -766
  130. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +0 -790
  131. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +0 -2594
  132. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +0 -1000
  133. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +0 -3489
  134. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +0 -766
  135. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +0 -2594
  136. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +0 -2456
  137. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +0 -2594
  138. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +0 -18
  139. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +0 -48
  140. package/.code/agents/5f8dc01c-47b3-4163-b0b3-aa31be89fcdc/status.txt +0 -1
  141. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +0 -1
  142. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +0 -1
  143. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +0 -1
  144. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +0 -1
  145. package/.code/agents/7/exec-call_HltHpkDox0Zm1vGEjdksUgpE.txt +0 -1120
  146. package/.code/agents/7/exec-call_LCATrOPPAgbxW9Q1z0XaVi2E.txt +0 -2646
  147. package/.code/agents/7/exec-call_W8DeRfNG9hvbgVFvf0clBf6R.txt +0 -2646
  148. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +0 -1271
  149. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +0 -1570
  150. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +0 -2590
  151. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +0 -3
  152. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +0 -3299
  153. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +0 -3299
  154. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +0 -1882
  155. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +0 -2594
  156. package/.code/agents/94a0ddf3-a304-4ec3-913e-3cceef509948/error.txt +0 -1
  157. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +0 -1
  158. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +0 -170
  159. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +0 -1
  160. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +0 -3
  161. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +0 -1
  162. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +0 -1
  163. package/.code/agents/e2c752b7-711d-423a-af57-f53c809deb84/result.txt +0 -160
  164. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +0 -3
  165. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +0 -1
  166. package/.code/agents/e6601719-c31f-4a0e-8c71-d70787d0ab71/status.txt +0 -1
  167. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +0 -1
  168. package/.code/agents/f250b7ed-5bd5-4036-aa8c-ce63caee7d61/result.txt +0 -20
  169. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +0 -5
  170. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +0 -1
  171. package/AGENTS.md +0 -1
  172. package/NUL +0 -0
  173. package/package.json.tmp +0 -105
  174. package/src/__tests__/delta.performance.test.ts +0 -80
  175. package/src/__tests__/workflows.e2e.test.ts +0 -1702
  176. package/temp-recon.ts +0 -126
  177. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +0 -23
  178. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +0 -23
  179. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +0 -44
  180. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +0 -58
  181. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +0 -3662
  182. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +0 -115
@@ -1 +0,0 @@
1
- Failed to spawn sandboxed agent: batch file arguments are invalid
@@ -1 +0,0 @@
1
- Failed to execute code-gpt-5.1-codex-mini: program not found
@@ -1,170 +0,0 @@
1
- I'm using the writing-plans skill to create the implementation plan.
2
-
3
- # MCP Resilience Implementation Plan
4
-
5
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
6
-
7
- **Goal:** Harden the MCP server by validating every tool’s metadata schema and making `clear_cache` fully reset delta/caching state so knowledge doesn’t drift while keeping diagnostics accurate.
8
-
9
- **Architecture:** Validate metadata against generated `zod` schemas inside `ToolRegistry`, expose mismatches immediately, and delegate cache clearing to `DeltaCache.forceFullRefresh()` so both the LRU cache and `ServerKnowledgeStore` reset before diagnostics observe the new state.
10
-
11
- **Tech Stack:** TypeScript 5/Node 18+, `zod`/`zod-validation-error`, MCP SDK server wiring in `src/server`, Vitest for fast unit/integration suites, and Markdown docs in `docs/reference/TOOLS.md`.
12
-
13
- ### Task 1: Tighten tool metadata schema validation
14
-
15
- **Files:**
16
- - Modify: `src/server/toolRegistry.ts`
17
- - Modify: `src/server/__tests__/toolRegistry.test.ts`
18
-
19
- **Step 1: Write the failing test**
20
-
21
- ```ts
22
- it('rejects registration when metadata inputJsonSchema diverges from the handler schema', () => {
23
- const registry = new ToolRegistry({
24
- withSecurityWrapper: vi.fn().mockReturnValue(() => async (handler) => handler({})),
25
- errorHandler: mockedErrorHandler,
26
- responseFormatter: responseFormatter,
27
- });
28
- const mismatchedMetadata = { type: 'object', properties: { bogus: { type: 'string' } } };
29
- expect(() =>
30
- registry.register({
31
- name: 'budget-sync',
32
- description: 'Sync budget data',
33
- inputSchema: z.object({ budget_id: z.string().min(1) }).strict(),
34
- handler: async () => ({ content: [{ type: 'text', text: responseFormatter.format({}) }] }),
35
- metadata: { inputJsonSchema: mismatchedMetadata },
36
- }),
37
- ).toThrow('metadata input schema must match generated schema for tool "budget-sync"');
38
- });
39
- ```
40
-
41
- **Step 2: Run the test to verify it fails**
42
-
43
- Run: `npm run test:unit -- --runInBand src/server/__tests__/toolRegistry.test.ts --testNamePattern "metadata input schema"`
44
- Expected: FAIL because the registry currently accepts mismatched metadata.
45
-
46
- **Step 3: Implement the minimal enforcement**
47
-
48
- ```ts
49
- private ensureMetadataSchemaMatches(definition: ToolDefinition): void {
50
- const generatedSchema = this.generateJsonSchema(definition.inputSchema, 'input');
51
- if (definition.metadata?.inputJsonSchema) {
52
- if (JSON.stringify(generatedSchema) !== JSON.stringify(definition.metadata.inputJsonSchema)) {
53
- throw new Error(
54
- `metadata input schema must match generated schema for tool "${definition.name}"`,
55
- );
56
- }
57
- } else {
58
- definition.metadata = {
59
- ...definition.metadata,
60
- inputJsonSchema: generatedSchema,
61
- };
62
- }
63
- }
64
- ```
65
-
66
- Call `this.ensureMetadataSchemaMatches(definition);` near the top of `register` before storing the tool so `metadata.inputJsonSchema` is always populated (and validated) for `listTools()`. This also means `ToolRegistry.listTools()` no longer needs to fall back to `generateJsonSchema` when metadata is present.
67
-
68
- **Step 4: Run the test to confirm it now passes**
69
-
70
- Run: `npm run test:unit -- --runInBand src/server/__tests__/toolRegistry.test.ts --testNamePattern "metadata input schema"`
71
- Expected: PASS.
72
-
73
- **Step 5: Commit**
74
-
75
- ```bash
76
- git add src/server/toolRegistry.ts src/server/__tests__/toolRegistry.test.ts
77
- git commit -m "fix: validate tool metadata schemas at registration"
78
- ```
79
-
80
- ### Task 2: Reset delta knowledge when clearing the cache
81
-
82
- **Files:**
83
- - Modify: `src/server/YNABMCPServer.ts`
84
- - Modify: `src/server/__tests__/YNABMCPServer.integration.test.ts`
85
- - Modify: `docs/reference/TOOLS.md`
86
-
87
- **Step 1: Write the failing integration test**
88
-
89
- ```ts
90
- it(
91
- 'resets delta knowledge entries when clear_cache runs',
92
- { meta: { tier: 'domain', domain: 'server' } },
93
- async (ctx) => {
94
- await skipOnRateLimit(async () => {
95
- const token = accessToken();
96
- await registry.executeTool({
97
- name: 'list_accounts',
98
- accessToken: token,
99
- arguments: { budget_id: budgetsResult?.budgets?.[0]?.id },
100
- });
101
-
102
- const before = await registry.executeTool({
103
- name: 'diagnostic_info',
104
- accessToken: token,
105
- arguments: { include_delta: true },
106
- });
107
- const beforeData = JSON.parse(before.content?.[0]?.text ?? '{}');
108
- expect(beforeData.delta?.knowledge_entries).toBeGreaterThan(0);
109
-
110
- await registry.executeTool({ name: 'clear_cache', accessToken: token, arguments: {} });
111
-
112
- const after = await registry.executeTool({
113
- name: 'diagnostic_info',
114
- accessToken: token,
115
- arguments: { include_delta: true },
116
- });
117
- const afterData = JSON.parse(after.content?.[0]?.text ?? '{}');
118
- expect(afterData.delta?.knowledge_entries).toBe(0);
119
- }, ctx);
120
- },
121
- );
122
- ```
123
-
124
- **Step 2: Run the integration test to verify it fails**
125
-
126
- Run: `npm run test:integration:domain -- --testNamePattern "clear cache runs"`
127
- Expected: FAIL because `clear_cache` currently only clears `cacheManager`, leaving `ServerKnowledgeStore` populated.
128
-
129
- **Step 3: Implement the minimal fix**
130
-
131
- - Update the `clear_cache` handler in `setupToolRegistry` to call `this.deltaCache.forceFullRefresh()` instead of directly `cacheManager.clear()`, ensuring both the cache and knowledge store reset:
132
-
133
- ```ts
134
- handler: async () => {
135
- this.deltaCache.forceFullRefresh();
136
- return {
137
- content: [{ type: 'text', text: responseFormatter.format({ success: true }) }],
138
- };
139
- },
140
- ```
141
-
142
- - Update `docs/reference/TOOLS.md` entry to explain the stronger guarantee:
143
-
144
- ```md
145
- | `clear_cache` | Clear the cache and reset delta knowledge so clients can start fresh | None |
146
- ```
147
-
148
- **Step 4: Run the integration test to confirm it passes**
149
-
150
- Run: `npm run test:integration:domain -- --testNamePattern "clear cache runs"`
151
- Expected: PASS.
152
-
153
- **Step 5: Commit**
154
-
155
- ```bash
156
- git add src/server/YNABMCPServer.ts src/server/__tests__/YNABMCPServer.integration.test.ts docs/reference/TOOLS.md
157
- git commit -m "feat: reset delta knowledge when cache is cleared"
158
- ```
159
-
160
- ---
161
-
162
- Because the sandbox is read-only, I couldn’t save this plan to `docs/plans/2025-11-21-mcp-resilience.md`; please copy the above Markdown there in a workspace-write session before executing the tasks.
163
-
164
- - **Plan scope:** Adds schema validation in `src/server/toolRegistry.ts` plus a new regression test in `src/server/__tests__/toolRegistry.test.ts`, and modifies `clear_cache` in `src/server/YNABMCPServer.ts` with an integration test addition and doc update in `docs/reference/TOOLS.md`.
165
- - **Testing:** Run the targeted unit test via `npm run test:unit -- --runInBand src/server/__tests__/toolRegistry.test.ts --testNamePattern "metadata input schema"` and the integration slice via `npm run test:integration:domain -- --testNamePattern "clear cache runs"` after making the code changes.
166
-
167
- Next steps:
168
- 1. Execute the described plan in a writable worktree (create `docs/plans/2025-11-21-mcp-resilience.md` with the above content first) and implement Task 1’s schema validation.
169
- 2. Run the targeted unit test from Task 1 to confirm failure, then fix the code, rerun to confirm success.
170
- 3. Implement Task 2, add the integration test, update `clear_cache`, refresh `docs/reference/TOOLS.md`, and run `npm run test:integration:domain -- --testNamePattern "clear cache runs"`.
@@ -1 +0,0 @@
1
- Required agent 'gemini' is not installed or not in PATH
@@ -1,3 +0,0 @@
1
- Command failed: Error loading configuration: config profile `Smoke test: from the current repo, run `pwd` and `git status -sb`, then list top-level files/directories and print a short summary. No file writes or edits.
2
-
3
- [Running in read-only mode - no modifications allowed]` not found
@@ -1 +0,0 @@
1
- Agent cancelled
@@ -1 +0,0 @@
1
- Agent cancelled
@@ -1,160 +0,0 @@
1
- Here’s a concise blueprint for Phase 1 that matches the factory pattern doc and repo conventions.
2
-
3
- **Module layout (production-ready, minimal)**
4
- - `src/types/toolRegistration.ts`: canonical types/interfaces for tool definitions and adapters.
5
- - `src/tools/adapters/index.ts`: adapter factory functions for the five categories (adapt, adaptNoInput, adaptWithDelta, adaptWrite, inline); export typed helpers.
6
- - `src/tools/schemas/common.ts`: shared zod schemas for inputs/outputs/memo/delta, reusable across tools.
7
- - `src/tools/index.ts`: re-export adapters and shared schemas (and future tool registrations).
8
- - Tests: `src/__tests__/tools/adapters.test.ts` (unit, Vitest).
9
-
10
- **Core types (toolRegistration.ts)**
11
- ```ts
12
- import { z } from 'zod';
13
-
14
- export type ToolKind =
15
- | 'adapt' // input schema -> handler(args) -> output
16
- | 'adaptNoInput' // no input -> handler() -> output
17
- | 'adaptWithDelta' // input + delta/metadata -> output + delta?
18
- | 'adaptWrite' // input triggers mutation; output includes status
19
- | 'inline'; // small inline tools (no registration factory)
20
-
21
- export interface ToolContext {
22
- logger?: { debug?: (...args: any[]) => void; info?: (...args: any[]) => void; error?: (...args: any[]) => void };
23
- now?: () => Date;
24
- // extend later with cache, env, config
25
- }
26
-
27
- export type ToolHandler<Input, Output> = (args: {
28
- input: Input;
29
- context?: ToolContext;
30
- }) => Promise<Output> | Output;
31
-
32
- export type ToolHandlerNoInput<Output> = (args?: { context?: ToolContext }) => Promise<Output> | Output;
33
-
34
- export interface ToolRegistration<Input, Output> {
35
- id: string; // kebab-case tool id
36
- description: string;
37
- inputSchema: z.ZodType<Input>;
38
- outputSchema?: z.ZodType<Output>;
39
- handler: ToolHandler<Input, Output>;
40
- kind?: ToolKind; // optional hint for tooling/metrics
41
- }
42
-
43
- export interface ToolRegistrationNoInput<Output>
44
- extends Omit<ToolRegistration<undefined, Output>, 'inputSchema' | 'handler'> {
45
- handler: ToolHandlerNoInput<Output>;
46
- }
47
- ```
48
-
49
- **Shared schemas (schemas/common.ts)**
50
- ```ts
51
- import { z } from 'zod';
52
-
53
- export const memoSchema = z.string().max(500).optional();
54
- export const baseInputSchema = z.object({ memo: memoSchema }).strip();
55
- export const deltaSchema = z.object({
56
- deltaToken: z.string(),
57
- revision: z.number().int().nonnegative(),
58
- });
59
- export const statusSchema = z.object({
60
- ok: z.boolean(),
61
- message: z.string().optional(),
62
- });
63
- ```
64
-
65
- **Adapters (tools/adapters/index.ts)**
66
- ```ts
67
- import { z } from 'zod';
68
- import {
69
- ToolRegistration,
70
- ToolRegistrationNoInput,
71
- ToolHandler,
72
- ToolHandlerNoInput,
73
- } from '../../types/toolRegistration';
74
-
75
- type AdapterResult<Input, Output> = ToolRegistration<Input, Output>;
76
-
77
- export function adapt<Input, Output>(
78
- config: Omit<ToolRegistration<Input, Output>, 'kind'>,
79
- ): AdapterResult<Input, Output> {
80
- return { ...config, kind: 'adapt' };
81
- }
82
-
83
- export function adaptNoInput<Output>(
84
- config: Omit<ToolRegistrationNoInput<Output>, 'kind' | 'inputSchema'>,
85
- ): AdapterResult<undefined, Output> {
86
- return {
87
- ...config,
88
- inputSchema: z.void(),
89
- kind: 'adaptNoInput',
90
- };
91
- }
92
-
93
- export function adaptWithDelta<Input, Output>(
94
- config: Omit<ToolRegistration<Input, Output>, 'kind'>,
95
- ): AdapterResult<Input, Output> {
96
- return { ...config, kind: 'adaptWithDelta' };
97
- }
98
-
99
- export function adaptWrite<Input, Output>(
100
- config: Omit<ToolRegistration<Input, Output>, 'kind'>,
101
- ): AdapterResult<Input, Output> {
102
- return { ...config, kind: 'adaptWrite' };
103
- }
104
-
105
- // inline: kept simple; bypasses factory if needed, but helper for consistency
106
- export function inline<Input, Output>(
107
- config: ToolRegistration<Input, Output>,
108
- ): AdapterResult<Input, Output> {
109
- return { ...config, kind: 'inline' };
110
- }
111
-
112
- export type {
113
- ToolRegistration,
114
- ToolRegistrationNoInput,
115
- ToolHandler,
116
- ToolHandlerNoInput,
117
- };
118
- ```
119
-
120
- **Exports (src/tools/index.ts)**
121
- ```ts
122
- export * from './adapters';
123
- export * as commonSchemas from './schemas/common';
124
- ```
125
-
126
- **Unit test plan (src/__tests__/tools/adapters.test.ts)**
127
- - `adapt`: returns config with `kind: 'adapt'`, preserves handler and schema identity, and is type-safe for input/output.
128
- - `adaptNoInput`: sets `inputSchema` to `z.void()`, `kind: 'adaptNoInput'`, handler invoked without input, rejects passing unexpected input at runtime (zod parse).
129
- - `adaptWithDelta`: attaches `kind: 'adaptWithDelta'`, works with a schema that includes delta fields (e.g., merged input + `deltaSchema`).
130
- - `adaptWrite`: sets `kind: 'adaptWrite'`, supports output schema with status; ensure no mutation to original config.
131
- - `inline`: passes through config and sets `kind: 'inline'`.
132
- - Shared behaviors: returned objects are new instances (no mutation), required fields (`id`, `description`, `handler`) are preserved.
133
- - Validation snapshots: optional quick zod parse test per adapter (when schema is provided) to prove wiring.
134
-
135
- **Test scaffolding snippet**
136
- ```ts
137
- import { describe, it, expect } from 'vitest';
138
- import { z } from 'zod';
139
- import {
140
- adapt,
141
- adaptNoInput,
142
- adaptWithDelta,
143
- adaptWrite,
144
- inline,
145
- } from '../../tools/adapters';
146
- import { deltaSchema, statusSchema } from '../../tools/schemas/common';
147
-
148
- describe('tool adapters', () => {
149
- it('adapt sets kind and preserves config', async () => { /* ... */ });
150
- it('adaptNoInput sets z.void and kind', async () => { /* ... */ });
151
- it('adaptWithDelta supports delta schema', async () => { /* ... */ });
152
- it('adaptWrite marks kind adaptWrite', async () => { /* ... */ });
153
- it('inline marks kind inline', async () => { /* ... */ });
154
- });
155
- ```
156
-
157
- **Notes**
158
- - Keep adapters thin; no side effects. `kind` supports metrics/routing; callers can branch on it.
159
- - Shared schemas stay minimal; expand as new tools land.
160
- - Maintain kebab-case `id` per repo guidance; single export surface via `src/tools/index.ts`.
@@ -1,3 +0,0 @@
1
- Command failed: Error loading configuration: config profile `You are helping craft an implementation plan for the YNAB MCP Server repo (Model Context Protocol server for YNAB). Repo root at C:/Users/ksutk/projects/ynab-mcpb. Key info: TypeScript/Node 18+, ts source in src/, server wiring in src/server, tools in src/tools, utils in src/utils, entry at src/index.ts. Build: npm run build (lint+tsc+bundle) outputs dist/. Tests use Vitest with projects (unit, integration, e2e). package.json scripts include build:prod, lint:fix, test:unit, test:integration, test:e2e, package:mcpb. Docs in docs/. Current version 0.13.4. Goal: propose a concrete step-by-step implementation plan for the next iteration to improve the project. Include where to change code (paths), suggested tests to add/run, and rationale. Assume current tests passing (test-results show success). Focus on actionable tasks (2-5 min steps) using TDD mindset. Deliver plan structure we can hand to engineer. Do not assume write access.
2
-
3
- [Running in read-only mode - no modifications allowed]` not found
@@ -1 +0,0 @@
1
- Agent cancelled
@@ -1 +0,0 @@
1
- Agent cancelled
@@ -1 +0,0 @@
1
- Agent cancelled
@@ -1,20 +0,0 @@
1
- **MCP Tool Catalogue**
2
- - **Budgets** (`src/server/YNABMCPServer.ts:467-540`): `list_budgets` (delta adapter; input inline `emptyObjectSchema`; output `ListBudgetsOutputSchema`), `get_budget` (read adapter; input `GetBudgetSchema` from `src/tools/budgetTools.ts`; output `GetBudgetOutputSchema`), `set_default_budget` (inline server write; input inline `setDefaultBudgetSchema`; output `SetDefaultBudgetOutputSchema`), `get_default_budget` (inline server read; input inline `emptyObjectSchema`; output `GetDefaultBudgetOutputSchema`).
3
- - **Accounts** (`src/server/YNABMCPServer.ts:573-615`): `list_accounts` (delta; input `ListAccountsSchema`; output `ListAccountsOutputSchema`), `get_account` (read; input `GetAccountSchema`; output `GetAccountOutputSchema`), `create_account` (write; input `CreateAccountSchema`; output inline `LooseObjectSchema`).
4
- - **Transactions – read/delta** (`src/server/YNABMCPServer.ts:618-692`): `list_transactions` (delta; input `ListTransactionsSchema`; output `LooseObjectSchema`), `export_transactions` (read; input `ExportTransactionsSchema`; output `ExportTransactionsOutputSchema`), `compare_transactions` (read; input `CompareTransactionsSchema`; output `CompareTransactionsOutputSchema`), `reconcile_account` (delta; input `ReconcileAccountSchema`; output `LooseObjectSchema`), `get_transaction` (read; input `GetTransactionSchema`; output `GetTransactionOutputSchema`).
5
- - **Transactions – write** (`src/server/YNABMCPServer.ts:694-785`): `create_transaction`, `create_transactions`, `create_receipt_split_transaction` (all write adapters; inputs `CreateTransactionSchema`/`CreateTransactionsSchema`/`CreateReceiptSplitTransactionSchema`; outputs `LooseObjectSchema`), `update_transaction`, `update_transactions` (write; inputs `UpdateTransactionSchema`/`UpdateTransactionsSchema`; outputs `LooseObjectSchema`), `delete_transaction` (write; input `DeleteTransactionSchema`; output `LooseObjectSchema`).
6
- - **Categories** (`src/server/YNABMCPServer.ts:787-830`): `list_categories` (delta; input `ListCategoriesSchema`; output `ListCategoriesOutputSchema`), `get_category` (read; input `GetCategorySchema`; output `GetCategoryOutputSchema`), `update_category` (write; input `UpdateCategorySchema`; output `LooseObjectSchema`).
7
- - **Payees** (`src/server/YNABMCPServer.ts:832-860`): `list_payees` (delta; input `ListPayeesSchema`; output `ListPayeesOutputSchema`), `get_payee` (read; input `GetPayeeSchema`; output `GetPayeeOutputSchema`).
8
- - **Months** (`src/server/YNABMCPServer.ts:862-889`): `get_month` (read; input `GetMonthSchema`; output `GetMonthOutputSchema`), `list_months` (delta; input `ListMonthsSchema`; output `ListMonthsOutputSchema`).
9
- - **Utilities/Local** (`src/server/YNABMCPServer.ts:892-998`): `get_user` (no-input adapter; input inline `emptyObjectSchema`; output `GetUserOutputSchema`), `convert_amount` (inline local handler; input `ConvertAmountSchema` from `src/tools/utilityTools.ts`; output `ConvertAmountOutputSchema`), `diagnostic_info` (inline server; input inline `diagnosticInfoSchema`; output `DiagnosticInfoOutputSchema`), `clear_cache` (inline server; input inline `emptyObjectSchema`; output `ClearCacheOutputSchema`), `set_output_format` (inline server; input inline `setOutputFormatSchema`; output `SetOutputFormatOutputSchema`).
10
-
11
- **Inline Schema Observations**
12
- - Inline definitions in `src/server/YNABMCPServer.ts:446-465` (`emptyObjectSchema`, `LooseObjectSchema`, `setDefaultBudgetSchema`, `diagnosticInfoSchema`, `setOutputFormatSchema`) are reused across tools and could move to a shared schema module (e.g., `src/tools/schemas/common.ts`) to reduce duplication and keep registrations declarative.
13
- - Most tools already pull input/output schemas from feature modules (`src/tools/*` and `src/tools/schemas/outputs/index.ts`); only the inline schemas above break that pattern.
14
-
15
- **Server-Owned Inline Tools to Keep Inline**
16
- - `set_default_budget`, `get_default_budget`, `diagnostic_info`, `clear_cache`, and `set_output_format` rely on server state (default budget cache warming, diagnostic manager, cache manager, response formatter) and likely stay in `YNABMCPServer` even if their schemas are centralized.
17
-
18
- **Notes for Migration**
19
- - Budget-aware tools share `defaultArgumentResolver` to inject `budget_id`; keep this resolver available wherever registrations move.
20
- - `LooseObjectSchema` is the permissive output for many write/delta tools—consider replacing with explicit output schemas in `src/tools/schemas/outputs` if stability is desired.
@@ -1,5 +0,0 @@
1
- Command failed: Error loading configuration: config profile `Context: cwd: C:\Users\ksutk\projects\ynab-mcpb. This is only to verify whether a gemini-3-pro backed agent can successfully start and complete.
2
-
3
- Agent: Minimal Gemini smoke test: from the ynab-mcpb repo, run `git status -sb` and list top-level directories, then print a short summary and exit. No file writes required.
4
-
5
- [Running in read-only mode - no modifications allowed]` not found
@@ -1 +0,0 @@
1
- Agent cancelled
package/AGENTS.md DELETED
@@ -1 +0,0 @@
1
- CLAUDE.md
package/NUL DELETED
File without changes
package/package.json.tmp DELETED
@@ -1,105 +0,0 @@
1
- {
2
- "name": "@dizzlkheinz/ynab-mcp-server",
3
- "version": "0.12.0",
4
- "description": "Model Context Protocol server for YNAB (You Need A Budget) integration",
5
- "main": "dist/index.js",
6
- "type": "module",
7
- "bin": {
8
- "ynab-mcp-server": "bin/ynab-mcp-server.cjs"
9
- },
10
- "scripts": {
11
- "prebuild": "npm run clean",
12
- "build": "npm run build:prod",
13
- "postbuild": "node scripts/run-generate-mcpb.js",
14
- "build:dev": "npm run clean && tsc && npm run bundle",
15
- "build:no-lint": "npm run clean && tsc && npm run bundle:prod",
16
- "build:prod": "npm run lint:fix && npm run prebuild && tsc --project tsconfig.prod.json && npm run bundle:prod && npm run verify-build",
17
- "clean": "rimraf dist",
18
- "dev": "tsc --watch",
19
- "start": "node -r dotenv/config dist/index.js",
20
- "start:prod": "NODE_ENV=production node dist/index.js",
21
- "start:mcp": "node dist/index.js",
22
- "validate-env": "node scripts/validate-env.js",
23
- "verify-build": "node scripts/verify-build.js",
24
- "lint": "npm run lint:eslint && npm run format:check",
25
- "lint:eslint": "eslint src --ext .ts",
26
- "lint:fix": "eslint src --ext .ts --fix && prettier --write .",
27
- "format": "prettier --write .",
28
- "format:check": "prettier --check .",
29
- "type-check": "tsc --noEmit",
30
- "test": "vitest run --project unit && npm run filter-test-results",
31
- "test:watch": "vitest",
32
- "test:unit": "vitest run --project unit",
33
- "test:integration": "npm run test:integration:core",
34
- "test:integration:core": "vitest run --project integration:core",
35
- "test:integration:domain": "vitest run --project integration:domain",
36
- "test:integration:full": "node scripts/run-throttled-integration-tests.js",
37
- "test:integration:budgets": "node scripts/run-domain-integration-tests.js budgets",
38
- "test:integration:accounts": "node scripts/run-domain-integration-tests.js accounts",
39
- "test:integration:transactions": "node scripts/run-domain-integration-tests.js transactions",
40
- "test:integration:categories": "node scripts/run-domain-integration-tests.js categories",
41
- "test:integration:payees": "node scripts/run-domain-integration-tests.js payees",
42
- "test:integration:months": "node scripts/run-domain-integration-tests.js months",
43
- "test:integration:delta": "node scripts/run-domain-integration-tests.js delta",
44
- "test:integration:reconciliation": "node scripts/run-domain-integration-tests.js reconciliation",
45
- "test:e2e": "vitest run --project e2e",
46
- "test:coverage": "vitest run --coverage --project unit",
47
- "test:performance": "vitest run src/__tests__/performance.test.ts",
48
- "test:comprehensive": "tsx src/__tests__/testRunner.ts",
49
- "test:all": "npm run test:unit && npm run test:integration:core && npm run test:e2e && npm run test:performance",
50
- "filter-test-results": "node -e \"const fs = require('fs'); try { const results = JSON.parse(fs.readFileSync('test-results.json', 'utf-8')); results.testResults = results.testResults.filter(r => r.status === 'fail'); fs.writeFileSync('test-results.json', JSON.stringify(results, null, 2)); } catch (e) { console.error('Failed to filter test results:', e); process.exit(1); }\"",
51
- "generate:mcpb": "node scripts/run-generate-mcpb.js",
52
- "bundle": "node esbuild.config.mjs",
53
- "bundle:prod": "node esbuild.config.mjs --production",
54
- "package:mcpb": "npm run build:prod && npm run generate:mcpb",
55
- "pr:description": "node scripts/create-pr-description.js",
56
- "pr:create": "npm run pr:description && gh pr create --body-file .pr-description.md",
57
- "prepare": "npm run build:prod",
58
- "prepublishOnly": "npm run test:unit && npm run build:prod"
59
- },
60
- "keywords": [
61
- "mcp",
62
- "ynab",
63
- "budget",
64
- "finance"
65
- ],
66
- "author": "",
67
- "license": "AGPL-3.0",
68
- "dependencies": {
69
- "@modelcontextprotocol/sdk": "^1.22.0",
70
- "csv-parse": "^6.1.0",
71
- "d3-array": "^3.2.4",
72
- "date-fns": "^4.1.0",
73
- "dotenv": "^17.2.1",
74
- "ynab": "^2.9.0",
75
- "zod": "^4.1.11"
76
- },
77
- "devDependencies": {
78
- "@eslint/js": "^9.35.0",
79
- "@types/d3-array": "^3.2.1",
80
- "@types/node": "^24.5.2",
81
- "@vitest/coverage-v8": "^3.2.4",
82
- "@vitest/ui": "^3.2.4",
83
- "esbuild": "^0.25.10",
84
- "eslint": "^9.35.0",
85
- "eslint-config-prettier": "^10.1.8",
86
- "prettier": "^3.3.3",
87
- "rimraf": "^6.0.1",
88
- "tsx": "^4.20.6",
89
- "typescript": "^5.9.2",
90
- "typescript-eslint": "^8.42.0",
91
- "vitest": "^3.2.4"
92
- },
93
- "directories": {
94
- "doc": "docs"
95
- },
96
- "repository": {
97
- "type": "git",
98
- "url": "git+https://github.com/dizzlkheinz/ynab-mcp-dxt.git"
99
- },
100
- "types": "./dist/index.d.ts",
101
- "bugs": {
102
- "url": "https://github.com/dizzlkheinz/ynab-mcp-dxt/issues"
103
- },
104
- "homepage": "https://github.com/dizzlkheinz/ynab-mcp-dxt#readme"
105
- }
@@ -1,80 +0,0 @@
1
- import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
2
- import * as ynab from 'ynab';
3
- import { performance } from 'node:perf_hooks';
4
- import { CacheManager } from '../server/cacheManager.js';
5
- import { ServerKnowledgeStore } from '../server/serverKnowledgeStore.js';
6
- import { DeltaCache } from '../server/deltaCache.js';
7
- import { DeltaFetcher } from '../tools/deltaFetcher.js';
8
-
9
- const skipPerfFlag = (process.env['SKIP_PERFORMANCE_TESTS'] ?? 'true').toLowerCase().trim();
10
- const shouldSkipPerformance = ['true', '1', 'yes', 'y', 'on'].includes(skipPerfFlag);
11
- const describePerformance = shouldSkipPerformance ? describe.skip : describe;
12
-
13
- describePerformance('Delta performance characteristics', () => {
14
- let ynabAPI: ynab.API;
15
- let testBudgetId: string;
16
- let testAccountId: string;
17
- let deltaFetcher: DeltaFetcher;
18
-
19
- beforeAll(async () => {
20
- const accessToken = process.env['YNAB_ACCESS_TOKEN'];
21
- if (!accessToken) {
22
- throw new Error('YNAB_ACCESS_TOKEN is required to run performance tests.');
23
- }
24
- ynabAPI = new ynab.API(accessToken);
25
- const budgetsResponse = await ynabAPI.budgets.getBudgets();
26
- if (
27
- !budgetsResponse.data ||
28
- !Array.isArray(budgetsResponse.data.budgets) ||
29
- budgetsResponse.data.budgets.length === 0
30
- ) {
31
- throw new Error('No budgets available for performance tests.');
32
- }
33
- const budget = budgetsResponse.data.budgets[0];
34
- testBudgetId = budget.id;
35
-
36
- const accountsResponse = await ynabAPI.accounts.getAccounts(testBudgetId);
37
- const account = accountsResponse.data.accounts.find((acct) => !acct.closed);
38
- if (!account) {
39
- throw new Error('No open accounts available for performance tests.');
40
- }
41
- testAccountId = account.id;
42
- });
43
-
44
- beforeEach(() => {
45
- const cacheManager = new CacheManager();
46
- const knowledgeStore = new ServerKnowledgeStore();
47
- const deltaCache = new DeltaCache(cacheManager, knowledgeStore);
48
- deltaFetcher = new DeltaFetcher(ynabAPI, deltaCache);
49
- process.env['YNAB_MCP_ENABLE_DELTA'] = 'true';
50
- });
51
-
52
- afterEach(() => {
53
- delete process.env['YNAB_MCP_ENABLE_DELTA'];
54
- });
55
-
56
- const measure = async <T>(loader: () => Promise<T>) => {
57
- const start = performance.now();
58
- const result = await loader();
59
- const duration = performance.now() - start;
60
- return { result, duration };
61
- };
62
-
63
- it('reuses cache and avoids repeated full refreshes', async () => {
64
- const sinceDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
65
-
66
- const first = await measure(() =>
67
- deltaFetcher.fetchTransactionsByAccount(testBudgetId, testAccountId, sinceDate, {
68
- forceFullRefresh: true,
69
- }),
70
- );
71
-
72
- const second = await measure(() =>
73
- deltaFetcher.fetchTransactionsByAccount(testBudgetId, testAccountId, sinceDate),
74
- );
75
-
76
- expect(first.result.wasCached).toBe(false);
77
- expect(second.result.wasCached).toBe(true);
78
- expect(second.duration).toBeLessThan(first.duration * 0.8); // Cached should be at least 20% faster
79
- });
80
- });