From d59ab2e80ec2274c912fa6333b411751042204f4 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Fri, 25 Sep 2026 17:17:27 -0600 Subject: [PATCH 1/2] fix: allow work item search to raise the 10-result cap `WorkItems.search()` accepts `RetrieveQueryParams`, which carries only expand/fields/external_id/external_source/order_by. The search endpoint itself supports a `limit`, but there is no way to send one through the SDK: `BaseQueryParams` is `extra="ignore"`, so a caller who passes `limit` anyway has it silently dropped. The result is that `search()` can never return more than the API default of 10 rows. The response carries no total and no truncation marker, and search is not cursor paginated, so a caller cannot tell a complete result set from a truncated one. Ten results look exactly like "there are ten matches". This is inconsistent with `advanced_search()`, which already models a `limit` via `AdvancedSearchWorkItem`. Adds `WorkItemSearchQueryParams`, following the existing endpoint-specific params pattern (`WorkItemQueryParams`, `MemberListQueryParams`, `WorkItemCountQueryParams`), and widens the `params` type on `search()` to accept it. `RetrieveQueryParams` is still accepted, so existing callers are unaffected. No change to the request-building logic was needed: the method already merges `params.model_dump(exclude_none=True)` into the query string. Verified against a self-hosted Plane instance (Community Edition): query "the" no params -> 10 results limit=1 -> 1 limit=25 -> 25 limit=50 -> 50 Adds `test_search_work_items_respects_limit` alongside the existing search test. Both pass against a live instance. The test is meaningful rather than vacuous: if `limit` were ignored the endpoint would return 10 and the `<= 1` assertion would fail. `black` produces no changes on the touched files, and `ruff check` reports the same 7 pre-existing findings before and after this change (shifted line numbers only) -- no new lint errors introduced. --- plane/api/work_items/base.py | 7 +++++-- plane/models/__init__.py | 2 ++ plane/models/query_params.py | 17 +++++++++++++++++ tests/unit/test_work_items.py | 20 +++++++++++++++++++- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/plane/api/work_items/base.py b/plane/api/work_items/base.py index 0b7c730..92a48cb 100644 --- a/plane/api/work_items/base.py +++ b/plane/api/work_items/base.py @@ -8,6 +8,7 @@ RetrieveQueryParams, WorkItemCountQueryParams, WorkItemQueryParams, + WorkItemSearchQueryParams, ) from ...models.work_items import ( AdvancedSearchResult, @@ -350,14 +351,16 @@ def search( self, workspace_slug: str, query: str, - params: RetrieveQueryParams | None = None, + params: WorkItemSearchQueryParams | RetrieveQueryParams | None = None, ) -> WorkItemSearch: """Search work items. Args: workspace_slug: The workspace slug identifier query: Search query string - params: Optional query parameters for expand, fields, etc. + params: Optional query parameters for expand, fields, limit, etc. + Pass :class:`WorkItemSearchQueryParams` to raise the result cap; + the API returns 10 results when ``limit`` is omitted. """ search_params = {"search": query} if params: diff --git a/plane/models/__init__.py b/plane/models/__init__.py index 7b3c069..47a6bad 100644 --- a/plane/models/__init__.py +++ b/plane/models/__init__.py @@ -24,6 +24,7 @@ PaginatedQueryParams, ProjectLiteListQueryParams, RetrieveQueryParams, + WorkItemSearchQueryParams, WorkItemQueryParams, ) @@ -53,6 +54,7 @@ "PaginatedQueryParams", "ProjectLiteListQueryParams", "RetrieveQueryParams", + "WorkItemSearchQueryParams", "WorkItemQueryParams", ] diff --git a/plane/models/query_params.py b/plane/models/query_params.py index c8458d2..81fdb9c 100644 --- a/plane/models/query_params.py +++ b/plane/models/query_params.py @@ -111,6 +111,23 @@ class RetrieveQueryParams(BaseQueryParams): model_config = ConfigDict(extra="ignore", populate_by_name=True) +class WorkItemSearchQueryParams(BaseQueryParams): + """Query parameters for the work item search endpoint. + + Search is not cursor paginated; the API caps the result set with ``limit`` + instead. When ``limit`` is omitted it returns 10 results, and the response + carries no total or truncation marker, so a caller cannot distinguish a + complete result set from a truncated one. + """ + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + limit: int | None = Field( + None, + description="Maximum number of results to return. The API returns 10 when omitted.", + ) + + class MemberQueryParams(BaseQueryParams): """Query parameters for workspace/project member list endpoints. diff --git a/tests/unit/test_work_items.py b/tests/unit/test_work_items.py index 2bbc9c2..37b0a01 100644 --- a/tests/unit/test_work_items.py +++ b/tests/unit/test_work_items.py @@ -4,7 +4,11 @@ from plane.client import PlaneClient from plane.models.projects import Project -from plane.models.query_params import PaginatedQueryParams, WorkItemQueryParams +from plane.models.query_params import ( + PaginatedQueryParams, + WorkItemQueryParams, + WorkItemSearchQueryParams, +) from plane.models.work_items import ( AdvancedSearchWorkItem, CreateWorkItem, @@ -154,6 +158,20 @@ def test_search_work_items(self, client: PlaneClient, workspace_slug: str) -> No assert hasattr(response, "issues") assert isinstance(response.issues, list) + def test_search_work_items_respects_limit( + self, client: PlaneClient, workspace_slug: str + ) -> None: + """Test that search honors an explicit result limit. + + Without ``limit`` the API returns at most 10 results and gives no + indication the set was truncated, so a caller cannot page past it. + """ + params = WorkItemSearchQueryParams(limit=1) + response = client.work_items.search(workspace_slug, "test", params=params) + assert response is not None + assert isinstance(response.issues, list) + assert len(response.issues) <= 1 + def test_advanced_search_work_items(self, client: PlaneClient, workspace_slug: str) -> None: """Test advanced search with query only.""" data = AdvancedSearchWorkItem(query="test", limit=10) From 7fe831b50e1da057290b44f7ed335306d2c4cd9c Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Fri, 25 Sep 2026 17:53:53 -0600 Subject: [PATCH 2/2] fix: address review feedback on search limit - Add WorkItemSearchQueryParams to query_params.__all__ (it was only added to plane.models.__all__). - Sort WorkItemSearchQueryParams after WorkItemQueryParams in the import block and __all__ of plane/models/__init__.py, matching isort. - Make the limit test non-vacuous. The previous assertion (len <= 1) would have passed even if limit were ignored, provided the query matched nothing. It now runs the unlimited search first, skips when the workspace has fewer than two matching work items, and asserts both that the limited search returns exactly one result and that it returns fewer than the unlimited search. --- plane/models/__init__.py | 4 ++-- plane/models/query_params.py | 1 + tests/unit/test_work_items.py | 17 +++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/plane/models/__init__.py b/plane/models/__init__.py index 47a6bad..af4d879 100644 --- a/plane/models/__init__.py +++ b/plane/models/__init__.py @@ -24,8 +24,8 @@ PaginatedQueryParams, ProjectLiteListQueryParams, RetrieveQueryParams, - WorkItemSearchQueryParams, WorkItemQueryParams, + WorkItemSearchQueryParams, ) __all__ = [ @@ -54,8 +54,8 @@ "PaginatedQueryParams", "ProjectLiteListQueryParams", "RetrieveQueryParams", - "WorkItemSearchQueryParams", "WorkItemQueryParams", + "WorkItemSearchQueryParams", ] diff --git a/plane/models/query_params.py b/plane/models/query_params.py index 81fdb9c..9e73cb8 100644 --- a/plane/models/query_params.py +++ b/plane/models/query_params.py @@ -375,4 +375,5 @@ class WorkItemCountQueryParams(BaseModel): "WorkItemCountGroupBy", "WorkItemCountQueryParams", "WorkItemQueryParams", + "WorkItemSearchQueryParams", ] diff --git a/tests/unit/test_work_items.py b/tests/unit/test_work_items.py index 37b0a01..1f939fc 100644 --- a/tests/unit/test_work_items.py +++ b/tests/unit/test_work_items.py @@ -165,12 +165,21 @@ def test_search_work_items_respects_limit( Without ``limit`` the API returns at most 10 results and gives no indication the set was truncated, so a caller cannot page past it. + + The unlimited search runs first and the test skips when the workspace + has fewer than two matching work items, so the assertions cannot pass + vacuously against an empty result set. """ + unlimited = client.work_items.search(workspace_slug, "e") + if len(unlimited.issues) < 2: + pytest.skip("workspace has too few matching work items to exercise the limit") + params = WorkItemSearchQueryParams(limit=1) - response = client.work_items.search(workspace_slug, "test", params=params) - assert response is not None - assert isinstance(response.issues, list) - assert len(response.issues) <= 1 + limited = client.work_items.search(workspace_slug, "e", params=params) + assert limited is not None + assert isinstance(limited.issues, list) + assert len(limited.issues) == 1 + assert len(limited.issues) < len(unlimited.issues) def test_advanced_search_work_items(self, client: PlaneClient, workspace_slug: str) -> None: """Test advanced search with query only."""