Skip to content

Commit 1f83c07

Browse files
committed
feat: test cost confirmation
1 parent 96dd014 commit 1f83c07

File tree

3 files changed

+251
-10
lines changed

3 files changed

+251
-10
lines changed

packages/mcp-server-supabase/src/pricing.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
type ManagementApiClient,
44
} from './management-api/index.js';
55

6-
const PROJECT_COST_MONTHLY = 10;
7-
const BRANCH_COST_HOURLY = 0.01344;
6+
export const PROJECT_COST_MONTHLY = 10;
7+
export const BRANCH_COST_HOURLY = 0.01344;
88

99
export type ProjectCost = {
1010
type: 'project';

packages/mcp-server-supabase/src/server.test.ts

Lines changed: 233 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
mockOrgs,
1919
mockProjects,
2020
} from '../test/mocks.js';
21+
import { BRANCH_COST_HOURLY, PROJECT_COST_MONTHLY } from './pricing.js';
2122
import { createSupabaseMcpServer } from './server.js';
2223

2324
beforeEach(() => {
@@ -117,7 +118,10 @@ describe('tools', () => {
117118
arguments: {},
118119
});
119120

120-
expect(result).toEqual(mockOrgs);
121+
expect(result).toEqual([
122+
{ id: 'org-1', name: 'Org 1' },
123+
{ id: 'org-2', name: 'Org 2' },
124+
]);
121125
});
122126

