@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,1120 +0,0 @@
1
- /**
2
- * End-to-end workflow tests for YNAB MCP Server
3
- * These tests require a real YNAB API key and test budget
4
- */
5
-
6
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
7
- import { YNABMCPServer } from '../server/YNABMCPServer.js';
8
- import { getCurrentMonth } from '../utils/dateUtils.js';
9
- import {
10
- getTestConfig,
11
- createTestServer,
12
- executeToolCall,
13
- parseToolResult,
14
- isErrorResult,
15
- getErrorMessage,
16
- skipIfRateLimitedResult,
17
- TestData,
18
- TestDataCleanup,
19
- YNABAssertions,
20
- validateOutputSchema,
21
- } from './testUtils.js';
22
- import { testEnv } from './setup.js';
23
-
24
- const runE2ETests = process.env['SKIP_E2E_TESTS'] !== 'true';
25
- const describeE2E = runE2ETests ? describe : describe.skip;
26
-
27
- describeE2E('YNAB MCP Server - End-to-End Workflows', () => {
28
- let server: YNABMCPServer;
29
- let testConfig: ReturnType<typeof getTestConfig>;
30
- let cleanup: TestDataCleanup;
31
- let testBudgetId: string;
32
- let testAccountId: string;
33
-
34
- beforeAll(async () => {
35
- testConfig = getTestConfig();
36
-
37
- if (testConfig.skipE2ETests) {
38
- console.warn('Skipping E2E tests - no real API key or SKIP_E2E_TESTS=true');
39
- return;
40
- }
41
-
42
- server = await createTestServer();
43
- cleanup = new TestDataCleanup();
44
-
45
- // Get the first budget for testing
46
- const budgetsResult = await executeToolCall(server, 'ynab:list_budgets');
47
- const budgets = parseToolResult(budgetsResult);
48
- const budgetList = budgets.data?.budgets ?? [];
49
-
50
- if (!budgetList.length && !testConfig.testBudgetId) {
51
- throw new Error('No budgets found for testing. Please create a test budget in YNAB.');
52
- }
53
-
54
- testBudgetId = testConfig.testBudgetId ?? budgetList[0]?.id;
55
-
56
- // Get the first account for testing
57
- const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
58
- budget_id: testBudgetId,
59
- });
60
- const accounts = parseToolResult(accountsResult);
61
- const accountList = accounts.data?.accounts ?? [];
62
-
63
- if (!accountList.length) {
64
- if (testConfig.testAccountId) {
65
- testAccountId = testConfig.testAccountId;
66
- } else {
67
- throw new Error('No accounts found for testing. Please create a test account in YNAB.');
68
- }
69
- } else {
70
- testAccountId = testConfig.testAccountId ?? accountList[0].id;
71
- }
72
- });
73
-
74
- afterAll(async () => {
75
- if (testConfig.skipE2ETests) return;
76
-
77
- if (cleanup && server && testBudgetId) {
78
- await cleanup.cleanup(server, testBudgetId);
79
- }
80
- });
81
-
82
- beforeEach(() => {
83
- if (testConfig.skipE2ETests) {
84
- // Skip individual tests if E2E tests are disabled
85
- return;
86
- }
87
- });
88
-
89
- describe('Complete Budget Management Workflow', () => {
90
- it('should retrieve and validate budget information', async () => {
91
- if (testConfig.skipE2ETests) return;
92
-
93
- // List all budgets
94
- const budgetsResult = await executeToolCall(server, 'ynab:list_budgets');
95
-
96
- // Validate output schema
97
- const budgetsValidation = validateOutputSchema(server, 'list_budgets', budgetsResult);
98
- expect(budgetsValidation.valid).toBe(true);
99
- if (!budgetsValidation.valid) {
100
- console.error('list_budgets schema validation errors:', budgetsValidation.errors);
101
- }
102
-
103
- const budgets = parseToolResult(budgetsResult);
104
-
105
- // Verify backward compatibility contract: parseToolResult returns {success: true, data: ...}
106
- expect(budgets).toHaveProperty('success');
107
- expect(budgets.success).toBe(true);
108
- expect(budgets).toHaveProperty('data');
109
-
110
- expect(budgets.data).toBeDefined();
111
- expect(budgets.data.budgets).toBeDefined();
112
- expect(Array.isArray(budgets.data.budgets)).toBe(true);
113
- expect(budgets.data.budgets.length).toBeGreaterThan(0);
114
-
115
- // Validate budget structure
116
- budgets.data.budgets.forEach(YNABAssertions.assertBudget);
117
-
118
- // Get specific budget details
119
- const budgetResult = await executeToolCall(server, 'ynab:get_budget', {
120
- budget_id: testBudgetId,
121
- });
122
-
123
- // Validate output schema
124
- const budgetValidation = validateOutputSchema(server, 'get_budget', budgetResult);
125
- expect(budgetValidation.valid).toBe(true);
126
- if (!budgetValidation.valid) {
127
- console.error('get_budget schema validation errors:', budgetValidation.errors);
128
- }
129
-
130
- const budget = parseToolResult(budgetResult);
131
-
132
- expect(budget.data).toBeDefined();
133
- expect(budget.data.budget).toBeDefined();
134
- YNABAssertions.assertBudget(budget.data.budget);
135
- expect(budget.data.budget.id).toBe(testBudgetId);
136
- });
137
-
138
- it('should retrieve user information', async () => {
139
- if (testConfig.skipE2ETests) return;
140
-
141
- const userResult = await executeToolCall(server, 'ynab:get_user');
142
- const user = parseToolResult(userResult);
143
-
144
- expect(user.data).toBeDefined();
145
- expect(user.data.user).toBeDefined();
146
- expect(typeof user.data.user.id).toBe('string');
147
- });
148
- });
149
-
150
- describe('Complete Account Management Workflow', () => {
151
- it('should list and retrieve account information', async () => {
152
- if (testConfig.skipE2ETests) return;
153
-
154
- // List all accounts
155
- const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
156
- budget_id: testBudgetId,
157
- });
158
-
159
- // Validate output schema
160
- const accountsValidation = validateOutputSchema(server, 'list_accounts', accountsResult);
161
- expect(accountsValidation.valid).toBe(true);
162
- if (!accountsValidation.valid) {
163
- console.error('list_accounts schema validation errors:', accountsValidation.errors);
164
- }
165
-
166
- const accounts = parseToolResult(accountsResult);
167
-
168
- // Verify backward compatibility contract: parseToolResult returns {success: true, data: ...}
169
- expect(accounts).toHaveProperty('success');
170
- expect(accounts.success).toBe(true);
171
- expect(accounts).toHaveProperty('data');
172
-
173
- expect(accounts.data).toBeDefined();
174
- expect(accounts.data.accounts).toBeDefined();
175
- expect(Array.isArray(accounts.data.accounts)).toBe(true);
176
- expect(accounts.data.accounts.length).toBeGreaterThan(0);
177
-
178
- // Validate account structures
179
- accounts.data.accounts.forEach(YNABAssertions.assertAccount);
180
-
181
- // Get specific account details
182
- const accountResult = await executeToolCall(server, 'ynab:get_account', {
183
- budget_id: testBudgetId,
184
- account_id: testAccountId,
185
- });
186
-
187
- // Validate output schema
188
- const accountValidation = validateOutputSchema(server, 'get_account', accountResult);
189
- expect(accountValidation.valid).toBe(true);
190
- if (!accountValidation.valid) {
191
- console.error('get_account schema validation errors:', accountValidation.errors);
192
- }
193
-
194
- const account = parseToolResult(accountResult);
195
-
196
- expect(account.data).toBeDefined();
197
- expect(account.data.account).toBeDefined();
198
- YNABAssertions.assertAccount(account.data.account);
199
- expect(account.data.account.id).toBe(testAccountId);
200
-
201
- // Reconcile account as part of account management workflow
202
- const reconcileResult = await executeToolCall(server, 'ynab:reconcile_account', {
203
- budget_id: testBudgetId,
204
- account_id: testAccountId,
205
- cleared_balance: account.data.account.cleared_balance,
206
- });
207
-
208
- // Validate reconcile_account output schema
209
- const reconcileValidation = validateOutputSchema(
210
- server,
211
- 'reconcile_account',
212
- reconcileResult,
213
- );
214
- expect(reconcileValidation.valid).toBe(true);
215
- if (!reconcileValidation.valid) {
216
- console.error('reconcile_account schema validation errors:', reconcileValidation.errors);
217
- }
218
- });
219
-
220
- it('should create a new account', async () => {
221
- if (testConfig.skipE2ETests) return;
222
-
223
- const accountName = TestData.generateAccountName();
224
-
225
- const createResult = await executeToolCall(server, 'ynab:create_account', {
226
- budget_id: testBudgetId,
227
- name: accountName,
228
- type: 'checking',
229
- balance: 10000, // $10.00
230
- });
231
- if (skipIfRateLimitedResult(createResult)) return;
232
-
233
- // Validate output schema
234
- const createValidation = validateOutputSchema(server, 'create_account', createResult);
235
- expect(createValidation.valid).toBe(true);
236
- if (!createValidation.valid) {
237
- console.error('create_account schema validation errors:', createValidation.errors);
238
- }
239
-
240
- const createdAccount = parseToolResult(createResult);
241
-
242
- expect(createdAccount.data).toBeDefined();
243
- expect(createdAccount.data.account).toBeDefined();
244
- YNABAssertions.assertAccount(createdAccount.data.account);
245
- expect(createdAccount.data.account.name).toBe(accountName);
246
- expect(createdAccount.data.account.type).toBe('checking');
247
-
248
- // Track for cleanup
249
- cleanup.trackAccount(createdAccount.data.account.id);
250
-
251
- // Verify account appears in list
252
- const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
253
- budget_id: testBudgetId,
254
- });
255
- if (skipIfRateLimitedResult(accountsResult)) return;
256
- const accounts = parseToolResult(accountsResult);
257
-
258
- const foundAccount = accounts.data.accounts.find(
259
- (acc: any) => acc.id === createdAccount.data.account.id,
260
- );
261
- expect(foundAccount).toBeDefined();
262
- expect(foundAccount.name).toBe(accountName);
263
- });
264
- });
265
-
266
- describe('Complete Transaction Management Workflow', () => {
267
- let testTransactionId: string;
268
-
269
- it('should create, retrieve, update, and delete a transaction', async () => {
270
- if (testConfig.skipE2ETests) return;
271
-
272
- // Get categories for transaction creation
273
- const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
274
- budget_id: testBudgetId,
275
- });
276
- const categories = parseToolResult(categoriesResult);
277
-
278
- expect(categories.data.category_groups).toBeDefined();
279
- expect(Array.isArray(categories.data.category_groups)).toBe(true);
280
-
281
- // Find a non-hidden category
282
- let testCategoryId: string | undefined;
283
- for (const group of categories.data.category_groups) {
284
- const availableCategory = group.categories?.find((cat: any) => !cat.hidden);
285
- if (availableCategory) {
286
- testCategoryId = availableCategory.id;
287
- break;
288
- }
289
- }
290
-
291
- // Create a transaction
292
- const transactionData = TestData.generateTransaction(testAccountId, testCategoryId);
293
-
294
- const createResult = await executeToolCall(server, 'ynab:create_transaction', {
295
- budget_id: testBudgetId,
296
- ...transactionData,
297
- });
298
- if (skipIfRateLimitedResult(createResult)) return;
299
-
300
- // Validate create_transaction output schema
301
- const createValidation = validateOutputSchema(server, 'create_transaction', createResult);
302
- expect(createValidation.valid).toBe(true);
303
- if (!createValidation.valid) {
304
- console.error('create_transaction schema validation errors:', createValidation.errors);
305
- }
306
-
307
- const createdTransaction = parseToolResult(createResult);
308
-
309
- if (!createdTransaction?.data?.transaction) {
310
- console.warn(
311
- '[rate-limit] Skipping transaction workflow because create_transaction returned no transaction data',
312
- );
313
- return;
314
- }
315
-
316
- // Verify backward compatibility contract: parseToolResult returns {success: true, data: ...}
317
- expect(createdTransaction).toHaveProperty('success');
318
- expect(createdTransaction.success).toBe(true);
319
- expect(createdTransaction).toHaveProperty('data');
320
-
321
- expect(createdTransaction.data).toBeDefined();
322
- expect(createdTransaction.data.transaction).toBeDefined();
323
- YNABAssertions.assertTransaction(createdTransaction.data.transaction);
324
-
325
- testTransactionId = createdTransaction.data.transaction.id;
326
- cleanup.trackTransaction(testTransactionId);
327
-
328
- // Retrieve the transaction
329
- const getResult = await executeToolCall(server, 'ynab:get_transaction', {
330
- budget_id: testBudgetId,
331
- transaction_id: testTransactionId,
332
- });
333
- if (skipIfRateLimitedResult(getResult)) return;
334
-
335
- // Validate get_transaction output schema
336
- const getValidation = validateOutputSchema(server, 'get_transaction', getResult);
337
- expect(getValidation.valid).toBe(true);
338
- if (!getValidation.valid) {
339
- console.error('get_transaction schema validation errors:', getValidation.errors);
340
- }
341
-
342
- const retrievedTransaction = parseToolResult(getResult);
343
-
344
- expect(retrievedTransaction.data).toBeDefined();
345
- expect(retrievedTransaction.data.transaction).toBeDefined();
346
- expect(retrievedTransaction.data.transaction.id).toBe(testTransactionId);
347
- YNABAssertions.assertTransaction(retrievedTransaction.data.transaction);
348
-
349
- // Update the transaction
350
- const updatedMemo = `Updated memo ${Date.now()}`;
351
- const updateResult = await executeToolCall(server, 'ynab:update_transaction', {
352
- budget_id: testBudgetId,
353
- transaction_id: testTransactionId,
354
- memo: updatedMemo,
355
- });
356
- if (skipIfRateLimitedResult(updateResult)) return;
357
-
358
- // Validate update_transaction output schema
359
- const updateValidation = validateOutputSchema(server, 'update_transaction', updateResult);
360
- expect(updateValidation.valid).toBe(true);
361
- if (!updateValidation.valid) {
362
- console.error('update_transaction schema validation errors:', updateValidation.errors);
363
- }
364
-
365
- const updatedTransaction = parseToolResult(updateResult);
366
-
367
- expect(updatedTransaction.data).toBeDefined();
368
- expect(updatedTransaction.data.transaction).toBeDefined();
369
- expect(updatedTransaction.data.transaction.memo).toBe(updatedMemo);
370
-
371
- // List transactions and verify our transaction is included
372
- const listResult = await executeToolCall(server, 'ynab:list_transactions', {
373
- budget_id: testBudgetId,
374
- account_id: testAccountId,
375
- });
376
- if (skipIfRateLimitedResult(listResult)) return;
377
-
378
- // Validate list_transactions output schema
379
- const listValidation = validateOutputSchema(server, 'list_transactions', listResult);
380
- expect(listValidation.valid).toBe(true);
381
- if (!listValidation.valid) {
382
- console.error('list_transactions schema validation errors:', listValidation.errors);
383
- }
384
-
385
- const transactions = parseToolResult(listResult);
386
-
387
- expect(transactions.data).toBeDefined();
388
- expect(transactions.data.transactions).toBeDefined();
389
- expect(Array.isArray(transactions.data.transactions)).toBe(true);
390
-
391
- const foundTransaction = transactions.data.transactions.find(
392
- (txn: any) => txn.id === testTransactionId,
393
- );
394
- expect(foundTransaction).toBeDefined();
395
- expect(foundTransaction.memo).toBe(updatedMemo);
396
-
397
- // Delete the transaction
398
- const deleteResult = await executeToolCall(server, 'ynab:delete_transaction', {
399
- budget_id: testBudgetId,
400
- transaction_id: testTransactionId,
401
- });
402
- if (skipIfRateLimitedResult(deleteResult)) return;
403
-
404
- // Validate delete_transaction output schema
405
- const deleteValidation = validateOutputSchema(server, 'delete_transaction', deleteResult);
406
- expect(deleteValidation.valid).toBe(true);
407
- if (!deleteValidation.valid) {
408
- console.error('delete_transaction schema validation errors:', deleteValidation.errors);
409
- }
410
-
411
- const deleteResponse = parseToolResult(deleteResult);
412
-
413
- expect(deleteResponse.data).toBeDefined();
414
-
415
- // Verify transaction is deleted (should return error when trying to retrieve)
416
- const getDeletedResult = await executeToolCall(server, 'ynab:get_transaction', {
417
- budget_id: testBudgetId,
418
- transaction_id: testTransactionId,
419
- });
420
- expect(isErrorResult(getDeletedResult)).toBe(true);
421
- // Expected - transaction should not be found
422
- expect(getDeletedResult.content).toBeDefined();
423
- expect(getDeletedResult.content.length).toBeGreaterThan(0);
424
- });
425
-
426
- it('should filter transactions by date and account', async () => {
427
- if (testConfig.skipE2ETests) return;
428
-
429
- const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
430
-
431
- // List transactions since last month
432
- const recentResult = await executeToolCall(server, 'ynab:list_transactions', {
433
- budget_id: testBudgetId,
434
- since_date: lastMonth,
435
- });
436
- if (skipIfRateLimitedResult(recentResult)) return;
437
- const recentTransactions = parseToolResult(recentResult);
438
-
439
- expect(recentTransactions.data).toBeDefined();
440
- expect(recentTransactions.data.transactions).toBeDefined();
441
- expect(Array.isArray(recentTransactions.data.transactions)).toBe(true);
442
-
443
- // List transactions for specific account
444
- const accountResult = await executeToolCall(server, 'ynab:list_transactions', {
445
- budget_id: testBudgetId,
446
- account_id: testAccountId,
447
- });
448
- if (skipIfRateLimitedResult(accountResult)) return;
449
- const accountTransactions = parseToolResult(accountResult);
450
-
451
- expect(accountTransactions.data).toBeDefined();
452
- expect(accountTransactions.data.transactions).toBeDefined();
453
- expect(Array.isArray(accountTransactions.data.transactions)).toBe(true);
454
-
455
- // All transactions should be for the specified account
456
- accountTransactions.data.transactions.forEach((txn: any) => {
457
- expect(txn.account_id).toBe(testAccountId);
458
- });
459
- });
460
-
461
- it('should export and compare transactions', async () => {
462
- if (testConfig.skipE2ETests) return;
463
-
464
- // Export transactions as part of transaction management workflow
465
- const exportResult = await executeToolCall(server, 'ynab:export_transactions', {
466
- budget_id: testBudgetId,
467
- account_id: testAccountId,
468
- });
469
- if (skipIfRateLimitedResult(exportResult)) return;
470
-
471
- // Validate export_transactions output schema
472
- const exportValidation = validateOutputSchema(server, 'export_transactions', exportResult);
473
- expect(exportValidation.valid).toBe(true);
474
- if (!exportValidation.valid) {
475
- console.error('export_transactions schema validation errors:', exportValidation.errors);
476
- }
477
-
478
- const exportData = parseToolResult(exportResult);
479
- expect(exportData.data).toBeDefined();
480
-
481
- // Compare transactions as part of transaction management workflow
482
- const csvData = `Date,Payee,Amount\n2025-01-15,Test Comparison Payee,-25.00`;
483
- const compareResult = await executeToolCall(server, 'ynab:compare_transactions', {
484
- budget_id: testBudgetId,
485
- account_id: testAccountId,
486
- csv_data: csvData,
487
- start_date: '2025-01-01',
488
- end_date: '2025-01-31',
489
- });
490
- if (skipIfRateLimitedResult(compareResult)) return;
491
-
492
- // Validate compare_transactions output schema
493
- const compareValidation = validateOutputSchema(server, 'compare_transactions', compareResult);
494
- expect(compareValidation.valid).toBe(true);
495
- if (!compareValidation.valid) {
496
- console.error('compare_transactions schema validation errors:', compareValidation.errors);
497
- }
498
-
499
- const compareData = parseToolResult(compareResult);
500
- expect(compareData.data).toBeDefined();
501
- });
502
-
503
- it('should create and update transactions in bulk', async () => {
504
- if (testConfig.skipE2ETests) return;
505
-
506
- // Create multiple transactions as part of bulk workflow
507
- const transactions = [
508
- {
509
- account_id: testAccountId,
510
- date: new Date().toISOString().split('T')[0],
511
- amount: -1500,
512
- payee_name: `Bulk Workflow Payee 1 ${Date.now()}`,
513
- memo: 'Bulk workflow test 1',
514
- cleared: 'uncleared' as const,
515
- },
516
- {
517
- account_id: testAccountId,
518
- date: new Date().toISOString().split('T')[0],
519
- amount: -2500,
520
- payee_name: `Bulk Workflow Payee 2 ${Date.now()}`,
521
- memo: 'Bulk workflow test 2',
522
- cleared: 'uncleared' as const,
523
- },
524
- ];
525
-
526
- const createBulkResult = await executeToolCall(server, 'ynab:create_transactions', {
527
- budget_id: testBudgetId,
528
- transactions,
529
- });
530
- if (skipIfRateLimitedResult(createBulkResult)) return;
531
-
532
- // Validate create_transactions (bulk) output schema
533
- const createBulkValidation = validateOutputSchema(
534
- server,
535
- 'create_transactions',
536
- createBulkResult,
537
- );
538
- expect(createBulkValidation.valid).toBe(true);
539
- if (!createBulkValidation.valid) {
540
- console.error('create_transactions schema validation errors:', createBulkValidation.errors);
541
- }
542
-
543
- const createdBulk = parseToolResult(createBulkResult);
544
- expect(createdBulk.data?.transactions).toBeDefined();
545
- expect(Array.isArray(createdBulk.data.transactions)).toBe(true);
546
- expect(createdBulk.data.transactions.length).toBe(2);
547
-
548
- // Track for cleanup
549
- const transactionIds = createdBulk.data.transactions.map((txn: any) => txn.id);
550
- transactionIds.forEach((id: string) => cleanup.trackTransaction(id));
551
-
552
- // Update transactions in bulk as part of workflow
553
- const updateBulkResult = await executeToolCall(server, 'ynab:update_transactions', {
554
- budget_id: testBudgetId,
555
- transactions: transactionIds.map((id: string, index: number) => ({
556
- id,
557
- memo: `Updated bulk memo ${index + 1}`,
558
- })),
559
- });
560
- if (skipIfRateLimitedResult(updateBulkResult)) return;
561
-
562
- // Validate update_transactions (bulk) output schema
563
- const updateBulkValidation = validateOutputSchema(
564
- server,
565
- 'update_transactions',
566
- updateBulkResult,
567
- );
568
- expect(updateBulkValidation.valid).toBe(true);
569
- if (!updateBulkValidation.valid) {
570
- console.error('update_transactions schema validation errors:', updateBulkValidation.errors);
571
- }
572
-
573
- const updatedBulk = parseToolResult(updateBulkResult);
574
- expect(updatedBulk.data?.transactions).toBeDefined();
575
- expect(Array.isArray(updatedBulk.data.transactions)).toBe(true);
576
- });
577
-
578
- it('should create receipt split transaction', async () => {
579
- if (testConfig.skipE2ETests) return;
580
-
581
- // Get categories for the receipt split
582
- const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
583
- budget_id: testBudgetId,
584
- });
585
- const categories = parseToolResult(categoriesResult);
586
-
587
- // Find a non-hidden category
588
- let testCategoryName: string | undefined;
589
- for (const group of categories.data.category_groups) {
590
- const availableCategory = group.categories?.find((cat: any) => !cat.hidden);
591
- if (availableCategory) {
592
- testCategoryName = availableCategory.name;
593
- break;
594
- }
595
- }
596
-
597
- if (!testCategoryName) {
598
- console.warn('No available categories found for receipt split test');
599
- return;
600
- }
601
-
602
- // Create receipt split transaction as part of transaction workflow
603
- const receiptResult = await executeToolCall(server, 'ynab:create_receipt_split_transaction', {
604
- budget_id: testBudgetId,
605
- account_id: testAccountId,
606
- date: new Date().toISOString().split('T')[0],
607
- payee_name: `Receipt Workflow ${Date.now()}`,
608
- tax_amount: 150,
609
- receipt_items: [
610
- {
611
- category_name: testCategoryName,
612
- amount: 2000,
613
- },
614
- ],
615
- });
616
-
617
- // Validate create_receipt_split_transaction output schema
618
- const receiptValidation = validateOutputSchema(
619
- server,
620
- 'create_receipt_split_transaction',
621
- receiptResult,
622
- );
623
- expect(receiptValidation.valid).toBe(true);
624
- if (!receiptValidation.valid) {
625
- console.error(
626
- 'create_receipt_split_transaction schema validation errors:',
627
- receiptValidation.errors,
628
- );
629
- }
630
-
631
- const receiptData = parseToolResult(receiptResult);
632
- expect(receiptData.data?.transaction).toBeDefined();
633
-
634
- // Track for cleanup
635
- if (receiptData.data.transaction.id) {
636
- cleanup.trackTransaction(receiptData.data.transaction.id);
637
- }
638
- });
639
- });
640
-
641
- describe('Complete Category Management Workflow', () => {
642
- it('should list categories and update category budget', async () => {
643
- if (testConfig.skipE2ETests) return;
644
-
645
- // List all categories
646
- const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
647
- budget_id: testBudgetId,
648
- });
649
-
650
- // Validate list_categories output schema
651
- const listValidation = validateOutputSchema(server, 'list_categories', categoriesResult);
652
- expect(listValidation.valid).toBe(true);
653
- if (!listValidation.valid) {
654
- console.error('list_categories schema validation errors:', listValidation.errors);
655
- }
656
-
657
- const categories = parseToolResult(categoriesResult);
658
-
659
- expect(categories.data).toBeDefined();
660
- expect(categories.data.category_groups).toBeDefined();
661
- expect(Array.isArray(categories.data.category_groups)).toBe(true);
662
-
663
- // Find a category to test with
664
- let testCategoryId: string | undefined;
665
- let testCategory: any;
666
-
667
- for (const group of categories.data.category_groups) {
668
- if (group.categories && group.categories.length > 0) {
669
- testCategory = group.categories.find((cat: any) => !cat.hidden);
670
- if (testCategory) {
671
- testCategoryId = testCategory.id;
672
- break;
673
- }
674
- }
675
- }
676
-
677
- if (!testCategoryId) {
678
- console.warn('No available categories found for testing');
679
- return;
680
- }
681
-
682
- // Get specific category details
683
- const categoryResult = await executeToolCall(server, 'ynab:get_category', {
684
- budget_id: testBudgetId,
685
- category_id: testCategoryId,
686
- });
687
-
688
- // Validate get_category output schema
689
- const getValidation = validateOutputSchema(server, 'get_category', categoryResult);
690
- expect(getValidation.valid).toBe(true);
691
- if (!getValidation.valid) {
692
- console.error('get_category schema validation errors:', getValidation.errors);
693
- }
694
-
695
- const category = parseToolResult(categoryResult);
696
-
697
- expect(category.data).toBeDefined();
698
- expect(category.data.category).toBeDefined();
699
- YNABAssertions.assertCategory(category.data.category);
700
- expect(category.data.category.id).toBe(testCategoryId);
701
-
702
- // Update category budget
703
- const newBudgetAmount = TestData.generateAmount(50); // $50.00
704
- const updateResult = await executeToolCall(server, 'ynab:update_category', {
705
- budget_id: testBudgetId,
706
- category_id: testCategoryId,
707
- budgeted: newBudgetAmount,
708
- });
709
-
710
- // Validate update_category output schema
711
- const updateValidation = validateOutputSchema(server, 'update_category', updateResult);
712
- expect(updateValidation.valid).toBe(true);
713
- if (!updateValidation.valid) {
714
- console.error('update_category schema validation errors:', updateValidation.errors);
715
- }
716
-
717
- const updatedCategory = parseToolResult(updateResult);
718
-
719
- expect(updatedCategory.data).toBeDefined();
720
- expect(updatedCategory.data.category).toBeDefined();
721
- expect(updatedCategory.data.category.budgeted).toBe(newBudgetAmount);
722
- });
723
- });
724
-
725
- describe('Complete Payee Management Workflow', () => {
726
- it('should list and retrieve payee information', async () => {
727
- if (testConfig.skipE2ETests) return;
728
-
729
- // List all payees
730
- const payeesResult = await executeToolCall(server, 'ynab:list_payees', {
731
- budget_id: testBudgetId,
732
- });
733
-
734
- // Validate list_payees output schema
735
- const listValidation = validateOutputSchema(server, 'list_payees', payeesResult);
736
- expect(listValidation.valid).toBe(true);
737
- if (!listValidation.valid) {
738
- console.error('list_payees schema validation errors:', listValidation.errors);
739
- }
740
-
741
- const payees = parseToolResult(payeesResult);
742
-
743
- expect(payees.data).toBeDefined();
744
- expect(payees.data.payees).toBeDefined();
745
- expect(Array.isArray(payees.data.payees)).toBe(true);
746
-
747
- if (payees.data.payees.length > 0) {
748
- // Validate payee structures
749
- payees.data.payees.forEach(YNABAssertions.assertPayee);
750
-
751
- // Get specific payee details
752
- const testPayeeId = payees.data.payees[0].id;
753
- const payeeResult = await executeToolCall(server, 'ynab:get_payee', {
754
- budget_id: testBudgetId,
755
- payee_id: testPayeeId,
756
- });
757
-
758
- // Validate get_payee output schema
759
- const getValidation = validateOutputSchema(server, 'get_payee', payeeResult);
760
- expect(getValidation.valid).toBe(true);
761
- if (!getValidation.valid) {
762
- console.error('get_payee schema validation errors:', getValidation.errors);
763
- }
764
-
765
- const payee = parseToolResult(payeeResult);
766
-
767
- expect(payee.data).toBeDefined();
768
- expect(payee.data.payee).toBeDefined();
769
- YNABAssertions.assertPayee(payee.data.payee);
770
- expect(payee.data.payee.id).toBe(testPayeeId);
771
- }
772
- });
773
- });
774
-
775
- describe('Complete Monthly Data Workflow', () => {
776
- it('should retrieve monthly budget data', async () => {
777
- if (testConfig.skipE2ETests) return;
778
-
779
- // List all months
780
- const monthsResult = await executeToolCall(server, 'ynab:list_months', {
781
- budget_id: testBudgetId,
782
- });
783
-
784
- // Validate list_months output schema
785
- const listValidation = validateOutputSchema(server, 'list_months', monthsResult);
786
- expect(listValidation.valid).toBe(true);
787
- if (!listValidation.valid) {
788
- console.error('list_months schema validation errors:', listValidation.errors);
789
- }
790
-
791
- const months = parseToolResult(monthsResult);
792
-
793
- expect(months.data).toBeDefined();
794
- expect(months.data.months).toBeDefined();
795
- expect(Array.isArray(months.data.months)).toBe(true);
796
- expect(months.data.months.length).toBeGreaterThan(0);
797
-
798
- // Get current month data
799
- const currentMonth = getCurrentMonth();
800
- const monthResult = await executeToolCall(server, 'ynab:get_month', {
801
- budget_id: testBudgetId,
802
- month: currentMonth,
803
- });
804
-
805
- // Validate get_month output schema
806
- const getValidation = validateOutputSchema(server, 'get_month', monthResult);
807
- expect(getValidation.valid).toBe(true);
808
- if (!getValidation.valid) {
809
- console.error('get_month schema validation errors:', getValidation.errors);
810
- }
811
-
812
- const month = parseToolResult(monthResult);
813
-
814
- expect(month.data).toBeDefined();
815
- expect(month.data.month).toBeDefined();
816
- expect(typeof month.data.month.month).toBe('string');
817
- expect(typeof month.data.month.income).toBe('number');
818
- expect(typeof month.data.month.budgeted).toBe('number');
819
- expect(typeof month.data.month.activity).toBe('number');
820
- expect(typeof month.data.month.to_be_budgeted).toBe('number');
821
- });
822
- });
823
-
824
- describe('Utility Tools Workflow', () => {
825
- it('should convert amounts between dollars and milliunits', async () => {
826
- if (testConfig.skipE2ETests) return;
827
-
828
- // Convert dollars to milliunits
829
- const toMilliunitsResult = await executeToolCall(server, 'ynab:convert_amount', {
830
- amount: 25.5,
831
- to_milliunits: true,
832
- });
833
- const milliunits = parseToolResult(toMilliunitsResult);
834
-
835
- expect(milliunits.data?.conversion?.converted_amount).toBe(25500);
836
- expect(milliunits.data?.conversion?.description).toContain('25500');
837
- expect(milliunits.data?.conversion?.to_milliunits).toBe(true);
838
-
839
- // Convert milliunits to dollars
840
- const toDollarsResult = await executeToolCall(server, 'ynab:convert_amount', {
841
- amount: 25500,
842
- to_milliunits: false,
843
- });
844
- const dollars = parseToolResult(toDollarsResult);
845
-
846
- expect(dollars.data?.conversion?.converted_amount).toBe(25.5);
847
- expect(dollars.data?.conversion?.description).toContain('$25.50');
848
- expect(dollars.data?.conversion?.to_milliunits).toBe(false);
849
- });
850
- });
851
-
852
- describe('v0.8.x Architecture Integration Tests', () => {
853
- describe('Cache System Verification', () => {
854
- it('should demonstrate cache warming after default budget set', async () => {
855
- if (testConfig.skipE2ETests) return;
856
-
857
- // Enable caching for this test
858
- testEnv.enableCache();
859
-
860
- try {
861
- // Get initial cache stats
862
- const initialStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
863
- const initialStats = parseToolResult(initialStatsResult);
864
- const initialCacheStats = initialStats.data?.cache;
865
-
866
- // Set default budget (should trigger cache warming)
867
- await executeToolCall(server, 'ynab:set_default_budget', {
868
- budget_id: testBudgetId,
869
- });
870
-
871
- // Allow time for cache warming (fire-and-forget)
872
- await new Promise((resolve) => setTimeout(resolve, 1000));
873
-
874
- // Get updated cache stats
875
- const finalStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
876
- const finalStats = parseToolResult(finalStatsResult);
877
- const finalCacheStats = finalStats.data?.cache;
878
-
879
- // Verify cache warming occurred
880
- expect(finalCacheStats?.entries).toBeGreaterThan(initialCacheStats?.entries || 0);
881
- expect(finalCacheStats?.hits).toBeGreaterThanOrEqual(0);
882
- } finally {
883
- // Restore original NODE_ENV
884
- testEnv.restoreEnv();
885
- }
886
- });
887
-
888
- it('should demonstrate LRU eviction and observability metrics', async () => {
889
- if (testConfig.skipE2ETests) return;
890
-
891
- // Enable caching for this test (bypass NODE_ENV='test' check)
892
- testEnv.enableCache();
893
-
894
- try {
895
- // Get initial cache stats
896
- const initialStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
897
- const initialStats = parseToolResult(initialStatsResult);
898
- const initialCacheStats = initialStats.data?.cache;
899
-
900
- // Perform operations that should hit cache
901
- await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
902
- await executeToolCall(server, 'ynab:list_categories', { budget_id: testBudgetId });
903
- await executeToolCall(server, 'ynab:list_payees', { budget_id: testBudgetId });
904
-
905
- // Perform same operations again (should hit cache)
906
- await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
907
- await executeToolCall(server, 'ynab:list_categories', { budget_id: testBudgetId });
908
- await executeToolCall(server, 'ynab:list_payees', { budget_id: testBudgetId });
909
-
910
- // Get final cache stats
911
- const finalStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
912
- const finalStats = parseToolResult(finalStatsResult);
913
- const finalCacheStats = finalStats.data?.cache;
914
-
915
- // Verify cache behavior
916
- expect(finalCacheStats?.hits).toBeGreaterThan(initialCacheStats?.hits || 0);
917
- expect(finalCacheStats?.misses).toBeGreaterThan(initialCacheStats?.misses || 0);
918
- expect(finalCacheStats?.hits).toBeGreaterThan(0);
919
- expect(finalCacheStats?.entries).toBeGreaterThan(0);
920
- } finally {
921
- // Restore original NODE_ENV
922
- testEnv.restoreEnv();
923
- }
924
- });
925
-
926
- it('should demonstrate cache invalidation on write operations', async () => {
927
- if (testConfig.skipE2ETests) return;
928
-
929
- // Enable caching for this test
930
- testEnv.enableCache();
931
-
932
- try {
933
- // Prime cache by listing accounts
934
- await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
935
-
936
- // Create new account (should invalidate accounts cache)
937
- const accountName = TestData.generateAccountName();
938
- const createResult = await executeToolCall(server, 'ynab:create_account', {
939
- budget_id: testBudgetId,
940
- name: accountName,
941
- type: 'checking',
942
- balance: 10000,
943
- });
944
-
945
- // Validate output schema
946
- const createValidation = validateOutputSchema(server, 'create_account', createResult);
947
- expect(createValidation.valid).toBe(true);
948
- if (!createValidation.valid) {
949
- console.error('create_account schema validation errors:', createValidation.errors);
950
- }
951
-
952
- const createdAccount = parseToolResult(createResult);
953
- cleanup.trackAccount(createdAccount.data.account.id);
954
-
955
- // List accounts again (should show new account due to cache invalidation)
956
- const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
957
- budget_id: testBudgetId,
958
- });
959
- const accounts = parseToolResult(accountsResult);
960
-
961
- const foundAccount = accounts.data.accounts.find(
962
- (acc: any) => acc.id === createdAccount.data.account.id,
963
- );
964
- expect(foundAccount).toBeDefined();
965
- expect(foundAccount.name).toBe(accountName);
966
- } finally {
967
- // Restore original NODE_ENV
968
- testEnv.restoreEnv();
969
- }
970
- });
971
- });
972
-
973
- describe('Budget Resolution Consistency', () => {
974
- it('should provide consistent error messages for missing budget ID', async () => {
975
- if (testConfig.skipE2ETests) return;
976
-
977
- // Clear default budget first
978
- server.clearDefaultBudget();
979
-
980
- // Test multiple tools for consistent error handling
981
- const toolsToTest = [
982
- 'ynab:list_accounts',
983
- 'ynab:list_categories',
984
- 'ynab:list_payees',
985
- 'ynab:list_transactions',
986
- ];
987
-
988
- for (const toolName of toolsToTest) {
989
- const result = await executeToolCall(server, toolName, {});
990
- expect(isErrorResult(result)).toBe(true);
991
- const errorMessage = getErrorMessage(result);
992
- expect(errorMessage).toContain('No budget ID provided and no default budget set');
993
- expect(errorMessage).toContain('set_default_budget');
994
- }
995
-
996
- // Restore default budget for other tests
997
- await executeToolCall(server, 'ynab:set_default_budget', { budget_id: testBudgetId });
998
- });
999
-
1000
- it('should handle invalid budget ID format consistently', async () => {
1001
- if (testConfig.skipE2ETests) return;
1002
-
1003
- const invalidBudgetId = 'invalid-format';
1004
- const toolsToTest = ['ynab:list_accounts', 'ynab:list_categories', 'ynab:list_payees'];
1005
-
1006
- for (const toolName of toolsToTest) {
1007
- const result = await executeToolCall(server, toolName, { budget_id: invalidBudgetId });
1008
- expect(isErrorResult(result)).toBe(true);
1009
- // All tools should provide similar error handling
1010
- expect(result.content).toBeDefined();
1011
- expect(result.content.length).toBeGreaterThan(0);
1012
- }
1013
- });
1014
- });
1015
-
1016
- describe('Month Data Integration', () => {
1017
- it('should execute month data tools', async () => {
1018
- if (testConfig.skipE2ETests) return;
1019
-
1020
- // Test get_month tool
1021
- const currentMonth = new Date().toISOString().substring(0, 8) + '01';
1022
- const monthResult = await executeToolCall(server, 'ynab:get_month', {
1023
- budget_id: testBudgetId,
1024
- month: currentMonth,
1025
- });
1026
- const monthData = parseToolResult(monthResult);
1027
-
1028
- expect(monthData.data, 'Month data should return data object').toBeDefined();
1029
- expect(monthData.data.month || monthData.data, 'Should contain month info').toBeDefined();
1030
-
1031
- // Test list_months tool
1032
- const monthsResult = await executeToolCall(server, 'ynab:list_months', {
1033
- budget_id: testBudgetId,
1034
- });
1035
- const monthsData = parseToolResult(monthsResult);
1036
-
1037
- expect(monthsData.data).toBeDefined();
1038
- expect(Array.isArray(monthsData.data.months), 'Should return months array').toBe(true);
1039
- });
1040
- });
1041
-
1042
- describe('Tool Registry Integration', () => {
1043
- it('should demonstrate tool registry functionality', async () => {
1044
- if (testConfig.skipE2ETests) return;
1045
-
1046
- // Test that tool listing includes all expected tools
1047
- const toolsResult = await server.handleListTools();
1048
- expect(toolsResult.tools).toBeDefined();
1049
- expect(Array.isArray(toolsResult.tools)).toBe(true);
1050
- expect(toolsResult.tools.length).toBeGreaterThan(20);
1051
-
1052
- // Verify key v0.8.x tools are present (tools are registered without ynab: prefix)
1053
- const toolNames = toolsResult.tools.map((tool: any) => tool.name);
1054
- expect(toolNames, 'Should contain list_budgets tool').toContain('list_budgets');
1055
- expect(toolNames, 'Should contain get_month tool').toContain('get_month');
1056
- expect(toolNames, 'Should contain list_months tool').toContain('list_months');
1057
- expect(toolNames, 'Should contain compare_transactions tool').toContain(
1058
- 'compare_transactions',
1059
- );
1060
- expect(toolNames, 'Should contain diagnostic_info tool').toContain('diagnostic_info');
1061
-
1062
- // Test that each tool has proper schema validation
1063
- for (const tool of toolsResult.tools) {
1064
- expect(tool.name).toBeDefined();
1065
- expect(tool.description).toBeDefined();
1066
- expect(tool.inputSchema).toBeDefined();
1067
-
1068
- // Verify that all tools define outputSchema (as guaranteed by CHANGELOG.md and docs/reference/TOOLS.md)
1069
- // Note: Some utility tools like diagnostic_info or clear_cache may not define structured outputs,
1070
- // but most data-retrieval and CRUD tools should have output schemas.
1071
- expect(
1072
- tool.outputSchema,
1073
- `Tool '${tool.name}' should define an outputSchema`,
1074
- ).toBeDefined();
1075
- }
1076
- });
1077
- });
1078
-
1079
- describe('Module Integration Tests', () => {
1080
- it('should verify resource manager integration', async () => {
1081
- if (testConfig.skipE2ETests) return;
1082
-
1083
- // Test resource listing
1084
- const resourcesResult = await server.handleListResources();
1085
- expect(resourcesResult.resources).toBeDefined();
1086
- expect(Array.isArray(resourcesResult.resources)).toBe(true);
1087
-
1088
- // Test reading a specific resource
1089
- if (resourcesResult.resources.length > 0) {
1090
- const resource = resourcesResult.resources[0];
1091
- const readResult = await server.handleReadResource({
1092
- uri: resource.uri,
1093
- });
1094
- expect(readResult.contents).toBeDefined();
1095
- }
1096
- });
1097
-
1098
- it('should verify prompt manager integration', async () => {
1099
- if (testConfig.skipE2ETests) return;
1100
-
1101
- // Test prompt listing
1102
- const promptsResult = await server.handleListPrompts();
1103
- expect(promptsResult.prompts).toBeDefined();
1104
- expect(Array.isArray(promptsResult.prompts)).toBe(true);
1105
-
1106
- // Test getting a specific prompt
1107
- if (promptsResult.prompts.length > 0) {
1108
- const prompt = promptsResult.prompts[0];
1109
- const getResult = await server.handleGetPrompt({
1110
- name: prompt.name,
1111
- arguments: {},
1112
- });
1113
- expect(getResult.messages).toBeDefined();
1114
- }
1115
- });
1116
-
1117
- it('should verify diagnostic manager integration', async () => {
1118
- if (testConfig.skipE2ETests) return;
1119
-
1120
- // Test diagnostic info tool