@@ -188,6 +188,40 @@ async def test_update_weight_persists(self, nc_mcp: McpTestHelper) -> None:
188188 )
189189 assert updated ["weight" ] == 3.5
190190
191+ @pytest .mark .asyncio
192+ async def test_update_activated_false_then_true_round_trip (self , nc_mcp : McpTestHelper ) -> None :
193+ """Soft-disable via update path, then re-enable. Catches the create/update field-name asymmetry
194+ (create uses `active` int, update uses `activated` bool)."""
195+ pid = "mcp-test-mem-toggle"
196+ await _make_project (nc_mcp , pid )
197+ member = await _make_member (nc_mcp , pid , "Toggle" )
198+ bob = await _make_member (nc_mcp , pid , "Bob" )
199+ # Member must own a bill so update(activated=False) soft-disables instead of deleting
200+ await _make_bill (nc_mcp , pid , "Lunch" , 10.0 , payer = member ["id" ], payed_for = [member ["id" ], bob ["id" ]])
201+
202+ disabled = json .loads (
203+ await nc_mcp .call (
204+ "update_cospend_member" ,
205+ project_id = pid ,
206+ member_id = member ["id" ],
207+ activated = False ,
208+ )
209+ )
210+ assert disabled is not None , "member with bills should be returned as soft-disabled, not deleted"
211+ assert disabled ["activated" ] is False
212+ members = json .loads (await nc_mcp .call ("list_cospend_members" , project_id = pid ))
213+ assert next (m for m in members if m ["id" ] == member ["id" ])["activated" ] is False
214+
215+ re_enabled = json .loads (
216+ await nc_mcp .call (
217+ "update_cospend_member" ,
218+ project_id = pid ,
219+ member_id = member ["id" ],
220+ activated = True ,
221+ )
222+ )
223+ assert re_enabled ["activated" ] is True
224+
191225 @pytest .mark .asyncio
192226 async def test_delete_removes_member_with_no_bills (self , nc_mcp : McpTestHelper ) -> None :
193227 await _make_project (nc_mcp , "mcp-test-mem-del" )
@@ -465,6 +499,34 @@ async def test_get_bill_with_bad_id_raises(self, nc_mcp: McpTestHelper) -> None:
465499 with pytest .raises ((ToolError , NextcloudError )):
466500 await nc_mcp .call ("get_cospend_bill" , project_id = "mcp-test-bad-bill" , bill_id = 999_999_999 )
467501
502+ @pytest .mark .asyncio
503+ async def test_create_bill_with_empty_payed_for_rejected (self , nc_mcp : McpTestHelper ) -> None :
504+ """payed_for=[] is rejected client-side — server would 400, we fail fast with a clearer message."""
505+ pid = "mcp-test-bill-empty-create"
506+ await _make_project (nc_mcp , pid )
507+ alice = await _make_member (nc_mcp , pid , "Alice" )
508+ with pytest .raises (ToolError , match = "non-empty" ):
509+ await nc_mcp .call (
510+ "create_cospend_bill" ,
511+ project_id = pid ,
512+ what = "X" ,
513+ amount = 1.0 ,
514+ payer = alice ["id" ],
515+ payed_for = [],
516+ )
517+
518+ @pytest .mark .asyncio
519+ async def test_update_bill_with_empty_payed_for_rejected (self , nc_mcp : McpTestHelper ) -> None :
520+ """payed_for=[] on update is rejected — server would silently no-op (200 OK with owers unchanged),
521+ which would look like a successful update. We reject up front instead."""
522+ pid = "mcp-test-bill-empty-update"
523+ await _make_project (nc_mcp , pid )
524+ alice = await _make_member (nc_mcp , pid , "Alice" )
525+ bob = await _make_member (nc_mcp , pid , "Bob" )
526+ bill_id = await _make_bill (nc_mcp , pid , "X" , 1.0 , alice ["id" ], [alice ["id" ], bob ["id" ]])
527+ with pytest .raises (ToolError , match = "non-empty" ):
528+ await nc_mcp .call ("update_cospend_bill" , project_id = pid , bill_id = bill_id , payed_for = [])
529+
468530
469531class TestToolRegistration :
470532 """Confirm all expected tools are registered (catches accidental drops)."""
0 commit comments