123127
test('get organization', async () => {
@@ -135,6 +139,108 @@ describe('tools', () => {
135139
expect(result).toEqual(firstOrg);
136140
});
137141

142+
test('get next project cost for free org', async () => {
143+
const { callTool } = await setup();
144+
145+
const freeOrg = mockOrgs.find((org) => org.plan === 'free')!;
146+
const result = await callTool({
147+
name: 'get_cost',
148+
arguments: {
149+
type: 'project',
150+
organization_id: freeOrg.id,
151+
},
152+
});
153+
154+
expect(result).toEqual(
155+
'The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.'
156+
);
157+
});
158+
159+
test('get next project cost for paid org with 0 projects', async () => {
160+
const { callTool } = await setup();
161+
162+
const paidOrg = mockOrgs.find((org) => org.plan !== 'free')!;
163+
const result = await callTool({
164+
name: 'get_cost',
165+
arguments: {
166+
type: 'project',
167+
organization_id: paidOrg.id,
168+
},
169+
});
170+
171+
expect(result).toEqual(
172+
'The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.'
173+
);
174+
});
175+
176+
test('get next project cost for paid org with > 0 ACTIVE_HEALTHY projects', async () => {
177+
const { callTool } = await setup();
178+
179+
const paidOrg = mockOrgs.find((org) => org.plan !== 'free')!;
180+
181+
const priorProject = await createProject({
182+
name: 'Project 1',
183+
region: 'us-east-1',
184+
organization_id: paidOrg.id,
185+
});
186+
priorProject.status = 'ACTIVE_HEALTHY';
187+
188+
const result = await callTool({
189+
name: 'get_cost',
190+
arguments: {
191+
type: 'project',
192+
organization_id: paidOrg.id,
193+
},
194+
});
195+
196+
expect(result).toEqual(
197+
`The new project will cost $${PROJECT_COST_MONTHLY} monthly. You must repeat this to the user and confirm their understanding.`
198+
);
199+
});
200+
201+
test('get next project cost for paid org with > 0 projects that are not ACTIVE_HEALTHY', async () => {
202+
const { callTool } = await setup();
203+
204+
const paidOrg = mockOrgs.find((org) => org.plan !== 'free')!;
205+
206+
const priorProject = await createProject({
207+
name: 'Project 1',
208+
region: 'us-east-1',
209+
organization_id: paidOrg.id,
210+
});
211+
priorProject.status = 'INACTIVE';
212+
213+
const result = await callTool({
214+
name: 'get_cost',
215+
arguments: {
216+
type: 'project',
217+
organization_id: paidOrg.id,
218+
},
219+
});
220+
221+
expect(result).toEqual(
222+
`The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.`
223+
);
224+
});
225+
226+
test('get branch cost', async () => {
227+
const { callTool } = await setup();
228+
229+
const paidOrg = mockOrgs.find((org) => org.plan !== 'free')!;
230+
231+
const result = await callTool({
232+
name: 'get_cost',
233+
arguments: {
234+
type: 'branch',
235+
organization_id: paidOrg.id,
236+
},
237+
});
238+
239+
expect(result).toEqual(
240+
`The new branch will cost $${BRANCH_COST_HOURLY} hourly. You must repeat this to the user and confirm their understanding.`
241+
);
242+
});
243+
138244
test('list projects', async () => {
139245
const { callTool } = await setup();
140246

@@ -164,19 +270,31 @@ describe('tools', () => {
164270
test('create project', async () => {
165271
const { callTool } = await setup();
166272

273+
const freeOrg = mockOrgs.find((org) => org.plan === 'free')!;
274+
275+
const confirm_cost_id = await callTool({
276+
name: 'confirm_cost',
277+
arguments: {
278+
type: 'project',
279+
recurrence: 'monthly',
280+
amount: 0,
281+
},
282+
});
283+
167284
const newProject = {
168285
name: 'New Project',
169286
region: 'us-east-1',
170-
organization_id: mockOrgs[0]!.id,
287+
organization_id: freeOrg.id,
171288
db_pass: 'dummy-password',
289+
confirm_cost_id,
172290
};
173291

174292
const result = await callTool({
175293
name: 'create_project',
176294
arguments: newProject,
177295
});
178296

179-
const { db_pass, ...projectInfo } = newProject;
297+
const { db_pass, confirm_cost_id: _, ...projectInfo } = newProject;
180298

181299
expect(result).toEqual({
182300
...projectInfo,
@@ -191,18 +309,28 @@ describe('tools', () => {
191309
test('create project chooses closest region when undefined', async () => {
192310
const { callTool } = await setup();
193311

312+
const confirm_cost_id = await callTool({
313+
name: 'confirm_cost',
314+
arguments: {
315+
type: 'project',
316+
recurrence: 'monthly',
317+
amount: 0,
318+
},
319+
});
320+
194321
const newProject = {
195322
name: 'New Project',
196323
organization_id: mockOrgs[0]!.id,
197324
db_pass: 'dummy-password',
325+
confirm_cost_id,
198326
};
199327

200328
const result = await callTool({
201329
name: 'create_project',
202330
arguments: newProject,
203331
});
204332

205-
const { db_pass, ...projectInfo } = newProject;
333+
const { db_pass, confirm_cost_id: _, ...projectInfo } = newProject;
206334

207335
expect(result).toEqual({
208336
...projectInfo,
@@ -215,6 +343,28 @@ describe('tools', () => {
215343
});
216344
});
217345

346+
test('create project without cost confirmation fails', async () => {
347+
const { callTool } = await setup();
348+
349+
const org = mockOrgs[0]!;
350+
351+
const newProject = {
352+
name: 'New Project',
353+
region: 'us-east-1',
354+
organization_id: org.id,
355+
db_pass: 'dummy-password',
356+
};
357+
358+
const createProjectPromise = callTool({
359+
name: 'create_project',
360+
arguments: newProject,
361+
});
362+
363+
await expect(createProjectPromise).rejects.toThrow(
364+
'User must confirm understanding of costs before creating a project.'
365+
);
366+
});
367+
218368
test('pause project', async () => {
219369
const { callTool } = await setup();
220370
const project = mockProjects.values().next().value!;
@@ -497,12 +647,22 @@ describe('tools', () => {
497647
const { callTool } = await setup();
498648
const project = mockProjects.values().next().value!;
499649

650+
const confirm_cost_id = await callTool({
651+
name: 'confirm_cost',
652+
arguments: {
653+
type: 'branch',
654+
recurrence: 'hourly',
655+
amount: BRANCH_COST_HOURLY,
656+
},
657+
});
658+
500659
const branchName = 'test-branch';
501660
const result = await callTool({
502661
name: 'create_branch',
503662
arguments: {
504663
project_id: project.id,
505664
name: branchName,
665+
confirm_cost_id,
506666
},
507667
});
508668

@@ -523,15 +683,44 @@ describe('tools', () => {
523683
});
524684
});
525685

686+
test('create branch without cost confirmation fails', async () => {
687+
const { callTool } = await setup();
688+
689+
const project = mockProjects.values().next().value!;
690+
691+
const branchName = 'test-branch';
692+
const createBranchPromise = callTool({
693+
name: 'create_branch',
694+
arguments: {
695+
project_id: project.id,
696+
name: branchName,
697+
},
698+
});
699+
700+
await expect(createBranchPromise).rejects.toThrow(
701+
'User must confirm understanding of costs before creating a branch.'
702+
);
703+
});
704+
526705
test('delete branch', async () => {
527706
const { callTool } = await setup();
528707
const project = mockProjects.values().next().value!;
529708

709+
const confirm_cost_id = await callTool({
710+
name: 'confirm_cost',
711+
arguments: {
712+
type: 'branch',
713+
recurrence: 'hourly',
714+
amount: BRANCH_COST_HOURLY,
715+
},
716+
});
717+
530718
const branch = await callTool({
531719
name: 'create_branch',
532720
arguments: {
533721
project_id: project.id,
534722
name: 'test-branch',
723+
confirm_cost_id,
535724
},
536725
});
537726

@@ -598,11 +787,21 @@ describe('tools', () => {
598787
const { callTool } = await setup();
599788
const project = mockProjects.values().next().value!;
600789

790+
const confirm_cost_id = await callTool({
791+
name: 'confirm_cost',
792+
arguments: {
793+
type: 'branch',
794+
recurrence: 'hourly',
795+
amount: BRANCH_COST_HOURLY,
796+
},
797+
});
798+
601799
const branch = await callTool({
602800
name: 'create_branch',
603801
arguments: {
604802
project_id: project.id,
605803
name: 'test-branch',
804+
confirm_cost_id,
606805
},
607806
});
608807

@@ -647,11 +846,21 @@ describe('tools', () => {
647846
const { callTool } = await setup();
648847
const project = mockProjects.values().next().value!;
649848

849+
const confirm_cost_id = await callTool({
850+
name: 'confirm_cost',
851+
arguments: {
852+
type: 'branch',
853+
recurrence: 'hourly',
854+
amount: BRANCH_COST_HOURLY,
855+
},
856+
});
857+
650858
const branch = await callTool({
651859
name: 'create_branch',
652860
arguments: {
653861
project_id: project.id,
654862
name: 'test-branch',
863+
confirm_cost_id,
655864
},
656865
});
657866

@@ -701,11 +910,21 @@ describe('tools', () => {
701910
const { callTool } = await setup();
702911
const project = mockProjects.values().next().value!;
703912

913+
const confirm_cost_id = await callTool({
914+
name: 'confirm_cost',
915+
arguments: {
916+
type: 'branch',
917+
recurrence: 'hourly',
918+
amount: BRANCH_COST_HOURLY,
919+
},
920+
});
921+
704922
const branch = await callTool({
705923
name: 'create_branch',
706924
arguments: {
707925
project_id: project.id,
708926
name: 'test-branch',
927+
confirm_cost_id,
709928
},
710929
});
711930

@@ -779,11 +998,21 @@ describe('tools', () => {
779998
const { callTool } = await setup();
780999
const project = mockProjects.values().next().value!;
7811000

1001+
const confirm_cost_id = await callTool({
1002+
name: 'confirm_cost',
1003+
arguments: {
1004+
type: 'branch',
1005+
recurrence: 'hourly',
1006+
amount: BRANCH_COST_HOURLY,
1007+
},
1008+
});
1009+
7821010
const branch = await callTool({
7831011
name: 'create_branch',
7841012
arguments: {
7851013
project_id: project.id,
7861014
name: 'test-branch',
1015+
confirm_cost_id,
7871016
},
7881017
});
7891018

0 commit comments

Comments
 (0)