Skip to content

Commit 5fb4e25

Browse files
committed
fix: accept UUID strings for WorkItemDetail labels and assignees
Plane v1.3.0 changed the API to return labels and assignees as UUID strings instead of expanded objects. WorkItemDetail typed these as list[Label] and list[UserLite] respectively, causing a Pydantic ValidationError on any retrieve_work_item call against a v1.3.0+ instance. Change both fields to list[str] | list[T] = [] so they accept either representation, and default to [] for sparse responses (consistent with WorkItemExpand and the fix in makeplane#28). Adds 8 unit tests covering: UUID strings, expanded objects, empty lists, omitted fields, and that name remains required.
1 parent 72ecc72 commit 5fb4e25

File tree

2 files changed

+111
-2
lines changed

2 files changed

+111
-2
lines changed

plane/models/work_items.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class WorkItemDetail(BaseModel):
5353
model_config = ConfigDict(extra="allow", populate_by_name=True)
5454

5555
id: str | None = None
56-
assignees: list[UserLite]
57-
labels: list[Label]
56+
assignees: list[str] | list[UserLite] = []
57+
labels: list[str] | list[Label] = []
5858
type_id: str | None = None
5959
created_at: str | None = None
6060
updated_at: str | None = None
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Unit tests for WorkItemDetail model type compatibility with Plane v1.3.0+.
2+
3+
Plane v1.3.0 changed the API to return labels and assignees as UUID strings
4+
instead of expanded objects, even when not using the `fields` sparse parameter.
5+
These tests verify that WorkItemDetail handles both representations.
6+
"""
7+
8+
import pytest
9+
from pydantic import ValidationError
10+
11+
from plane.models.work_items import WorkItemDetail
12+
13+
14+
MINIMAL_WORK_ITEM = {
15+
"id": "ef6bf853-fecb-433e-a4e3-8546e60abebd",
16+
"name": "Test issue",
17+
"sequence_id": 1,
18+
}
19+
20+
21+
class TestWorkItemDetailLabelsAssigneesTypes:
22+
"""WorkItemDetail should accept labels/assignees as UUID strings or objects."""
23+
24+
def test_labels_as_uuid_strings(self) -> None:
25+
"""Plane v1.3.0 returns labels as UUID strings — must not raise."""
26+
data = {
27+
**MINIMAL_WORK_ITEM,
28+
"labels": ["f6a24a78-a275-4fd1-a777-0e9e0e99dbef"],
29+
"assignees": [],
30+
}
31+
item = WorkItemDetail(**data)
32+
assert item.labels == ["f6a24a78-a275-4fd1-a777-0e9e0e99dbef"]
33+
34+
def test_assignees_as_uuid_strings(self) -> None:
35+
"""Plane v1.3.0 returns assignees as UUID strings — must not raise."""
36+
data = {
37+
**MINIMAL_WORK_ITEM,
38+
"labels": [],
39+
"assignees": ["48b05854-3e71-44f0-9fcb-7a5d6887f5ec"],
40+
}
41+
item = WorkItemDetail(**data)
42+
assert item.assignees == ["48b05854-3e71-44f0-9fcb-7a5d6887f5ec"]
43+
44+
def test_both_as_uuid_strings(self) -> None:
45+
"""Both fields as UUID strings simultaneously."""
46+
data = {
47+
**MINIMAL_WORK_ITEM,
48+
"labels": [
49+
"f6a24a78-a275-4fd1-a777-0e9e0e99dbef",
50+
"a8509ac8-7c71-4b9e-9a4a-623ef2c2365b",
51+
],
52+
"assignees": ["48b05854-3e71-44f0-9fcb-7a5d6887f5ec"],
53+
}
54+
item = WorkItemDetail(**data)
55+
assert len(item.labels) == 2
56+
assert len(item.assignees) == 1
57+
58+
def test_labels_as_objects(self) -> None:
59+
"""Expanded label objects (pre-v1.3.0 behavior) still parse correctly."""
60+
data = {
61+
**MINIMAL_WORK_ITEM,
62+
"labels": [
63+
{
64+
"id": "f6a24a78-a275-4fd1-a777-0e9e0e99dbef",
65+
"name": "v3",
66+
"color": "#FF6900",
67+
"project": "9aa02e26-3b44-4fd2-96f9-015aa9ee7a52",
68+
"workspace": "0f4d413a-a164-4168-aa31-abbdaa0aecd1",
69+
}
70+
],
71+
"assignees": [],
72+
}
73+
item = WorkItemDetail(**data)
74+
assert len(item.labels) == 1
75+
76+
def test_assignees_as_objects(self) -> None:
77+
"""Expanded assignee objects (pre-v1.3.0 behavior) still parse correctly."""
78+
data = {
79+
**MINIMAL_WORK_ITEM,
80+
"labels": [],
81+
"assignees": [
82+
{
83+
"id": "48b05854-3e71-44f0-9fcb-7a5d6887f5ec",
84+
"display_name": "vic",
85+
"avatar": None,
86+
"is_bot": False,
87+
}
88+
],
89+
}
90+
item = WorkItemDetail(**data)
91+
assert len(item.assignees) == 1
92+
93+
def test_empty_lists(self) -> None:
94+
"""Empty lists for both fields are valid."""
95+
data = {**MINIMAL_WORK_ITEM, "labels": [], "assignees": []}
96+
item = WorkItemDetail(**data)
97+
assert item.labels == []
98+
assert item.assignees == []
99+
100+
def test_fields_omitted_defaults_to_empty(self) -> None:
101+
"""Fields absent from response default to empty lists (sparse responses)."""
102+
item = WorkItemDetail(**MINIMAL_WORK_ITEM)
103+
assert item.labels == []
104+
assert item.assignees == []
105+
106+
def test_name_still_required(self) -> None:
107+
"""name remains a required field — no over-relaxation."""
108+
with pytest.raises(ValidationError):
109+
WorkItemDetail(id="abc", labels=[], assignees=[])

0 commit comments

Comments
 (0)