@dizzlkheinz/ynab-mcpb 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci-tests.yml +4 -4
- package/.github/workflows/full-integration.yml +2 -2
- package/.github/workflows/publish.yml +1 -1
- package/.github/workflows/release.yml +2 -2
- package/CHANGELOG.md +12 -1
- package/CLAUDE.md +10 -7
- package/README.md +6 -1
- package/dist/bundle/index.cjs +52 -52
- package/dist/server/YNABMCPServer.d.ts +7 -2
- package/dist/server/YNABMCPServer.js +42 -11
- package/dist/server/cacheManager.js +6 -5
- package/dist/server/completions.d.ts +25 -0
- package/dist/server/completions.js +160 -0
- package/dist/server/config.d.ts +2 -2
- package/dist/server/errorHandler.js +1 -0
- package/dist/server/rateLimiter.js +3 -1
- package/dist/server/resources.d.ts +1 -0
- package/dist/server/resources.js +33 -16
- package/dist/server/securityMiddleware.d.ts +2 -1
- package/dist/server/securityMiddleware.js +1 -0
- package/dist/server/toolRegistry.d.ts +9 -0
- package/dist/server/toolRegistry.js +11 -0
- package/dist/tools/adapters.d.ts +3 -1
- package/dist/tools/adapters.js +1 -0
- package/dist/tools/reconciliation/executor.d.ts +2 -0
- package/dist/tools/reconciliation/executor.js +26 -9
- package/dist/tools/reconciliation/index.d.ts +3 -2
- package/dist/tools/reconciliation/index.js +4 -3
- package/docs/reference/API.md +68 -27
- package/package.json +2 -2
- package/src/__tests__/comprehensive.integration.test.ts +4 -4
- package/src/__tests__/performance.test.ts +1 -2
- package/src/__tests__/smoke.e2e.test.ts +70 -0
- package/src/__tests__/testUtils.ts +2 -113
- package/src/server/YNABMCPServer.ts +64 -10
- package/src/server/__tests__/completions.integration.test.ts +117 -0
- package/src/server/__tests__/completions.test.ts +319 -0
- package/src/server/__tests__/resources.template.test.ts +3 -3
- package/src/server/__tests__/resources.test.ts +3 -3
- package/src/server/__tests__/toolRegistration.test.ts +1 -1
- package/src/server/cacheManager.ts +7 -6
- package/src/server/completions.ts +279 -0
- package/src/server/errorHandler.ts +1 -0
- package/src/server/rateLimiter.ts +4 -1
- package/src/server/resources.ts +49 -13
- package/src/server/securityMiddleware.ts +1 -0
- package/src/server/toolRegistry.ts +42 -0
- package/src/tools/adapters.ts +22 -1
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +12 -26
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +462 -0
- package/src/tools/reconciliation/__tests__/executor.test.ts +36 -31
- package/src/tools/reconciliation/executor.ts +56 -27
- package/src/tools/reconciliation/index.ts +7 -3
- package/vitest.config.ts +2 -0
- package/src/__tests__/delta.performance.test.ts +0 -80
- package/src/__tests__/workflows.e2e.test.ts +0 -1658
|
@@ -1,1658 +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('v0.8.x Architecture Integration Tests', () => {
|
|
825
|
-
describe('Cache System Verification', () => {
|
|
826
|
-
it('should demonstrate cache warming after default budget set', async () => {
|
|
827
|
-
if (testConfig.skipE2ETests) return;
|
|
828
|
-
|
|
829
|
-
// Enable caching for this test
|
|
830
|
-
testEnv.enableCache();
|
|
831
|
-
|
|
832
|
-
try {
|
|
833
|
-
// Get initial cache stats
|
|
834
|
-
const initialStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
|
|
835
|
-
const initialStats = parseToolResult(initialStatsResult);
|
|
836
|
-
const initialCacheStats = initialStats.data?.cache;
|
|
837
|
-
|
|
838
|
-
// Set default budget (should trigger cache warming)
|
|
839
|
-
await executeToolCall(server, 'ynab:set_default_budget', {
|
|
840
|
-
budget_id: testBudgetId,
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
// Allow time for cache warming (fire-and-forget)
|
|
844
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
845
|
-
|
|
846
|
-
// Get updated cache stats
|
|
847
|
-
const finalStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
|
|
848
|
-
const finalStats = parseToolResult(finalStatsResult);
|
|
849
|
-
const finalCacheStats = finalStats.data?.cache;
|
|
850
|
-
|
|
851
|
-
// Verify cache warming occurred
|
|
852
|
-
expect(finalCacheStats?.entries).toBeGreaterThan(initialCacheStats?.entries || 0);
|
|
853
|
-
expect(finalCacheStats?.hits).toBeGreaterThanOrEqual(0);
|
|
854
|
-
} finally {
|
|
855
|
-
// Restore original NODE_ENV
|
|
856
|
-
testEnv.restoreEnv();
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
it('should demonstrate LRU eviction and observability metrics', async () => {
|
|
861
|
-
if (testConfig.skipE2ETests) return;
|
|
862
|
-
|
|
863
|
-
// Enable caching for this test (bypass NODE_ENV='test' check)
|
|
864
|
-
testEnv.enableCache();
|
|
865
|
-
|
|
866
|
-
try {
|
|
867
|
-
// Get initial cache stats
|
|
868
|
-
const initialStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
|
|
869
|
-
const initialStats = parseToolResult(initialStatsResult);
|
|
870
|
-
const initialCacheStats = initialStats.data?.cache;
|
|
871
|
-
|
|
872
|
-
// Perform operations that should hit cache
|
|
873
|
-
await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
|
|
874
|
-
await executeToolCall(server, 'ynab:list_categories', { budget_id: testBudgetId });
|
|
875
|
-
await executeToolCall(server, 'ynab:list_payees', { budget_id: testBudgetId });
|
|
876
|
-
|
|
877
|
-
// Perform same operations again (should hit cache)
|
|
878
|
-
await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
|
|
879
|
-
await executeToolCall(server, 'ynab:list_categories', { budget_id: testBudgetId });
|
|
880
|
-
await executeToolCall(server, 'ynab:list_payees', { budget_id: testBudgetId });
|
|
881
|
-
|
|
882
|
-
// Get final cache stats
|
|
883
|
-
const finalStatsResult = await executeToolCall(server, 'ynab:diagnostic_info');
|
|
884
|
-
const finalStats = parseToolResult(finalStatsResult);
|
|
885
|
-
const finalCacheStats = finalStats.data?.cache;
|
|
886
|
-
|
|
887
|
-
// Verify cache behavior
|
|
888
|
-
expect(finalCacheStats?.hits).toBeGreaterThan(initialCacheStats?.hits || 0);
|
|
889
|
-
expect(finalCacheStats?.misses).toBeGreaterThan(initialCacheStats?.misses || 0);
|
|
890
|
-
expect(finalCacheStats?.hits).toBeGreaterThan(0);
|
|
891
|
-
expect(finalCacheStats?.entries).toBeGreaterThan(0);
|
|
892
|
-
} finally {
|
|
893
|
-
// Restore original NODE_ENV
|
|
894
|
-
testEnv.restoreEnv();
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
it('should demonstrate cache invalidation on write operations', async () => {
|
|
899
|
-
if (testConfig.skipE2ETests) return;
|
|
900
|
-
|
|
901
|
-
// Enable caching for this test
|
|
902
|
-
testEnv.enableCache();
|
|
903
|
-
|
|
904
|
-
try {
|
|
905
|
-
// Prime cache by listing accounts
|
|
906
|
-
await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
|
|
907
|
-
|
|
908
|
-
// Create new account (should invalidate accounts cache)
|
|
909
|
-
const accountName = TestData.generateAccountName();
|
|
910
|
-
const createResult = await executeToolCall(server, 'ynab:create_account', {
|
|
911
|
-
budget_id: testBudgetId,
|
|
912
|
-
name: accountName,
|
|
913
|
-
type: 'checking',
|
|
914
|
-
balance: 10000,
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
// Validate output schema
|
|
918
|
-
const createValidation = validateOutputSchema(server, 'create_account', createResult);
|
|
919
|
-
expect(createValidation.valid).toBe(true);
|
|
920
|
-
if (!createValidation.valid) {
|
|
921
|
-
console.error('create_account schema validation errors:', createValidation.errors);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const createdAccount = parseToolResult(createResult);
|
|
925
|
-
cleanup.trackAccount(createdAccount.data.account.id);
|
|
926
|
-
|
|
927
|
-
// List accounts again (should show new account due to cache invalidation)
|
|
928
|
-
const accountsResult = await executeToolCall(server, 'ynab:list_accounts', {
|
|
929
|
-
budget_id: testBudgetId,
|
|
930
|
-
});
|
|
931
|
-
const accounts = parseToolResult(accountsResult);
|
|
932
|
-
|
|
933
|
-
const foundAccount = accounts.data.accounts.find(
|
|
934
|
-
(acc: any) => acc.id === createdAccount.data.account.id,
|
|
935
|
-
);
|
|
936
|
-
expect(foundAccount).toBeDefined();
|
|
937
|
-
expect(foundAccount.name).toBe(accountName);
|
|
938
|
-
} finally {
|
|
939
|
-
// Restore original NODE_ENV
|
|
940
|
-
testEnv.restoreEnv();
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
describe('Budget Resolution Consistency', () => {
|
|
946
|
-
it('should provide consistent error messages for missing budget ID', async () => {
|
|
947
|
-
if (testConfig.skipE2ETests) return;
|
|
948
|
-
|
|
949
|
-
// Clear default budget first
|
|
950
|
-
server.clearDefaultBudget();
|
|
951
|
-
|
|
952
|
-
// Test multiple tools for consistent error handling
|
|
953
|
-
const toolsToTest = [
|
|
954
|
-
'ynab:list_accounts',
|
|
955
|
-
'ynab:list_categories',
|
|
956
|
-
'ynab:list_payees',
|
|
957
|
-
'ynab:list_transactions',
|
|
958
|
-
];
|
|
959
|
-
|
|
960
|
-
for (const toolName of toolsToTest) {
|
|
961
|
-
const result = await executeToolCall(server, toolName, {});
|
|
962
|
-
expect(isErrorResult(result)).toBe(true);
|
|
963
|
-
const errorMessage = getErrorMessage(result);
|
|
964
|
-
expect(errorMessage).toContain('No budget ID provided and no default budget set');
|
|
965
|
-
expect(errorMessage).toContain('set_default_budget');
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Restore default budget for other tests
|
|
969
|
-
await executeToolCall(server, 'ynab:set_default_budget', { budget_id: testBudgetId });
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
it('should handle invalid budget ID format consistently', async () => {
|
|
973
|
-
if (testConfig.skipE2ETests) return;
|
|
974
|
-
|
|
975
|
-
const invalidBudgetId = 'invalid-format';
|
|
976
|
-
const toolsToTest = ['ynab:list_accounts', 'ynab:list_categories', 'ynab:list_payees'];
|
|
977
|
-
|
|
978
|
-
for (const toolName of toolsToTest) {
|
|
979
|
-
const result = await executeToolCall(server, toolName, { budget_id: invalidBudgetId });
|
|
980
|
-
expect(isErrorResult(result)).toBe(true);
|
|
981
|
-
// All tools should provide similar error handling
|
|
982
|
-
expect(result.content).toBeDefined();
|
|
983
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
describe('Month Data Integration', () => {
|
|
989
|
-
it('should execute month data tools', async () => {
|
|
990
|
-
if (testConfig.skipE2ETests) return;
|
|
991
|
-
|
|
992
|
-
// Test get_month tool
|
|
993
|
-
const currentMonth = new Date().toISOString().substring(0, 8) + '01';
|
|
994
|
-
const monthResult = await executeToolCall(server, 'ynab:get_month', {
|
|
995
|
-
budget_id: testBudgetId,
|
|
996
|
-
month: currentMonth,
|
|
997
|
-
});
|
|
998
|
-
const monthData = parseToolResult(monthResult);
|
|
999
|
-
|
|
1000
|
-
expect(monthData.data, 'Month data should return data object').toBeDefined();
|
|
1001
|
-
expect(monthData.data.month || monthData.data, 'Should contain month info').toBeDefined();
|
|
1002
|
-
|
|
1003
|
-
// Test list_months tool
|
|
1004
|
-
const monthsResult = await executeToolCall(server, 'ynab:list_months', {
|
|
1005
|
-
budget_id: testBudgetId,
|
|
1006
|
-
});
|
|
1007
|
-
const monthsData = parseToolResult(monthsResult);
|
|
1008
|
-
|
|
1009
|
-
expect(monthsData.data).toBeDefined();
|
|
1010
|
-
expect(Array.isArray(monthsData.data.months), 'Should return months array').toBe(true);
|
|
1011
|
-
});
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
describe('Tool Registry Integration', () => {
|
|
1015
|
-
it('should demonstrate tool registry functionality', async () => {
|
|
1016
|
-
if (testConfig.skipE2ETests) return;
|
|
1017
|
-
|
|
1018
|
-
// Test that tool listing includes all expected tools
|
|
1019
|
-
const toolsResult = await server.handleListTools();
|
|
1020
|
-
expect(toolsResult.tools).toBeDefined();
|
|
1021
|
-
expect(Array.isArray(toolsResult.tools)).toBe(true);
|
|
1022
|
-
expect(toolsResult.tools.length).toBeGreaterThan(20);
|
|
1023
|
-
|
|
1024
|
-
// Verify key v0.8.x tools are present (tools are registered without ynab: prefix)
|
|
1025
|
-
const toolNames = toolsResult.tools.map((tool: any) => tool.name);
|
|
1026
|
-
expect(toolNames, 'Should contain list_budgets tool').toContain('list_budgets');
|
|
1027
|
-
expect(toolNames, 'Should contain get_month tool').toContain('get_month');
|
|
1028
|
-
expect(toolNames, 'Should contain list_months tool').toContain('list_months');
|
|
1029
|
-
expect(toolNames, 'Should contain compare_transactions tool').toContain(
|
|
1030
|
-
'compare_transactions',
|
|
1031
|
-
);
|
|
1032
|
-
expect(toolNames, 'Should contain diagnostic_info tool').toContain('diagnostic_info');
|
|
1033
|
-
|
|
1034
|
-
// Test that each tool has proper schema validation
|
|
1035
|
-
for (const tool of toolsResult.tools) {
|
|
1036
|
-
expect(tool.name).toBeDefined();
|
|
1037
|
-
expect(tool.description).toBeDefined();
|
|
1038
|
-
expect(tool.inputSchema).toBeDefined();
|
|
1039
|
-
|
|
1040
|
-
// Output schemas are optional; tools may omit them.
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
describe('Module Integration Tests', () => {
|
|
1046
|
-
it('should verify resource manager integration', async () => {
|
|
1047
|
-
if (testConfig.skipE2ETests) return;
|
|
1048
|
-
|
|
1049
|
-
// Test resource listing
|
|
1050
|
-
const resourcesResult = await server.handleListResources();
|
|
1051
|
-
expect(resourcesResult.resources).toBeDefined();
|
|
1052
|
-
expect(Array.isArray(resourcesResult.resources)).toBe(true);
|
|
1053
|
-
|
|
1054
|
-
// Test reading a specific resource
|
|
1055
|
-
if (resourcesResult.resources.length > 0) {
|
|
1056
|
-
const resource = resourcesResult.resources[0];
|
|
1057
|
-
const readResult = await server.handleReadResource({
|
|
1058
|
-
uri: resource.uri,
|
|
1059
|
-
});
|
|
1060
|
-
expect(readResult.contents).toBeDefined();
|
|
1061
|
-
}
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
it('should verify prompt manager integration', async () => {
|
|
1065
|
-
if (testConfig.skipE2ETests) return;
|
|
1066
|
-
|
|
1067
|
-
// Test prompt listing
|
|
1068
|
-
const promptsResult = await server.handleListPrompts();
|
|
1069
|
-
expect(promptsResult.prompts).toBeDefined();
|
|
1070
|
-
expect(Array.isArray(promptsResult.prompts)).toBe(true);
|
|
1071
|
-
|
|
1072
|
-
// Test getting a specific prompt
|
|
1073
|
-
if (promptsResult.prompts.length > 0) {
|
|
1074
|
-
const prompt = promptsResult.prompts[0];
|
|
1075
|
-
const getResult = await server.handleGetPrompt({
|
|
1076
|
-
name: prompt.name,
|
|
1077
|
-
arguments: {},
|
|
1078
|
-
});
|
|
1079
|
-
expect(getResult.messages).toBeDefined();
|
|
1080
|
-
}
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
it('should verify diagnostic manager integration', async () => {
|
|
1084
|
-
if (testConfig.skipE2ETests) return;
|
|
1085
|
-
|
|
1086
|
-
// Test diagnostic info tool
|
|
1087
|
-
const diagnosticResult = await executeToolCall(server, 'ynab:diagnostic_info');
|
|
1088
|
-
const diagnostic = parseToolResult(diagnosticResult);
|
|
1089
|
-
|
|
1090
|
-
expect(diagnostic.data, 'Diagnostic should return data object').toBeDefined();
|
|
1091
|
-
|
|
1092
|
-
// The diagnostic data is in the root of data, not under diagnostics
|
|
1093
|
-
expect(diagnostic.data.timestamp, 'Should contain timestamp').toBeDefined();
|
|
1094
|
-
expect(diagnostic.data.server, 'Should contain server info').toBeDefined();
|
|
1095
|
-
expect(diagnostic.data.memory, 'Should contain memory info').toBeDefined();
|
|
1096
|
-
expect(diagnostic.data.environment, 'Should contain environment info').toBeDefined();
|
|
1097
|
-
expect(diagnostic.data.cache, 'Should contain cache info').toBeDefined();
|
|
1098
|
-
});
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
describe('Backward Compatibility Verification', () => {
|
|
1102
|
-
it('should maintain v0.7.x API compatibility', async () => {
|
|
1103
|
-
if (testConfig.skipE2ETests) return;
|
|
1104
|
-
|
|
1105
|
-
// Test that all existing tool calls work identically
|
|
1106
|
-
const v7Tools = [
|
|
1107
|
-
{ name: 'ynab:list_budgets', args: {} },
|
|
1108
|
-
{ name: 'ynab:list_accounts', args: { budget_id: testBudgetId } },
|
|
1109
|
-
{ name: 'ynab:list_categories', args: { budget_id: testBudgetId } },
|
|
1110
|
-
{ name: 'ynab:list_payees', args: { budget_id: testBudgetId } },
|
|
1111
|
-
{ name: 'ynab:get_user', args: {} },
|
|
1112
|
-
];
|
|
1113
|
-
|
|
1114
|
-
for (const tool of v7Tools) {
|
|
1115
|
-
const result = await executeToolCall(server, tool.name, tool.args);
|
|
1116
|
-
const parsed = parseToolResult(result);
|
|
1117
|
-
|
|
1118
|
-
// Verify response structure is consistent with v0.7.x
|
|
1119
|
-
expect(parsed.data).toBeDefined();
|
|
1120
|
-
expect(parsed.success).toBe(true);
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
|
|
1124
|
-
it('should maintain response format consistency', async () => {
|
|
1125
|
-
if (testConfig.skipE2ETests) return;
|
|
1126
|
-
|
|
1127
|
-
// Test that response formats match expected v0.7.x structure
|
|
1128
|
-
const budgetsResult = await executeToolCall(server, 'ynab:list_budgets');
|
|
1129
|
-
const budgets = parseToolResult(budgetsResult);
|
|
1130
|
-
|
|
1131
|
-
// Verify standard response wrapper
|
|
1132
|
-
expect(budgets).toHaveProperty('success');
|
|
1133
|
-
expect(budgets).toHaveProperty('data');
|
|
1134
|
-
expect(budgets.success).toBe(true);
|
|
1135
|
-
expect(budgets.data).toHaveProperty('budgets');
|
|
1136
|
-
});
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
describe('Performance Regression Tests', () => {
|
|
1140
|
-
it('should not introduce performance regressions', async () => {
|
|
1141
|
-
if (testConfig.skipE2ETests) return;
|
|
1142
|
-
|
|
1143
|
-
// Test response times for common operations
|
|
1144
|
-
const operations = [
|
|
1145
|
-
{ name: 'ynab:list_budgets', args: {} },
|
|
1146
|
-
{ name: 'ynab:list_accounts', args: { budget_id: testBudgetId } },
|
|
1147
|
-
{ name: 'ynab:list_categories', args: { budget_id: testBudgetId } },
|
|
1148
|
-
];
|
|
1149
|
-
|
|
1150
|
-
for (const operation of operations) {
|
|
1151
|
-
const startTime = Date.now();
|
|
1152
|
-
await executeToolCall(server, operation.name, operation.args);
|
|
1153
|
-
const endTime = Date.now();
|
|
1154
|
-
const duration = endTime - startTime;
|
|
1155
|
-
|
|
1156
|
-
// Response should be reasonably fast (under 5 seconds for E2E)
|
|
1157
|
-
expect(duration).toBeLessThan(5000);
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
it('should demonstrate cache performance improvements', async () => {
|
|
1162
|
-
if (testConfig.skipE2ETests) return;
|
|
1163
|
-
|
|
1164
|
-
// Enable caching for this test
|
|
1165
|
-
testEnv.enableCache();
|
|
1166
|
-
|
|
1167
|
-
try {
|
|
1168
|
-
// First call (cache miss)
|
|
1169
|
-
const startTime1 = Date.now();
|
|
1170
|
-
await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
|
|
1171
|
-
const duration1 = Date.now() - startTime1;
|
|
1172
|
-
|
|
1173
|
-
// Second call (cache hit)
|
|
1174
|
-
const startTime2 = Date.now();
|
|
1175
|
-
await executeToolCall(server, 'ynab:list_accounts', { budget_id: testBudgetId });
|
|
1176
|
-
const duration2 = Date.now() - startTime2;
|
|
1177
|
-
|
|
1178
|
-
// Cached call should be faster (allowing for some variance in E2E environment)
|
|
1179
|
-
expect(duration2).toBeLessThanOrEqual(duration1 + 500); // Allow 500ms tolerance for E2E environment
|
|
1180
|
-
} finally {
|
|
1181
|
-
// Restore original NODE_ENV
|
|
1182
|
-
testEnv.restoreEnv();
|
|
1183
|
-
}
|
|
1184
|
-
});
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
describe('Enhanced Error Handling', () => {
|
|
1188
|
-
it('should provide improved error messages with actionable suggestions', async () => {
|
|
1189
|
-
if (testConfig.skipE2ETests) return;
|
|
1190
|
-
|
|
1191
|
-
// Clear default budget
|
|
1192
|
-
server.clearDefaultBudget();
|
|
1193
|
-
|
|
1194
|
-
const result = await executeToolCall(server, 'ynab:list_accounts', {});
|
|
1195
|
-
expect(isErrorResult(result)).toBe(true);
|
|
1196
|
-
const errorMessage = getErrorMessage(result);
|
|
1197
|
-
|
|
1198
|
-
// Error should provide actionable guidance
|
|
1199
|
-
expect(errorMessage).toContain('No budget ID provided and no default budget set');
|
|
1200
|
-
expect(errorMessage).toContain('set_default_budget');
|
|
1201
|
-
expect(errorMessage).toContain('budget_id parameter');
|
|
1202
|
-
|
|
1203
|
-
// Restore default budget
|
|
1204
|
-
await executeToolCall(server, 'ynab:set_default_budget', { budget_id: testBudgetId });
|
|
1205
|
-
});
|
|
1206
|
-
});
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
describe('Error Handling Workflow', () => {
|
|
1210
|
-
it('should handle invalid budget ID gracefully', async () => {
|
|
1211
|
-
if (testConfig.skipE2ETests) return;
|
|
1212
|
-
|
|
1213
|
-
const result = await executeToolCall(server, 'ynab:get_budget', {
|
|
1214
|
-
budget_id: 'invalid-budget-id',
|
|
1215
|
-
});
|
|
1216
|
-
expect(isErrorResult(result)).toBe(true);
|
|
1217
|
-
expect(result.content).toBeDefined();
|
|
1218
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
1219
|
-
|
|
1220
|
-
// Verify error response contract: error responses should not have success: true
|
|
1221
|
-
const textContent = result.content.find((c) => c.type === 'text');
|
|
1222
|
-
if (textContent && textContent.type === 'text') {
|
|
1223
|
-
const parsed = JSON.parse(textContent.text);
|
|
1224
|
-
expect(parsed).toHaveProperty('error');
|
|
1225
|
-
// If success property exists, it should be false for errors
|
|
1226
|
-
if ('success' in parsed) {
|
|
1227
|
-
expect(parsed.success).toBe(false);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
it('should handle invalid account ID gracefully', async () => {
|
|
1233
|
-
if (testConfig.skipE2ETests) return;
|
|
1234
|
-
|
|
1235
|
-
const result = await executeToolCall(server, 'ynab:get_account', {
|
|
1236
|
-
budget_id: testBudgetId,
|
|
1237
|
-
account_id: 'invalid-account-id',
|
|
1238
|
-
});
|
|
1239
|
-
expect(isErrorResult(result)).toBe(true);
|
|
1240
|
-
expect(result.content).toBeDefined();
|
|
1241
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
1242
|
-
|
|
1243
|
-
// Verify error response contract
|
|
1244
|
-
const textContent = result.content.find((c) => c.type === 'text');
|
|
1245
|
-
if (textContent && textContent.type === 'text') {
|
|
1246
|
-
const parsed = JSON.parse(textContent.text);
|
|
1247
|
-
expect(parsed).toHaveProperty('error');
|
|
1248
|
-
if ('success' in parsed) {
|
|
1249
|
-
expect(parsed.success).toBe(false);
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
it('should handle invalid transaction ID gracefully', async () => {
|
|
1255
|
-
if (testConfig.skipE2ETests) return;
|
|
1256
|
-
|
|
1257
|
-
const result = await executeToolCall(server, 'ynab:get_transaction', {
|
|
1258
|
-
budget_id: testBudgetId,
|
|
1259
|
-
transaction_id: 'invalid-transaction-id',
|
|
1260
|
-
});
|
|
1261
|
-
expect(isErrorResult(result)).toBe(true);
|
|
1262
|
-
expect(result.content).toBeDefined();
|
|
1263
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
1264
|
-
|
|
1265
|
-
// Verify error response contract
|
|
1266
|
-
const textContent = result.content.find((c) => c.type === 'text');
|
|
1267
|
-
if (textContent && textContent.type === 'text') {
|
|
1268
|
-
const parsed = JSON.parse(textContent.text);
|
|
1269
|
-
expect(parsed).toHaveProperty('error');
|
|
1270
|
-
if ('success' in parsed) {
|
|
1271
|
-
expect(parsed.success).toBe(false);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
describe('Output Schema Validation', () => {
|
|
1278
|
-
it('should validate list_budgets output schema', async () => {
|
|
1279
|
-
if (testConfig.skipE2ETests) return;
|
|
1280
|
-
|
|
1281
|
-
const result = await executeToolCall(server, 'ynab:list_budgets');
|
|
1282
|
-
const validation = validateOutputSchema(server, 'list_budgets', result);
|
|
1283
|
-
expect(validation.hasSchema).toBe(true);
|
|
1284
|
-
expect(validation.valid).toBe(true);
|
|
1285
|
-
if (!validation.valid) {
|
|
1286
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
it('should validate list_accounts output schema', async () => {
|
|
1291
|
-
if (testConfig.skipE2ETests) return;
|
|
1292
|
-
|
|
1293
|
-
const result = await executeToolCall(server, 'ynab:list_accounts', {
|
|
1294
|
-
budget_id: testBudgetId,
|
|
1295
|
-
});
|
|
1296
|
-
const validation = validateOutputSchema(server, 'list_accounts', result);
|
|
1297
|
-
expect(validation.hasSchema).toBe(true);
|
|
1298
|
-
expect(validation.valid).toBe(true);
|
|
1299
|
-
if (!validation.valid) {
|
|
1300
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
it('should validate list_transactions output schema', async () => {
|
|
1305
|
-
if (testConfig.skipE2ETests) return;
|
|
1306
|
-
|
|
1307
|
-
const result = await executeToolCall(server, 'ynab:list_transactions', {
|
|
1308
|
-
budget_id: testBudgetId,
|
|
1309
|
-
since_date: '2025-01-01',
|
|
1310
|
-
});
|
|
1311
|
-
const validation = validateOutputSchema(server, 'list_transactions', result);
|
|
1312
|
-
expect(validation.hasSchema).toBe(true);
|
|
1313
|
-
expect(validation.valid).toBe(true);
|
|
1314
|
-
if (!validation.valid) {
|
|
1315
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1316
|
-
}
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
it('should validate list_categories output schema', async () => {
|
|
1320
|
-
if (testConfig.skipE2ETests) return;
|
|
1321
|
-
|
|
1322
|
-
const result = await executeToolCall(server, 'ynab:list_categories', {
|
|
1323
|
-
budget_id: testBudgetId,
|
|
1324
|
-
});
|
|
1325
|
-
const validation = validateOutputSchema(server, 'list_categories', result);
|
|
1326
|
-
expect(validation.hasSchema).toBe(true);
|
|
1327
|
-
expect(validation.valid).toBe(true);
|
|
1328
|
-
if (!validation.valid) {
|
|
1329
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1330
|
-
}
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
it('should validate list_payees output schema', async () => {
|
|
1334
|
-
if (testConfig.skipE2ETests) return;
|
|
1335
|
-
|
|
1336
|
-
const result = await executeToolCall(server, 'ynab:list_payees', {
|
|
1337
|
-
budget_id: testBudgetId,
|
|
1338
|
-
});
|
|
1339
|
-
const validation = validateOutputSchema(server, 'list_payees', result);
|
|
1340
|
-
expect(validation.hasSchema).toBe(true);
|
|
1341
|
-
expect(validation.valid).toBe(true);
|
|
1342
|
-
if (!validation.valid) {
|
|
1343
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1344
|
-
}
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
it('should validate list_months output schema', async () => {
|
|
1348
|
-
if (testConfig.skipE2ETests) return;
|
|
1349
|
-
|
|
1350
|
-
const result = await executeToolCall(server, 'ynab:list_months', {
|
|
1351
|
-
budget_id: testBudgetId,
|
|
1352
|
-
});
|
|
1353
|
-
const validation = validateOutputSchema(server, 'list_months', result);
|
|
1354
|
-
expect(validation.hasSchema).toBe(true);
|
|
1355
|
-
expect(validation.valid).toBe(true);
|
|
1356
|
-
if (!validation.valid) {
|
|
1357
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1358
|
-
}
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
it('should validate get_month output schema', async () => {
|
|
1362
|
-
if (testConfig.skipE2ETests) return;
|
|
1363
|
-
|
|
1364
|
-
const currentMonth = getCurrentMonth();
|
|
1365
|
-
const result = await executeToolCall(server, 'ynab:get_month', {
|
|
1366
|
-
budget_id: testBudgetId,
|
|
1367
|
-
month: currentMonth,
|
|
1368
|
-
});
|
|
1369
|
-
const validation = validateOutputSchema(server, 'get_month', result);
|
|
1370
|
-
expect(validation.hasSchema).toBe(true);
|
|
1371
|
-
expect(validation.valid).toBe(true);
|
|
1372
|
-
if (!validation.valid) {
|
|
1373
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
it('should validate get_user output schema', async () => {
|
|
1378
|
-
if (testConfig.skipE2ETests) return;
|
|
1379
|
-
|
|
1380
|
-
const result = await executeToolCall(server, 'ynab:get_user');
|
|
1381
|
-
const validation = validateOutputSchema(server, 'get_user', result);
|
|
1382
|
-
expect(validation.hasSchema).toBe(true);
|
|
1383
|
-
expect(validation.valid).toBe(true);
|
|
1384
|
-
if (!validation.valid) {
|
|
1385
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1386
|
-
}
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
it('should validate diagnostic_info output schema', async () => {
|
|
1390
|
-
if (testConfig.skipE2ETests) return;
|
|
1391
|
-
|
|
1392
|
-
const result = await executeToolCall(server, 'ynab:diagnostic_info');
|
|
1393
|
-
const validation = validateOutputSchema(server, 'diagnostic_info', result);
|
|
1394
|
-
expect(validation.hasSchema).toBe(true);
|
|
1395
|
-
expect(validation.valid).toBe(true);
|
|
1396
|
-
if (!validation.valid) {
|
|
1397
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1398
|
-
}
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
|
-
it('should validate set_default_budget output schema', async () => {
|
|
1402
|
-
if (testConfig.skipE2ETests) return;
|
|
1403
|
-
|
|
1404
|
-
const result = await executeToolCall(server, 'ynab:set_default_budget', {
|
|
1405
|
-
budget_id: testBudgetId,
|
|
1406
|
-
});
|
|
1407
|
-
const validation = validateOutputSchema(server, 'set_default_budget', result);
|
|
1408
|
-
expect(validation.hasSchema).toBe(true);
|
|
1409
|
-
expect(validation.valid).toBe(true);
|
|
1410
|
-
if (!validation.valid) {
|
|
1411
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1412
|
-
}
|
|
1413
|
-
});
|
|
1414
|
-
|
|
1415
|
-
it('should validate get_default_budget output schema', async () => {
|
|
1416
|
-
if (testConfig.skipE2ETests) return;
|
|
1417
|
-
|
|
1418
|
-
// Ensure default budget is set
|
|
1419
|
-
await executeToolCall(server, 'ynab:set_default_budget', {
|
|
1420
|
-
budget_id: testBudgetId,
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
const result = await executeToolCall(server, 'ynab:get_default_budget');
|
|
1424
|
-
const validation = validateOutputSchema(server, 'get_default_budget', result);
|
|
1425
|
-
expect(validation.hasSchema).toBe(true);
|
|
1426
|
-
expect(validation.valid).toBe(true);
|
|
1427
|
-
if (!validation.valid) {
|
|
1428
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1429
|
-
}
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
it('should validate clear_cache output schema', async () => {
|
|
1433
|
-
if (testConfig.skipE2ETests) return;
|
|
1434
|
-
|
|
1435
|
-
const result = await executeToolCall(server, 'ynab:clear_cache');
|
|
1436
|
-
const validation = validateOutputSchema(server, 'clear_cache', result);
|
|
1437
|
-
expect(validation.hasSchema).toBe(true);
|
|
1438
|
-
expect(validation.valid).toBe(true);
|
|
1439
|
-
if (!validation.valid) {
|
|
1440
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1441
|
-
}
|
|
1442
|
-
});
|
|
1443
|
-
|
|
1444
|
-
it('should validate set_output_format output schema', async () => {
|
|
1445
|
-
if (testConfig.skipE2ETests) return;
|
|
1446
|
-
|
|
1447
|
-
const result = await executeToolCall(server, 'ynab:set_output_format', {
|
|
1448
|
-
minify: false,
|
|
1449
|
-
});
|
|
1450
|
-
const validation = validateOutputSchema(server, 'set_output_format', result);
|
|
1451
|
-
expect(validation.hasSchema).toBe(true);
|
|
1452
|
-
expect(validation.valid).toBe(true);
|
|
1453
|
-
if (!validation.valid) {
|
|
1454
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// Reset to default
|
|
1458
|
-
await executeToolCall(server, 'ynab:set_output_format', {
|
|
1459
|
-
minify: true,
|
|
1460
|
-
});
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
it('should validate reconcile_account output schema', async () => {
|
|
1464
|
-
if (testConfig.skipE2ETests) return;
|
|
1465
|
-
|
|
1466
|
-
const result = await executeToolCall(server, 'ynab:reconcile_account', {
|
|
1467
|
-
budget_id: testBudgetId,
|
|
1468
|
-
account_id: testAccountId,
|
|
1469
|
-
cleared_balance: 0,
|
|
1470
|
-
});
|
|
1471
|
-
const validation = validateOutputSchema(server, 'reconcile_account', result);
|
|
1472
|
-
expect(validation.hasSchema).toBe(true);
|
|
1473
|
-
expect(validation.valid).toBe(true);
|
|
1474
|
-
if (!validation.valid) {
|
|
1475
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1476
|
-
}
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
it('should validate create_transactions (bulk) output schema', async () => {
|
|
1480
|
-
if (testConfig.skipE2ETests) return;
|
|
1481
|
-
|
|
1482
|
-
// Create multiple transactions
|
|
1483
|
-
const transactions = [
|
|
1484
|
-
{
|
|
1485
|
-
account_id: testAccountId,
|
|
1486
|
-
date: new Date().toISOString().split('T')[0],
|
|
1487
|
-
amount: -1000,
|
|
1488
|
-
payee_name: `Test Payee 1 ${Date.now()}`,
|
|
1489
|
-
memo: 'Bulk test 1',
|
|
1490
|
-
cleared: 'uncleared' as const,
|
|
1491
|
-
},
|
|
1492
|
-
{
|
|
1493
|
-
account_id: testAccountId,
|
|
1494
|
-
date: new Date().toISOString().split('T')[0],
|
|
1495
|
-
amount: -2000,
|
|
1496
|
-
payee_name: `Test Payee 2 ${Date.now()}`,
|
|
1497
|
-
memo: 'Bulk test 2',
|
|
1498
|
-
cleared: 'uncleared' as const,
|
|
1499
|
-
},
|
|
1500
|
-
];
|
|
1501
|
-
|
|
1502
|
-
const result = await executeToolCall(server, 'ynab:create_transactions', {
|
|
1503
|
-
budget_id: testBudgetId,
|
|
1504
|
-
transactions,
|
|
1505
|
-
});
|
|
1506
|
-
if (skipIfRateLimitedResult(result)) return;
|
|
1507
|
-
const validation = validateOutputSchema(server, 'create_transactions', result);
|
|
1508
|
-
expect(validation.hasSchema).toBe(true);
|
|
1509
|
-
expect(validation.valid).toBe(true);
|
|
1510
|
-
if (!validation.valid) {
|
|
1511
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// Track transactions for cleanup
|
|
1515
|
-
const parsed = parseToolResult(result);
|
|
1516
|
-
if (parsed.data?.transactions) {
|
|
1517
|
-
parsed.data.transactions.forEach((txn: any) => {
|
|
1518
|
-
cleanup.trackTransaction(txn.id);
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
it('should validate update_transactions (bulk) output schema', async () => {
|
|
1524
|
-
if (testConfig.skipE2ETests) return;
|
|
1525
|
-
|
|
1526
|
-
// First create a transaction to update
|
|
1527
|
-
const createResult = await executeToolCall(server, 'ynab:create_transaction', {
|
|
1528
|
-
budget_id: testBudgetId,
|
|
1529
|
-
account_id: testAccountId,
|
|
1530
|
-
date: new Date().toISOString().split('T')[0],
|
|
1531
|
-
amount: -3000,
|
|
1532
|
-
payee_name: `Test Update Payee ${Date.now()}`,
|
|
1533
|
-
memo: 'Before update',
|
|
1534
|
-
cleared: 'uncleared',
|
|
1535
|
-
});
|
|
1536
|
-
if (skipIfRateLimitedResult(createResult)) return;
|
|
1537
|
-
const created = parseToolResult(createResult);
|
|
1538
|
-
if (!created?.data?.transaction?.id) {
|
|
1539
|
-
console.warn(
|
|
1540
|
-
'[rate-limit] Skipping update_transactions schema check because create_transaction returned no transaction data',
|
|
1541
|
-
);
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const transactionId = created.data.transaction.id;
|
|
1546
|
-
cleanup.trackTransaction(transactionId);
|
|
1547
|
-
|
|
1548
|
-
// Update the transaction
|
|
1549
|
-
const result = await executeToolCall(server, 'ynab:update_transactions', {
|
|
1550
|
-
budget_id: testBudgetId,
|
|
1551
|
-
transactions: [
|
|
1552
|
-
{
|
|
1553
|
-
id: transactionId,
|
|
1554
|
-
memo: 'After update',
|
|
1555
|
-
},
|
|
1556
|
-
],
|
|
1557
|
-
});
|
|
1558
|
-
if (skipIfRateLimitedResult(result)) return;
|
|
1559
|
-
const validation = validateOutputSchema(server, 'update_transactions', result);
|
|
1560
|
-
expect(validation.hasSchema).toBe(true);
|
|
1561
|
-
expect(validation.valid).toBe(true);
|
|
1562
|
-
if (!validation.valid) {
|
|
1563
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1564
|
-
}
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
it('should validate compare_transactions output schema', async () => {
|
|
1568
|
-
if (testConfig.skipE2ETests) return;
|
|
1569
|
-
|
|
1570
|
-
// Create a minimal CSV for comparison
|
|
1571
|
-
const csvData = `Date,Payee,Amount\n2025-01-15,Test Payee,-10.00`;
|
|
1572
|
-
|
|
1573
|
-
const result = await executeToolCall(server, 'ynab:compare_transactions', {
|
|
1574
|
-
budget_id: testBudgetId,
|
|
1575
|
-
account_id: testAccountId,
|
|
1576
|
-
csv_data: csvData,
|
|
1577
|
-
start_date: '2025-01-01',
|
|
1578
|
-
end_date: '2025-01-31',
|
|
1579
|
-
});
|
|
1580
|
-
if (skipIfRateLimitedResult(result)) return;
|
|
1581
|
-
const validation = validateOutputSchema(server, 'compare_transactions', result);
|
|
1582
|
-
expect(validation.hasSchema).toBe(true);
|
|
1583
|
-
expect(validation.valid).toBe(true);
|
|
1584
|
-
if (!validation.valid) {
|
|
1585
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1586
|
-
}
|
|
1587
|
-
});
|
|
1588
|
-
|
|
1589
|
-
it('should validate export_transactions output schema', async () => {
|
|
1590
|
-
if (testConfig.skipE2ETests) return;
|
|
1591
|
-
|
|
1592
|
-
const result = await executeToolCall(server, 'ynab:export_transactions', {
|
|
1593
|
-
budget_id: testBudgetId,
|
|
1594
|
-
account_id: testAccountId,
|
|
1595
|
-
});
|
|
1596
|
-
if (skipIfRateLimitedResult(result)) return;
|
|
1597
|
-
const validation = validateOutputSchema(server, 'export_transactions', result);
|
|
1598
|
-
expect(validation.hasSchema).toBe(true);
|
|
1599
|
-
expect(validation.valid).toBe(true);
|
|
1600
|
-
if (!validation.valid) {
|
|
1601
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1602
|
-
}
|
|
1603
|
-
});
|
|
1604
|
-
|
|
1605
|
-
it('should validate create_receipt_split_transaction output schema', async () => {
|
|
1606
|
-
if (testConfig.skipE2ETests) return;
|
|
1607
|
-
|
|
1608
|
-
// Get categories to find a valid one
|
|
1609
|
-
const categoriesResult = await executeToolCall(server, 'ynab:list_categories', {
|
|
1610
|
-
budget_id: testBudgetId,
|
|
1611
|
-
});
|
|
1612
|
-
const categories = parseToolResult(categoriesResult);
|
|
1613
|
-
|
|
1614
|
-
// Find a non-hidden category
|
|
1615
|
-
let testCategoryName: string | undefined;
|
|
1616
|
-
for (const group of categories.data.category_groups) {
|
|
1617
|
-
const availableCategory = group.categories?.find((cat: any) => !cat.hidden);
|
|
1618
|
-
if (availableCategory) {
|
|
1619
|
-
testCategoryName = availableCategory.name;
|
|
1620
|
-
break;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
if (!testCategoryName) {
|
|
1625
|
-
console.warn('No available categories found for create_receipt_split_transaction test');
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// Create a minimal receipt split transaction
|
|
1630
|
-
const result = await executeToolCall(server, 'ynab:create_receipt_split_transaction', {
|
|
1631
|
-
budget_id: testBudgetId,
|
|
1632
|
-
account_id: testAccountId,
|
|
1633
|
-
date: new Date().toISOString().split('T')[0],
|
|
1634
|
-
payee_name: `Test Receipt ${Date.now()}`,
|
|
1635
|
-
tax_amount: 100,
|
|
1636
|
-
receipt_items: [
|
|
1637
|
-
{
|
|
1638
|
-
category_name: testCategoryName,
|
|
1639
|
-
amount: 1000,
|
|
1640
|
-
},
|
|
1641
|
-
],
|
|
1642
|
-
});
|
|
1643
|
-
|
|
1644
|
-
const validation = validateOutputSchema(server, 'create_receipt_split_transaction', result);
|
|
1645
|
-
expect(validation.hasSchema).toBe(true);
|
|
1646
|
-
expect(validation.valid).toBe(true);
|
|
1647
|
-
if (!validation.valid) {
|
|
1648
|
-
console.error('Schema validation errors:', validation.errors);
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// Track the created transaction for cleanup
|
|
1652
|
-
const parsed = parseToolResult(result);
|
|
1653
|
-
if (parsed.data?.transaction?.id) {
|
|
1654
|
-
cleanup.trackTransaction(parsed.data.transaction.id);
|
|
1655
|
-
}
|
|
1656
|
-
});
|
|
1657
|
-
});
|
|
1658
|
-
});
|