Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion float/config.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
8 changes: 5 additions & 3 deletions float/float.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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)}")
Expand Down Expand Up @@ -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)}")
Expand Down
99 changes: 97 additions & 2 deletions float/tests/test_float_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Comment on lines +618 to +619

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up created time off in the destructive test

When the documented destructive suite is run, this call creates a real time-off entry for the first person but the test never captures the timeoff_id for cleanup or calls delete_time_off afterward. Each run leaves a July 1, 2026 absence in the connected Float account, which can affect schedules/reports and future test runs; please make this a lifecycle test or delete the created entry in a cleanup path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed create_time_off to send people_ids as an array and unwrapped the array response from create_logged_time/update_logged_time - 306ec07

{
"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):
Expand Down
2 changes: 1 addition & 1 deletion float/tests/test_float_timeoff_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading