From b960262e8b2341fde7c49beaa932457ee85757a1 Mon Sep 17 00:00:00 2001 From: Gregori Jenkins Date: Tue, 7 Apr 2026 08:20:47 -0700 Subject: [PATCH] fix: make WorkItemDetail assignees/labels optional to support sparse API responses When the Plane API is called with the `fields` parameter, it returns only the requested fields. If `assignees` and `labels` are not in the field list, the response omits them entirely, causing a pydantic ValidationError. Default both fields to `[]` so sparse responses parse without error while preserving identical behavior for full responses. Co-Authored-By: Claude Opus 4.6 (1M context) --- plane/models/work_items.py | 4 +- tests/unit/test_work_item_detail_sparse.py | 94 ++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_work_item_detail_sparse.py diff --git a/plane/models/work_items.py b/plane/models/work_items.py index 9e93080..2e13d0b 100644 --- a/plane/models/work_items.py +++ b/plane/models/work_items.py @@ -53,8 +53,8 @@ class WorkItemDetail(BaseModel): model_config = ConfigDict(extra="allow", populate_by_name=True) id: str | None = None - assignees: list[UserLite] - labels: list[Label] + assignees: list[UserLite] = [] + labels: list[Label] = [] type_id: str | None = None created_at: str | None = None updated_at: str | None = None diff --git a/tests/unit/test_work_item_detail_sparse.py b/tests/unit/test_work_item_detail_sparse.py new file mode 100644 index 0000000..c705715 --- /dev/null +++ b/tests/unit/test_work_item_detail_sparse.py @@ -0,0 +1,94 @@ +"""Tests for WorkItemDetail parsing with sparse API responses (MFT-34). + +When the Plane API is called with a ``fields`` parameter, it returns only the +requested fields. ``assignees`` and ``labels`` may be absent. These tests +verify that ``WorkItemDetail`` handles both full and sparse payloads without +raising a ``ValidationError``. +""" + +import pytest +from pydantic import ValidationError + +from plane.models.work_items import WorkItemDetail + + +# -- Fixtures ---------------------------------------------------------------- + +FULL_RESPONSE = { + "id": "abc-123", + "name": "Test item", + "assignees": [ + { + "id": "user-1", + "first_name": "Alice", + "last_name": "Smith", + "email": "alice@example.com", + "display_name": "alice", + } + ], + "labels": [ + { + "id": "label-1", + "name": "Bug", + "color": "#FF0000", + } + ], + "priority": "high", + "state": "state-uuid", + "project": "proj-uuid", + "workspace": "ws-uuid", +} + +SPARSE_RESPONSE_NO_ASSIGNEES_LABELS = { + "id": "abc-123", + "name": "Test item", + "state": "state-uuid", +} + +SPARSE_RESPONSE_ID_AND_NAME_ONLY = { + "id": "abc-123", + "name": "Test item", +} + + +# -- Tests ------------------------------------------------------------------- + + +class TestWorkItemDetailSparseResponse: + """Regression tests for MFT-34: fields parameter causes ValidationError.""" + + def test_full_response_parses(self): + """Full API response (with assignees + labels) still works.""" + item = WorkItemDetail.model_validate(FULL_RESPONSE) + assert item.id == "abc-123" + assert len(item.assignees) == 1 + assert item.assignees[0].id == "user-1" + assert len(item.labels) == 1 + assert item.labels[0].name == "Bug" + + def test_sparse_response_without_assignees_and_labels(self): + """Sparse response (fields=id,name,state) must not raise ValidationError.""" + item = WorkItemDetail.model_validate(SPARSE_RESPONSE_NO_ASSIGNEES_LABELS) + assert item.id == "abc-123" + assert item.name == "Test item" + assert item.assignees == [] + assert item.labels == [] + + def test_sparse_response_id_and_name_only(self): + """Minimal sparse response (fields=id,name) must not raise.""" + item = WorkItemDetail.model_validate(SPARSE_RESPONSE_ID_AND_NAME_ONLY) + assert item.id == "abc-123" + assert item.assignees == [] + assert item.labels == [] + + def test_empty_assignees_and_labels_accepted(self): + """Explicit empty lists are accepted (normal case with no assignees).""" + data = {**FULL_RESPONSE, "assignees": [], "labels": []} + item = WorkItemDetail.model_validate(data) + assert item.assignees == [] + assert item.labels == [] + + def test_name_still_required(self): + """``name`` is still required — sparse responses always include it.""" + with pytest.raises(ValidationError): + WorkItemDetail.model_validate({"id": "abc-123"})