From 2832bd3e81bb7135720a7c6d9ccd73826d65c8ad Mon Sep 17 00:00:00 2001 From: Henry Stern Date: Wed, 3 Jun 2026 20:09:55 -0300 Subject: [PATCH] fix: accept unexpanded id lists for WorkItemDetail assignees/labels WorkItemDetail typed `assignees: list[UserLite]` and `labels: list[Label]`, but the Plane API returns these relations as bare UUID strings unless the request expands them (`expand=assignees,labels`). Retrieving any assigned or labelled work item without that expand therefore raised: pydantic_core.ValidationError: ... validation errors for WorkItemDetail assignees.0 Input should be a valid dictionary or instance of UserLite labels.0 Input should be a valid dictionary or instance of Label Widen both to `list[str] | list[UserLite]` / `list[str] | list[Label]`, matching the tolerance `WorkItemDetail.state` (`str | StateLite`) and the whole `WorkItemExpand` model already grant. Adds pure model-validation unit tests covering the unexpanded, expanded, and default-empty shapes. Co-Authored-By: Claude Opus 4.8 (1M context) --- plane/models/work_items.py | 7 +++-- tests/unit/test_work_item_models.py | 48 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_work_item_models.py diff --git a/plane/models/work_items.py b/plane/models/work_items.py index 09c91a0..d0d454b 100644 --- a/plane/models/work_items.py +++ b/plane/models/work_items.py @@ -53,8 +53,11 @@ class WorkItemDetail(BaseModel): model_config = ConfigDict(extra="allow", populate_by_name=True) id: str | None = None - assignees: list[UserLite] = Field(default_factory=list) - labels: list[Label] = Field(default_factory=list) + # The API returns bare UUID strings for these relations unless the request + # expands them (``expand=assignees,labels``), so accept both shapes — the + # same tolerance ``state`` (``str | StateLite``) and ``WorkItemExpand`` use. + assignees: list[str] | list[UserLite] = Field(default_factory=list) + labels: list[str] | list[Label] = Field(default_factory=list) type_id: str | None = None created_at: str | None = None updated_at: str | None = None diff --git a/tests/unit/test_work_item_models.py b/tests/unit/test_work_item_models.py new file mode 100644 index 0000000..d973147 --- /dev/null +++ b/tests/unit/test_work_item_models.py @@ -0,0 +1,48 @@ +"""Pure model-validation tests for work item response models (no HTTP). + +The Plane API returns ``assignees`` and ``labels`` on a work item as bare UUID +strings unless the request expands them (``expand=assignees,labels``), in which +case they come back as nested objects. ``WorkItemDetail`` must accept both +shapes — the same tolerance it already grants ``state`` (``str | StateLite``). +""" + +from plane.models.labels import Label +from plane.models.users import UserLite +from plane.models.work_items import WorkItemDetail + + +class TestWorkItemDetailRelations: + def test_accepts_unexpanded_id_lists(self) -> None: + """Default (unexpanded) responses carry bare UUID strings.""" + item = WorkItemDetail.model_validate( + { + "id": "wi-1", + "assignees": ["00000000-0000-0000-0000-000000000001"], + "labels": ["00000000-0000-0000-0000-000000000002"], + "state": "00000000-0000-0000-0000-000000000003", + } + ) + assert item.assignees == ["00000000-0000-0000-0000-000000000001"] + assert item.labels == ["00000000-0000-0000-0000-000000000002"] + assert item.state == "00000000-0000-0000-0000-000000000003" + + def test_accepts_expanded_object_lists(self) -> None: + """``expand=assignees,labels`` responses carry nested objects.""" + item = WorkItemDetail.model_validate( + { + "id": "wi-1", + "assignees": [{"id": "u-1", "display_name": "henry"}], + "labels": [{"id": "l-1", "name": "bug"}], + "state": {"id": "s-1", "name": "Todo"}, + } + ) + assert isinstance(item.assignees[0], UserLite) + assert item.assignees[0].id == "u-1" + assert isinstance(item.labels[0], Label) + assert item.labels[0].name == "bug" + + def test_defaults_to_empty_lists(self) -> None: + """Absent relations default to empty lists, not None.""" + item = WorkItemDetail.model_validate({"id": "wi-1"}) + assert item.assignees == [] + assert item.labels == []