@dizzlkheinz/ynab-mcpb 0.16.1 → 0.17.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/.env.example +33 -33
- package/.github/workflows/ci-tests.yml +45 -45
- package/.github/workflows/claude-code-review.yml +57 -57
- package/.github/workflows/claude.yml +50 -50
- package/.github/workflows/full-integration.yml +22 -22
- package/.github/workflows/publish.yml +11 -2
- package/CLAUDE.md +33 -47
- package/README.md +8 -10
- package/dist/bundle/index.cjs +54 -54
- package/dist/server/YNABMCPServer.d.ts +120 -54
- package/dist/server/YNABMCPServer.js +28 -381
- package/dist/server/config.d.ts +2 -0
- package/dist/server/config.js +1 -0
- package/dist/server/securityMiddleware.d.ts +37 -8
- package/dist/tools/accountTools.d.ts +2 -0
- package/dist/tools/accountTools.js +45 -0
- package/dist/tools/adapters.d.ts +12 -0
- package/dist/tools/adapters.js +25 -0
- package/dist/tools/budgetTools.d.ts +2 -0
- package/dist/tools/budgetTools.js +30 -0
- package/dist/tools/categoryTools.d.ts +2 -0
- package/dist/tools/categoryTools.js +45 -0
- package/dist/tools/monthTools.d.ts +2 -0
- package/dist/tools/monthTools.js +32 -0
- package/dist/tools/payeeTools.d.ts +2 -0
- package/dist/tools/payeeTools.js +32 -0
- package/dist/tools/reconciliation/index.d.ts +2 -0
- package/dist/tools/reconciliation/index.js +33 -0
- package/dist/tools/schemas/common.d.ts +3 -0
- package/dist/tools/schemas/common.js +3 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/index.d.ts +2 -2
- package/dist/tools/schemas/outputs/index.js +2 -2
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +0 -15
- package/dist/tools/schemas/outputs/utilityOutputs.js +0 -9
- package/dist/tools/transactionTools.d.ts +2 -0
- package/dist/tools/transactionTools.js +124 -0
- package/dist/tools/utilityTools.d.ts +2 -7
- package/dist/tools/utilityTools.js +19 -38
- package/dist/types/index.d.ts +1 -0
- package/dist/types/toolRegistration.d.ts +27 -0
- package/dist/types/toolRegistration.js +1 -0
- package/docs/maintainers/npm-publishing.md +27 -0
- package/docs/reference/API.md +15 -70
- package/docs/technical/reconciliation-system-architecture.md +2251 -2251
- package/package.json +6 -6
- package/scripts/analyze-bundle.mjs +41 -41
- package/scripts/generate-mcpb.ps1 +95 -95
- package/scripts/run-domain-integration-tests.js +4 -1
- package/scripts/watch-and-restart.ps1 +49 -49
- package/src/__tests__/comprehensive.integration.test.ts +0 -28
- package/src/__tests__/performance.test.ts +4 -12
- package/src/__tests__/setup.ts +45 -14
- package/src/__tests__/workflows.e2e.test.ts +1 -51
- package/src/server/YNABMCPServer.ts +33 -519
- package/src/server/__tests__/YNABMCPServer.test.ts +0 -1
- package/src/server/__tests__/toolRegistration.test.ts +236 -0
- package/src/server/config.ts +1 -0
- package/src/tools/__tests__/adapters.test.ts +113 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +63 -3
- package/src/tools/__tests__/utilityTools.integration.test.ts +1 -85
- package/src/tools/__tests__/utilityTools.test.ts +1 -123
- package/src/tools/accountTools.ts +53 -0
- package/src/tools/adapters.ts +74 -0
- package/src/tools/budgetTools.ts +37 -0
- package/src/tools/categoryTools.ts +53 -0
- package/src/tools/monthTools.ts +39 -0
- package/src/tools/payeeTools.ts +39 -0
- package/src/tools/reconciliation/index.ts +45 -0
- package/src/tools/schemas/common.ts +18 -0
- package/src/tools/schemas/outputs/index.ts +0 -3
- package/src/tools/schemas/outputs/utilityOutputs.ts +2 -43
- package/src/tools/toolCategories.ts +0 -1
- package/src/tools/transactionTools.ts +140 -0
- package/src/tools/utilityTools.ts +24 -55
- package/src/types/index.ts +3 -0
- package/src/types/toolRegistration.ts +88 -0
- package/vitest.config.ts +2 -1
- package/.chunkhound.json +0 -11
- package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +0 -3
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +0 -3
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +0 -1
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +0 -5
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +0 -1569
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +0 -3
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +0 -2832
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +0 -2709
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +0 -2832
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +0 -2832
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +0 -2709
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +0 -1
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +0 -5217
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +0 -2594
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +0 -2594
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +0 -231
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +0 -2590
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +0 -5195
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +0 -286
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +0 -218
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +0 -180
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +0 -3
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +0 -1
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +0 -747
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +0 -1
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +0 -2594
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +0 -2594
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +0 -144
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +0 -416
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +0 -2590
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +0 -2590
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +0 -2590
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +0 -790
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +0 -766
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +0 -790
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +0 -2594
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +0 -1000
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +0 -3489
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +0 -766
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +0 -2594
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +0 -2456
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +0 -2594
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +0 -18
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +0 -48
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +0 -1
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +0 -1
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +0 -1
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +0 -1
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +0 -1271
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +0 -1570
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +0 -2590
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +0 -3
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +0 -3299
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +0 -3299
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +0 -1882
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +0 -2594
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +0 -1
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +0 -170
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +0 -1
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +0 -3
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +0 -1
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +0 -1
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +0 -3
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +0 -1
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +0 -1
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +0 -5
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +0 -1
- package/.github/workflows/pr-description-check.yml +0 -88
- package/AGENTS.md +0 -36
- package/NUL +0 -1
- package/docs/README.md +0 -72
- package/docs/getting-started/CONFIGURATION.md +0 -175
- package/docs/getting-started/INSTALLATION.md +0 -333
- package/docs/getting-started/QUICKSTART.md +0 -282
- package/docs/guides/ARCHITECTURE.md +0 -533
- package/docs/guides/DEPLOYMENT.md +0 -189
- package/docs/guides/INTEGRATION_TESTING.md +0 -730
- package/docs/guides/TESTING.md +0 -591
- package/docs/reconciliation-flow.md +0 -83
- package/docs/reference/EXAMPLES.md +0 -946
- package/docs/reference/TOOLS.md +0 -348
- package/docs/reference/TROUBLESHOOTING.md +0 -481
- package/package.json.tmp +0 -105
- package/temp-recon.ts +0 -126
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +0 -23
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +0 -23
- package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +0 -44
- package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +0 -58
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +0 -3662
- package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +0 -115
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for tool registration verification.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that all expected tools are registered through the factory pattern
|
|
5
|
+
* and that tool metadata (annotations, schemas) are properly configured.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Mock config before importing YNABMCPServer
|
|
10
|
+
vi.mock('../config.js', () => ({
|
|
11
|
+
loadConfig: () => ({
|
|
12
|
+
YNAB_ACCESS_TOKEN: 'test-token-for-registration-tests',
|
|
13
|
+
YNAB_DEFAULT_BUDGET_ID: undefined,
|
|
14
|
+
LOG_LEVEL: 'info',
|
|
15
|
+
}),
|
|
16
|
+
config: {
|
|
17
|
+
YNAB_ACCESS_TOKEN: 'test-token-for-registration-tests',
|
|
18
|
+
YNAB_DEFAULT_BUDGET_ID: undefined,
|
|
19
|
+
LOG_LEVEL: 'info',
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { YNABMCPServer } from '../YNABMCPServer.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_BUDGET_ID = '11111111-1111-1111-1111-111111111111';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Expected tool names organized by domain.
|
|
29
|
+
* This serves as the authoritative list of all 30 registered tools.
|
|
30
|
+
*/
|
|
31
|
+
const EXPECTED_TOOLS_BY_DOMAIN = {
|
|
32
|
+
budget: ['list_budgets', 'get_budget'],
|
|
33
|
+
account: ['list_accounts', 'get_account', 'create_account'],
|
|
34
|
+
transaction: [
|
|
35
|
+
'list_transactions',
|
|
36
|
+
'export_transactions',
|
|
37
|
+
'get_transaction',
|
|
38
|
+
'create_transaction',
|
|
39
|
+
'create_transactions',
|
|
40
|
+
'update_transaction',
|
|
41
|
+
'update_transactions',
|
|
42
|
+
'delete_transaction',
|
|
43
|
+
'create_receipt_split_transaction',
|
|
44
|
+
],
|
|
45
|
+
category: ['list_categories', 'get_category', 'update_category'],
|
|
46
|
+
payee: ['list_payees', 'get_payee'],
|
|
47
|
+
month: ['get_month', 'list_months'],
|
|
48
|
+
reconciliation: ['compare_transactions', 'reconcile_account'],
|
|
49
|
+
utility: ['get_user'],
|
|
50
|
+
server: [
|
|
51
|
+
'set_default_budget',
|
|
52
|
+
'get_default_budget',
|
|
53
|
+
'clear_cache',
|
|
54
|
+
'diagnostic_info',
|
|
55
|
+
'set_output_format',
|
|
56
|
+
],
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
/** Flat list of all expected tool names */
|
|
60
|
+
const ALL_EXPECTED_TOOLS = Object.values(EXPECTED_TOOLS_BY_DOMAIN).flat();
|
|
61
|
+
|
|
62
|
+
/** Expected total tool count */
|
|
63
|
+
const EXPECTED_TOOL_COUNT = 29;
|
|
64
|
+
|
|
65
|
+
describe('Tool Registration', () => {
|
|
66
|
+
// Config is mocked at module level, no env setup needed
|
|
67
|
+
|
|
68
|
+
describe('Tool Count Verification', () => {
|
|
69
|
+
it('registers exactly 30 tools', () => {
|
|
70
|
+
const server = new YNABMCPServer(false);
|
|
71
|
+
const tools = server.getToolRegistry().listTools();
|
|
72
|
+
expect(tools).toHaveLength(EXPECTED_TOOL_COUNT);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('registers all expected tools with correct names', () => {
|
|
76
|
+
const server = new YNABMCPServer(false);
|
|
77
|
+
const tools = server.getToolRegistry().listTools();
|
|
78
|
+
const toolNames = tools.map((t) => t.name).sort();
|
|
79
|
+
const expectedNames = [...ALL_EXPECTED_TOOLS].sort();
|
|
80
|
+
|
|
81
|
+
expect(toolNames).toEqual(expectedNames);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Domain Tool Registration', () => {
|
|
86
|
+
it.each([
|
|
87
|
+
['budget', EXPECTED_TOOLS_BY_DOMAIN.budget],
|
|
88
|
+
['account', EXPECTED_TOOLS_BY_DOMAIN.account],
|
|
89
|
+
['transaction', EXPECTED_TOOLS_BY_DOMAIN.transaction],
|
|
90
|
+
['category', EXPECTED_TOOLS_BY_DOMAIN.category],
|
|
91
|
+
['payee', EXPECTED_TOOLS_BY_DOMAIN.payee],
|
|
92
|
+
['month', EXPECTED_TOOLS_BY_DOMAIN.month],
|
|
93
|
+
['reconciliation', EXPECTED_TOOLS_BY_DOMAIN.reconciliation],
|
|
94
|
+
['utility', EXPECTED_TOOLS_BY_DOMAIN.utility],
|
|
95
|
+
['server', EXPECTED_TOOLS_BY_DOMAIN.server],
|
|
96
|
+
])('registers all %s domain tools', (domain: string, expectedTools: readonly string[]) => {
|
|
97
|
+
const server = new YNABMCPServer(false);
|
|
98
|
+
const tools = server.getToolRegistry().listTools();
|
|
99
|
+
const toolNames = tools.map((t) => t.name);
|
|
100
|
+
|
|
101
|
+
for (const toolName of expectedTools) {
|
|
102
|
+
expect(toolNames, `Missing ${domain} tool: ${toolName}`).toContain(toolName);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Tool Metadata Verification', () => {
|
|
108
|
+
it('has descriptions for all tools', () => {
|
|
109
|
+
const server = new YNABMCPServer(false);
|
|
110
|
+
const tools = server.getToolRegistry().listTools();
|
|
111
|
+
|
|
112
|
+
for (const tool of tools) {
|
|
113
|
+
expect(tool.description, `${tool.name} missing description`).toBeDefined();
|
|
114
|
+
expect(tool.description.length, `${tool.name} has empty description`).toBeGreaterThan(0);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('has input schemas for all tools', () => {
|
|
119
|
+
const server = new YNABMCPServer(false);
|
|
120
|
+
const tools = server.getToolRegistry().listTools();
|
|
121
|
+
|
|
122
|
+
for (const tool of tools) {
|
|
123
|
+
expect(tool.inputSchema, `${tool.name} missing inputSchema`).toBeDefined();
|
|
124
|
+
expect(tool.inputSchema.type, `${tool.name} inputSchema not object type`).toBe('object');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('has annotations for all tools', () => {
|
|
129
|
+
const server = new YNABMCPServer(false);
|
|
130
|
+
const tools = server.getToolRegistry().listTools();
|
|
131
|
+
|
|
132
|
+
for (const tool of tools) {
|
|
133
|
+
expect(tool.annotations, `${tool.name} missing annotations`).toBeDefined();
|
|
134
|
+
expect(tool.annotations, `${tool.name} missing title`).toHaveProperty('title');
|
|
135
|
+
expect(tool.annotations, `${tool.name} missing readOnlyHint`).toHaveProperty(
|
|
136
|
+
'readOnlyHint',
|
|
137
|
+
);
|
|
138
|
+
expect(tool.annotations, `${tool.name} missing openWorldHint`).toHaveProperty(
|
|
139
|
+
'openWorldHint',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('has YNAB prefix in all tool titles', () => {
|
|
145
|
+
const server = new YNABMCPServer(false);
|
|
146
|
+
const tools = server.getToolRegistry().listTools();
|
|
147
|
+
|
|
148
|
+
for (const tool of tools) {
|
|
149
|
+
expect(tool.annotations?.title, `${tool.name} title should start with "YNAB:"`).toMatch(
|
|
150
|
+
/^YNAB:/,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('No Duplicate Tools', () => {
|
|
157
|
+
it('does not have duplicate tool names', () => {
|
|
158
|
+
const server = new YNABMCPServer(false);
|
|
159
|
+
const tools = server.getToolRegistry().listTools();
|
|
160
|
+
const toolNames = tools.map((t) => t.name);
|
|
161
|
+
const uniqueNames = new Set(toolNames);
|
|
162
|
+
|
|
163
|
+
expect(uniqueNames.size).toBe(toolNames.length);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Factory Registration Completeness', () => {
|
|
168
|
+
it('does not have unexpected tools registered', () => {
|
|
169
|
+
const server = new YNABMCPServer(false);
|
|
170
|
+
const tools = server.getToolRegistry().listTools();
|
|
171
|
+
const toolNames = tools.map((t) => t.name);
|
|
172
|
+
|
|
173
|
+
const unexpectedTools = toolNames.filter((name) => !ALL_EXPECTED_TOOLS.includes(name));
|
|
174
|
+
expect(unexpectedTools, 'Unexpected tools found').toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('does not have missing expected tools', () => {
|
|
178
|
+
const server = new YNABMCPServer(false);
|
|
179
|
+
const tools = server.getToolRegistry().listTools();
|
|
180
|
+
const toolNames = tools.map((t) => t.name);
|
|
181
|
+
|
|
182
|
+
const missingTools = ALL_EXPECTED_TOOLS.filter((name) => !toolNames.includes(name));
|
|
183
|
+
expect(missingTools, 'Missing expected tools').toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Default Argument Resolution', () => {
|
|
188
|
+
it('applies defaultArgumentResolver when budget_id is omitted', () => {
|
|
189
|
+
const server = new YNABMCPServer(false);
|
|
190
|
+
server.setDefaultBudget(DEFAULT_BUDGET_ID);
|
|
191
|
+
|
|
192
|
+
const listAccounts = server
|
|
193
|
+
.getToolRegistry()
|
|
194
|
+
.getToolDefinitions()
|
|
195
|
+
.find((tool) => tool.name === 'list_accounts');
|
|
196
|
+
|
|
197
|
+
expect(listAccounts?.defaultArgumentResolver).toBeDefined();
|
|
198
|
+
|
|
199
|
+
const resolved = listAccounts?.defaultArgumentResolver?.({
|
|
200
|
+
name: 'list_accounts',
|
|
201
|
+
accessToken: 'token',
|
|
202
|
+
rawArguments: {},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(resolved).toEqual({ budget_id: DEFAULT_BUDGET_ID });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('budget-dependent tools have defaultArgumentResolver', () => {
|
|
209
|
+
const server = new YNABMCPServer(false);
|
|
210
|
+
const definitions = server.getToolRegistry().getToolDefinitions();
|
|
211
|
+
|
|
212
|
+
// Tools that require budget_id should have resolvers
|
|
213
|
+
const budgetDependentTools = [
|
|
214
|
+
'list_accounts',
|
|
215
|
+
'get_account',
|
|
216
|
+
'create_account',
|
|
217
|
+
'list_transactions',
|
|
218
|
+
'get_transaction',
|
|
219
|
+
'list_categories',
|
|
220
|
+
'get_category',
|
|
221
|
+
'list_payees',
|
|
222
|
+
'get_payee',
|
|
223
|
+
'get_month',
|
|
224
|
+
'list_months',
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const toolName of budgetDependentTools) {
|
|
228
|
+
const tool = definitions.find((t) => t.name === toolName);
|
|
229
|
+
expect(
|
|
230
|
+
tool?.defaultArgumentResolver,
|
|
231
|
+
`${toolName} should have defaultArgumentResolver`,
|
|
232
|
+
).toBeDefined();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
package/src/server/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ValidationError } from '../utils/errors.js';
|
|
|
5
5
|
|
|
6
6
|
const envSchema = z.object({
|
|
7
7
|
YNAB_ACCESS_TOKEN: z.string().trim().min(1, 'YNAB_ACCESS_TOKEN must be a non-empty string'),
|
|
8
|
+
YNAB_DEFAULT_BUDGET_ID: z.string().uuid('YNAB_DEFAULT_BUDGET_ID must be a valid UUID').optional(),
|
|
8
9
|
MCP_PORT: z.coerce.number().int().positive().optional(),
|
|
9
10
|
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
|
|
10
11
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { DefaultArgumentResolutionError } from '../../server/toolRegistry.js';
|
|
3
|
+
import type { ToolContext } from '../../types/toolRegistration.js';
|
|
4
|
+
import { createAdapters, createBudgetResolver } from '../adapters.js';
|
|
5
|
+
|
|
6
|
+
const createMockContext = (overrides: Partial<ToolContext> = {}): ToolContext => {
|
|
7
|
+
return {
|
|
8
|
+
ynabAPI: (overrides.ynabAPI ?? ({ budgets: {} } as unknown as any)) as ToolContext['ynabAPI'],
|
|
9
|
+
deltaFetcher: overrides.deltaFetcher ?? ({} as any),
|
|
10
|
+
deltaCache: overrides.deltaCache ?? ({} as any),
|
|
11
|
+
serverKnowledgeStore: overrides.serverKnowledgeStore ?? ({} as any),
|
|
12
|
+
getDefaultBudgetId:
|
|
13
|
+
overrides.getDefaultBudgetId ?? vi.fn(() => '123e4567-e89b-12d3-a456-426614174000'),
|
|
14
|
+
setDefaultBudget: overrides.setDefaultBudget ?? vi.fn(),
|
|
15
|
+
cacheManager: overrides.cacheManager ?? ({} as any),
|
|
16
|
+
diagnosticManager: overrides.diagnosticManager,
|
|
17
|
+
} satisfies ToolContext;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('createAdapters', () => {
|
|
21
|
+
it('adapt passes api and params to handler', async () => {
|
|
22
|
+
const context = createMockContext();
|
|
23
|
+
const { adapt } = createAdapters(context);
|
|
24
|
+
const handler = vi.fn().mockResolvedValue({ content: [] });
|
|
25
|
+
|
|
26
|
+
const adapted = adapt(handler);
|
|
27
|
+
await adapted({ input: { foo: 'bar' }, context: {} as any });
|
|
28
|
+
|
|
29
|
+
expect(handler).toHaveBeenCalledWith(context.ynabAPI, { foo: 'bar' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('adaptNoInput passes api without params', async () => {
|
|
33
|
+
const context = createMockContext();
|
|
34
|
+
const { adaptNoInput } = createAdapters(context);
|
|
35
|
+
const handler = vi.fn().mockResolvedValue({ content: [] });
|
|
36
|
+
|
|
37
|
+
const adapted = adaptNoInput(handler);
|
|
38
|
+
await adapted({ input: {}, context: {} as any });
|
|
39
|
+
|
|
40
|
+
expect(handler).toHaveBeenCalledWith(context.ynabAPI);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('adaptWithDelta passes deltaFetcher to handler', async () => {
|
|
44
|
+
const deltaFetcher = { fetch: vi.fn() } as any;
|
|
45
|
+
const context = createMockContext({ deltaFetcher });
|
|
46
|
+
const { adaptWithDelta } = createAdapters(context);
|
|
47
|
+
const handler = vi.fn().mockResolvedValue({ content: [] });
|
|
48
|
+
|
|
49
|
+
const adapted = adaptWithDelta(handler);
|
|
50
|
+
await adapted({ input: { a: 1 }, context: {} as any });
|
|
51
|
+
|
|
52
|
+
expect(handler).toHaveBeenCalledWith(context.ynabAPI, deltaFetcher, { a: 1 });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('adaptWrite passes cache and knowledge store to handler', async () => {
|
|
56
|
+
const deltaCache = { invalidate: vi.fn() } as any;
|
|
57
|
+
const serverKnowledgeStore = { update: vi.fn() } as any;
|
|
58
|
+
const context = createMockContext({ deltaCache, serverKnowledgeStore });
|
|
59
|
+
const { adaptWrite } = createAdapters(context);
|
|
60
|
+
const handler = vi.fn().mockResolvedValue({ content: [] });
|
|
61
|
+
|
|
62
|
+
const adapted = adaptWrite(handler);
|
|
63
|
+
await adapted({ input: { a: 2 }, context: {} as any });
|
|
64
|
+
|
|
65
|
+
expect(handler).toHaveBeenCalledWith(context.ynabAPI, deltaCache, serverKnowledgeStore, {
|
|
66
|
+
a: 2,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('createBudgetResolver', () => {
|
|
72
|
+
it('returns provided budget_id when supplied', () => {
|
|
73
|
+
const context = createMockContext({ getDefaultBudgetId: () => 'default-budget' });
|
|
74
|
+
const resolverFactory = createBudgetResolver(context);
|
|
75
|
+
const resolver = resolverFactory<{ budget_id?: string }>();
|
|
76
|
+
|
|
77
|
+
const result = resolver({
|
|
78
|
+
name: 'tool',
|
|
79
|
+
accessToken: 'token',
|
|
80
|
+
rawArguments: { budget_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({ budget_id: '123e4567-e89b-12d3-a456-426614174000' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls back to default budget id when not provided', () => {
|
|
87
|
+
const context = createMockContext({
|
|
88
|
+
getDefaultBudgetId: () => '89abcdef-0123-4567-89ab-cdef01234567',
|
|
89
|
+
});
|
|
90
|
+
const resolver = createBudgetResolver(context)<{ budget_id?: string }>();
|
|
91
|
+
|
|
92
|
+
const result = resolver({
|
|
93
|
+
name: 'tool',
|
|
94
|
+
accessToken: 'token',
|
|
95
|
+
rawArguments: {},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result).toEqual({ budget_id: '89abcdef-0123-4567-89ab-cdef01234567' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('throws DefaultArgumentResolutionError when no budget id available', () => {
|
|
102
|
+
const context = createMockContext({ getDefaultBudgetId: () => undefined });
|
|
103
|
+
const resolver = createBudgetResolver(context)<{ budget_id?: string }>();
|
|
104
|
+
|
|
105
|
+
expect(() =>
|
|
106
|
+
resolver({
|
|
107
|
+
name: 'tool',
|
|
108
|
+
accessToken: 'token',
|
|
109
|
+
rawArguments: {},
|
|
110
|
+
}),
|
|
111
|
+
).toThrow(DefaultArgumentResolutionError);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
handleUpdateTransactions,
|
|
10
10
|
CreateTransactionsSchema,
|
|
11
11
|
} from '../transactionTools.js';
|
|
12
|
+
import { waitFor } from '../../__tests__/testUtils.js';
|
|
12
13
|
|
|
13
14
|
const isSkip = ['true', '1', 'yes', 'y', 'on'].includes(
|
|
14
15
|
(process.env['SKIP_E2E_TESTS'] || '').toLowerCase().trim(),
|
|
@@ -325,9 +326,22 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
325
326
|
],
|
|
326
327
|
});
|
|
327
328
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
let transactions: any[] | undefined;
|
|
330
|
+
await waitFor(
|
|
331
|
+
async () => {
|
|
332
|
+
const afterList = await fetchBudgetTransactions();
|
|
333
|
+
transactions =
|
|
334
|
+
afterList.transactions ||
|
|
335
|
+
afterList.preview_transactions ||
|
|
336
|
+
afterList.transaction_preview;
|
|
337
|
+
return (
|
|
338
|
+
(transactions as any[])?.some((transaction) => transaction.memo === memo) ?? false
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
10000,
|
|
342
|
+
500,
|
|
343
|
+
);
|
|
344
|
+
|
|
331
345
|
expect(transactions).toBeDefined();
|
|
332
346
|
expect((transactions as any[]).some((transaction) => transaction.memo === memo)).toBe(true);
|
|
333
347
|
},
|
|
@@ -576,6 +590,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
576
590
|
expect(updateResponse.results[1].correlation_key).toBe(transactionIds[1]);
|
|
577
591
|
|
|
578
592
|
// Verify changes persisted
|
|
593
|
+
await waitFor(
|
|
594
|
+
async () => {
|
|
595
|
+
const getResult1 = await handleGetTransaction(ynabAPI, {
|
|
596
|
+
budget_id: testBudgetId,
|
|
597
|
+
transaction_id: transactionIds[0],
|
|
598
|
+
});
|
|
599
|
+
const transaction1 = parseToolResult(getResult1).transaction;
|
|
600
|
+
return transaction1.amount === -7.5 && transaction1.memo === 'Updated memo 1';
|
|
601
|
+
},
|
|
602
|
+
10000,
|
|
603
|
+
500,
|
|
604
|
+
);
|
|
605
|
+
|
|
579
606
|
const getResult1 = await handleGetTransaction(ynabAPI, {
|
|
580
607
|
budget_id: testBudgetId,
|
|
581
608
|
transaction_id: transactionIds[0],
|
|
@@ -584,6 +611,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
584
611
|
expect(transaction1.amount).toBe(-7.5);
|
|
585
612
|
expect(transaction1.memo).toBe('Updated memo 1');
|
|
586
613
|
|
|
614
|
+
await waitFor(
|
|
615
|
+
async () => {
|
|
616
|
+
const getResult2 = await handleGetTransaction(ynabAPI, {
|
|
617
|
+
budget_id: testBudgetId,
|
|
618
|
+
transaction_id: transactionIds[1],
|
|
619
|
+
});
|
|
620
|
+
const transaction2 = parseToolResult(getResult2).transaction;
|
|
621
|
+
return transaction2.memo === 'Updated memo 2' && transaction2.cleared === 'cleared';
|
|
622
|
+
},
|
|
623
|
+
10000,
|
|
624
|
+
500,
|
|
625
|
+
);
|
|
626
|
+
|
|
587
627
|
const getResult2 = await handleGetTransaction(ynabAPI, {
|
|
588
628
|
budget_id: testBudgetId,
|
|
589
629
|
transaction_id: transactionIds[1],
|
|
@@ -635,6 +675,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
635
675
|
expect(updateResponse.summary.updated).toBe(1);
|
|
636
676
|
|
|
637
677
|
// Verify change
|
|
678
|
+
await waitFor(
|
|
679
|
+
async () => {
|
|
680
|
+
const getResult = await handleGetTransaction(ynabAPI, {
|
|
681
|
+
budget_id: testBudgetId,
|
|
682
|
+
transaction_id: transactionId,
|
|
683
|
+
});
|
|
684
|
+
const transaction = parseToolResult(getResult).transaction;
|
|
685
|
+
return transaction.memo === 'Updated without metadata';
|
|
686
|
+
},
|
|
687
|
+
10000,
|
|
688
|
+
500,
|
|
689
|
+
);
|
|
690
|
+
|
|
638
691
|
const getResult = await handleGetTransaction(ynabAPI, {
|
|
639
692
|
budget_id: testBudgetId,
|
|
640
693
|
transaction_id: transactionId,
|
|
@@ -749,6 +802,13 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
749
802
|
});
|
|
750
803
|
|
|
751
804
|
const updateResponse = parseToolResult(updateResult);
|
|
805
|
+
|
|
806
|
+
if (updateResponse.error) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Tool execution failed unexpectedly: ${JSON.stringify(updateResponse.error)}`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
752
812
|
expect(updateResponse.summary.total_requested).toBe(2);
|
|
753
813
|
expect(updateResponse.summary.updated).toBe(1);
|
|
754
814
|
expect(updateResponse.summary.failed).toBeGreaterThan(0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { handleGetUser
|
|
3
|
+
import { handleGetUser } from '../utilityTools.js';
|
|
4
4
|
import { skipOnRateLimit } from '../../__tests__/testUtils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -41,88 +41,4 @@ describeIntegration('Utility Tools Integration Tests', () => {
|
|
|
41
41
|
},
|
|
42
42
|
);
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
describe('handleConvertAmount', () => {
|
|
46
|
-
it(
|
|
47
|
-
'should convert various dollar amounts to milliunits',
|
|
48
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
49
|
-
async () => {
|
|
50
|
-
const testCases = [
|
|
51
|
-
{ dollars: 1.0, expectedMilliunits: 1000 },
|
|
52
|
-
{ dollars: 0.01, expectedMilliunits: 10 },
|
|
53
|
-
{ dollars: 10.5, expectedMilliunits: 10500 },
|
|
54
|
-
{ dollars: 999.99, expectedMilliunits: 999990 },
|
|
55
|
-
{ dollars: 0, expectedMilliunits: 0 },
|
|
56
|
-
{ dollars: -5.25, expectedMilliunits: -5250 },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
for (const testCase of testCases) {
|
|
60
|
-
const result = await handleConvertAmount({
|
|
61
|
-
amount: testCase.dollars,
|
|
62
|
-
to_milliunits: true,
|
|
63
|
-
});
|
|
64
|
-
const response = JSON.parse(result.content[0].text);
|
|
65
|
-
|
|
66
|
-
expect(response.conversion.converted_amount).toBe(testCase.expectedMilliunits);
|
|
67
|
-
expect(response.conversion.to_milliunits).toBe(true);
|
|
68
|
-
expect(response.conversion.description).toContain(`$${testCase.dollars.toFixed(2)}`);
|
|
69
|
-
expect(response.conversion.description).toContain(
|
|
70
|
-
`${testCase.expectedMilliunits} milliunits`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
it(
|
|
77
|
-
'should convert various milliunit amounts to dollars',
|
|
78
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
79
|
-
async () => {
|
|
80
|
-
const testCases = [
|
|
81
|
-
{ milliunits: 1000, expectedDollars: 1.0 },
|
|
82
|
-
{ milliunits: 10, expectedDollars: 0.01 },
|
|
83
|
-
{ milliunits: 10500, expectedDollars: 10.5 },
|
|
84
|
-
{ milliunits: 999990, expectedDollars: 999.99 },
|
|
85
|
-
{ milliunits: 0, expectedDollars: 0 },
|
|
86
|
-
{ milliunits: -5250, expectedDollars: -5.25 },
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
for (const testCase of testCases) {
|
|
90
|
-
const result = await handleConvertAmount({
|
|
91
|
-
amount: testCase.milliunits,
|
|
92
|
-
to_milliunits: false,
|
|
93
|
-
});
|
|
94
|
-
const response = JSON.parse(result.content[0].text);
|
|
95
|
-
|
|
96
|
-
expect(response.conversion.converted_amount).toBe(testCase.expectedDollars);
|
|
97
|
-
expect(response.conversion.to_milliunits).toBe(false);
|
|
98
|
-
expect(response.conversion.description).toContain(`${testCase.milliunits} milliunits`);
|
|
99
|
-
expect(response.conversion.description).toContain(
|
|
100
|
-
`$${testCase.expectedDollars.toFixed(2)}`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
it(
|
|
107
|
-
'should handle precision edge cases',
|
|
108
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
109
|
-
async () => {
|
|
110
|
-
// Test floating-point precision issues
|
|
111
|
-
const precisionTests = [
|
|
112
|
-
{ amount: 0.1 + 0.2, to_milliunits: true }, // Should handle 0.30000000000000004
|
|
113
|
-
{ amount: 1.005, to_milliunits: true }, // Should round correctly
|
|
114
|
-
{ amount: 999.999, to_milliunits: true }, // Should handle near-integer values
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
for (const test of precisionTests) {
|
|
118
|
-
const result = await handleConvertAmount(test);
|
|
119
|
-
const response = JSON.parse(result.content[0].text);
|
|
120
|
-
|
|
121
|
-
expect(response.conversion).toHaveProperty('converted_amount');
|
|
122
|
-
expect(typeof response.conversion.converted_amount).toBe('number');
|
|
123
|
-
expect(Number.isInteger(response.conversion.converted_amount)).toBe(true);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
44
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { handleGetUser
|
|
3
|
+
import { handleGetUser } from '../utilityTools.js';
|
|
4
4
|
|
|
5
5
|
// Mock the YNAB API
|
|
6
6
|
const mockYnabAPI = {
|
|
@@ -80,126 +80,4 @@ describe('Utility Tools', () => {
|
|
|
80
80
|
expect(result.content[0].text).toContain('Failed to get user information');
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
-
|
|
84
|
-
describe('handleConvertAmount', () => {
|
|
85
|
-
it('should convert dollars to milliunits correctly', async () => {
|
|
86
|
-
const params = { amount: 10.5, to_milliunits: true };
|
|
87
|
-
|
|
88
|
-
const result = await handleConvertAmount(params);
|
|
89
|
-
const response = JSON.parse(result.content[0].text);
|
|
90
|
-
|
|
91
|
-
expect(response.conversion.original_amount).toBe(10.5);
|
|
92
|
-
expect(response.conversion.converted_amount).toBe(10500);
|
|
93
|
-
expect(response.conversion.to_milliunits).toBe(true);
|
|
94
|
-
expect(response.conversion.description).toBe('$10.50 = 10500 milliunits');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should convert milliunits to dollars correctly', async () => {
|
|
98
|
-
const params = { amount: 10500, to_milliunits: false };
|
|
99
|
-
|
|
100
|
-
const result = await handleConvertAmount(params);
|
|
101
|
-
const response = JSON.parse(result.content[0].text);
|
|
102
|
-
|
|
103
|
-
expect(response.conversion.original_amount).toBe(10500);
|
|
104
|
-
expect(response.conversion.converted_amount).toBe(10.5);
|
|
105
|
-
expect(response.conversion.to_milliunits).toBe(false);
|
|
106
|
-
expect(response.conversion.description).toBe('10500 milliunits = $10.50');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should handle zero amounts', async () => {
|
|
110
|
-
const params = { amount: 0, to_milliunits: true };
|
|
111
|
-
|
|
112
|
-
const result = await handleConvertAmount(params);
|
|
113
|
-
const response = JSON.parse(result.content[0].text);
|
|
114
|
-
|
|
115
|
-
expect(response.conversion.original_amount).toBe(0);
|
|
116
|
-
expect(response.conversion.converted_amount).toBe(0);
|
|
117
|
-
expect(response.conversion.description).toBe('$0.00 = 0 milliunits');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should handle negative amounts', async () => {
|
|
121
|
-
const params = { amount: -5.25, to_milliunits: true };
|
|
122
|
-
|
|
123
|
-
const result = await handleConvertAmount(params);
|
|
124
|
-
const response = JSON.parse(result.content[0].text);
|
|
125
|
-
|
|
126
|
-
expect(response.conversion.original_amount).toBe(-5.25);
|
|
127
|
-
expect(response.conversion.converted_amount).toBe(-5250);
|
|
128
|
-
expect(response.conversion.description).toBe('$-5.25 = -5250 milliunits');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should handle floating-point precision correctly', async () => {
|
|
132
|
-
const params = { amount: 0.01, to_milliunits: true };
|
|
133
|
-
|
|
134
|
-
const result = await handleConvertAmount(params);
|
|
135
|
-
const response = JSON.parse(result.content[0].text);
|
|
136
|
-
|
|
137
|
-
expect(response.conversion.converted_amount).toBe(10);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should handle large amounts', async () => {
|
|
141
|
-
const params = { amount: 999999.99, to_milliunits: true };
|
|
142
|
-
|
|
143
|
-
const result = await handleConvertAmount(params);
|
|
144
|
-
const response = JSON.parse(result.content[0].text);
|
|
145
|
-
|
|
146
|
-
expect(response.conversion.converted_amount).toBe(999999990);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should round to nearest milliunit when converting from dollars', async () => {
|
|
150
|
-
const params = { amount: 10.5555, to_milliunits: true };
|
|
151
|
-
|
|
152
|
-
const result = await handleConvertAmount(params);
|
|
153
|
-
const response = JSON.parse(result.content[0].text);
|
|
154
|
-
|
|
155
|
-
expect(response.conversion.converted_amount).toBe(10556); // Rounded from 10555.5
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('ConvertAmountSchema validation', () => {
|
|
160
|
-
it('should validate correct parameters', () => {
|
|
161
|
-
const validParams = { amount: 10.5, to_milliunits: true };
|
|
162
|
-
const result = ConvertAmountSchema.safeParse(validParams);
|
|
163
|
-
|
|
164
|
-
expect(result.success).toBe(true);
|
|
165
|
-
if (result.success) {
|
|
166
|
-
expect(result.data).toEqual(validParams);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should reject non-finite numbers', () => {
|
|
171
|
-
const invalidParams = { amount: Infinity, to_milliunits: true };
|
|
172
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
173
|
-
|
|
174
|
-
expect(result.success).toBe(false);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should reject NaN values', () => {
|
|
178
|
-
const invalidParams = { amount: NaN, to_milliunits: true };
|
|
179
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
180
|
-
|
|
181
|
-
expect(result.success).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should reject missing amount parameter', () => {
|
|
185
|
-
const invalidParams = { to_milliunits: true };
|
|
186
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
187
|
-
|
|
188
|
-
expect(result.success).toBe(false);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should reject missing to_milliunits parameter', () => {
|
|
192
|
-
const invalidParams = { amount: 10.5 };
|
|
193
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
194
|
-
|
|
195
|
-
expect(result.success).toBe(false);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should reject non-boolean to_milliunits parameter', () => {
|
|
199
|
-
const invalidParams = { amount: 10.5, to_milliunits: 'true' };
|
|
200
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
201
|
-
|
|
202
|
-
expect(result.success).toBe(false);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
83
|
});
|