diff --git a/float/config.json b/float/config.json index 891db378..65bd2ec6 100644 --- a/float/config.json +++ b/float/config.json @@ -1,6 +1,6 @@ { "name": "Float", - "version": "2.0.0", + "version": "2.0.1", "description": "Comprehensive Float API integration for resource management, project scheduling, time tracking, and team management. Supports people, projects, tasks/allocations, time off, logged time, clients, departments, and roles.", "entry_point": "float.py", "supports_billing": false, diff --git a/float/float.py b/float/float.py index c91f2873..6df3657e 100644 --- a/float/float.py +++ b/float/float.py @@ -1244,7 +1244,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac ActionResult containing created time off details """ request_body = { - "people_id": inputs["people_id"], + "people_ids": [inputs["people_id"]], "timeoff_type_id": inputs["timeoff_type_id"], "start_date": inputs["start_date"], "end_date": inputs["end_date"], @@ -1522,7 +1522,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac json=request_body, ) - return ActionResult(data=response.data, cost_usd=0.0) + data = response.data[0] if isinstance(response.data, list) else response.data + return ActionResult(data=data, cost_usd=0.0) except Exception as e: return ActionError(message=f"Failed to create logged time: {str(e)}") @@ -1586,7 +1587,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac json=request_body, ) - return ActionResult(data=response.data, cost_usd=0.0) + data = response.data[0] if isinstance(response.data, list) else response.data + return ActionResult(data=data, cost_usd=0.0) except Exception as e: return ActionError(message=f"Failed to update logged time {logged_time_id}: {str(e)}") diff --git a/float/tests/test_float_integration.py b/float/tests/test_float_integration.py index 12a8da58..d66ae01c 100644 --- a/float/tests/test_float_integration.py +++ b/float/tests/test_float_integration.py @@ -4,8 +4,11 @@ These tests call the real Float API and require a valid API key set in the FLOAT_API_KEY environment variable (via .env or export). -Run with: - pytest float/tests/test_float_integration.py -m integration +Run (safe, read-only): + pytest float/tests/test_float_integration.py -m "integration and not destructive" + +Run (destructive — creates/updates/deletes real data): + pytest float/tests/test_float_integration.py -m "integration and destructive" Never runs in CI — the default pytest marker filter (-m unit) excludes these, and the file naming (test_*_integration.py) is not matched by python_files. @@ -595,6 +598,98 @@ async def test_full_lifecycle(self, live_context): assert delete_result.result.data is not None +@pytest.mark.destructive +class TestCreateTimeOff: + """Verifies create_time_off sends people_ids as an array (not people_id integer).""" + + async def test_creates_and_deletes_time_off(self, live_context): + people_result = await float_integration.execute_action("list_people", {"per_page": 1}, live_context) + people = people_result.result.data + if not people: + pytest.skip("No people in account to test with") + person_id = people[0]["people_id"] + + types_result = await float_integration.execute_action("list_time_off_types", {}, live_context) + timeoff_types = types_result.result.data + if not timeoff_types: + pytest.skip("No time off types in account to test with") + timeoff_type_id = timeoff_types[0]["timeoff_type_id"] + + result = await float_integration.execute_action( + "create_time_off", + { + "people_id": person_id, + "timeoff_type_id": timeoff_type_id, + "start_date": "2026-07-01", + "end_date": "2026-07-01", + "hours": 8, + }, + live_context, + ) + assert result.result_type == "ActionResult" + data = result.result.data + assert "timeoff_id" in data + assert person_id in data.get("people_ids", []) + + # Cleanup + await float_integration.execute_action("delete_time_off", {"timeoff_id": data["timeoff_id"]}, live_context) + + +@pytest.mark.destructive +class TestLoggedTimeLifecycle: + """Verifies create and update return an object (not a raw array) after unwrapping.""" + + async def test_create_update_get_delete(self, live_context): + people_result = await float_integration.execute_action("list_people", {"per_page": 1}, live_context) + people = people_result.result.data + if not people: + pytest.skip("No people in account to test with") + person_id = people[0]["people_id"] + + projects_result = await float_integration.execute_action("list_projects", {"per_page": 1}, live_context) + projects = projects_result.result.data + if not projects: + pytest.skip("No projects in account to test with") + project_id = projects[0]["project_id"] + + # Create — must return a dict, not an array + create_result = await float_integration.execute_action( + "create_logged_time", + {"people_id": person_id, "project_id": project_id, "date": "2026-07-01", "hours": 3}, + live_context, + ) + assert create_result.result_type == "ActionResult" + created = create_result.result.data + assert isinstance(created, dict), "Expected dict — array unwrap fix missing in create_logged_time" + assert "logged_time_id" in created + logged_time_id = created["logged_time_id"] + + # Update — must also return a dict, not an array + update_result = await float_integration.execute_action( + "update_logged_time", + {"logged_time_id": logged_time_id, "hours": 5}, + live_context, + ) + assert update_result.result_type == "ActionResult" + updated = update_result.result.data + assert isinstance(updated, dict), "Expected dict — array unwrap fix missing in update_logged_time" + assert updated["hours"] == 5 + + # Get — returns object directly (no fix needed, verify it still works) + get_result = await float_integration.execute_action( + "get_logged_time", {"logged_time_id": logged_time_id}, live_context + ) + assert get_result.result_type == "ActionResult" + assert get_result.result.data["logged_time_id"] == logged_time_id + + # Delete (cleanup) + delete_result = await float_integration.execute_action( + "delete_logged_time", {"logged_time_id": logged_time_id}, live_context + ) + assert delete_result.result_type == "ActionResult" + assert delete_result.result.data["success"] is True + + @pytest.mark.destructive class TestMergeProjectTasks: async def test_merge_project_tasks(self, live_context): diff --git a/float/tests/test_float_timeoff_unit.py b/float/tests/test_float_timeoff_unit.py index 663a47b3..b5045cc4 100644 --- a/float/tests/test_float_timeoff_unit.py +++ b/float/tests/test_float_timeoff_unit.py @@ -147,7 +147,7 @@ async def test_create_time_off_request_body(self, mock_context): ) body = mock_context.fetch.call_args.kwargs.get("json", {}) - assert body["people_id"] == 123 + assert body["people_ids"] == [123] assert body["full_day"] is True @pytest.mark.asyncio