fix: WorkItemAttachments.create() returns wrapper response#34
fix: WorkItemAttachments.create() returns wrapper response#34volodchenkov wants to merge 1 commit intomakeplane:mainfrom
Conversation
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughCreate now returns a wrapper model that includes the created attachment plus S3 multipart upload data; update performs a PATCH then re-lists attachments to return the updated record (raising if not found). Tests exercising list/create/update/delete were added. ChangesWorkItem Attachments API & Model
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client\nrgba(66,133,244,0.5)
participant API as Plane API\nrgba(52,168,83,0.5)
participant S3 as S3 Multipart\nrgba(234,67,53,0.5)
Client->>API: POST /work_items/{id}/attachments (create)
API-->>Client: 201 { attachment, upload_data{url, fields}, asset_id?, asset_url? }
Client->>S3: multipart upload using upload_data.url & fields + parts
S3-->>Client: 200 OK (upload complete)
Client->>API: PATCH /work_items/{id}/attachments/{attachment_id} (is_uploaded=True)
API-->>Client: 204 No Content
Client->>API: GET /work_items/{id}/attachments (list) -> find attachment by id
API-->>Client: 200 [ ... attachment (is_uploaded=True) ... ]
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/unit/test_work_item_attachments.py (1)
31-34: 💤 Low valueBare
except Exception: passin three cleanup blocks — flagged by Ruff S110/BLE001.All three teardown paths silently swallow any exception, making failure diagnosis difficult. A lightweight fix is to emit a warning instead of passing silently.
♻️ Proposed fix (shown for the fixture; apply the same pattern to the other two blocks)
+import warnings ... try: client.work_items.delete(workspace_slug, project.id, work_item.id) - except Exception: - pass + except Exception as exc: + warnings.warn(f"Cleanup failed: {exc}", stacklevel=2)Also applies to: 61-66, 99-104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_work_item_attachments.py` around lines 31 - 34, The cleanup blocks use bare "except Exception: pass" (e.g., around client.work_items.delete(workspace_slug, project.id, work_item.id)); replace each bare except with an explicit except Exception as e: that emits a warning or log containing the exception details (for example via warnings.warn or process logger) so failures during teardown are visible; also ensure warnings is imported and apply the same change to the other two cleanup blocks that currently swallow exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/unit/test_work_item_attachments.py`:
- Line 57: The test incorrectly asserts identity against False so it will fail
when WorkItemAttachment.is_uploaded is None; update the assertion in the test to
accept both False and None by replacing "assert result.attachment.is_uploaded is
False" with a check that allows either value (e.g., use "assert not
result.attachment.is_uploaded" or "assert result.attachment.is_uploaded in
(False, None)") so that result.attachment.is_uploaded (the
WorkItemAttachment.is_uploaded field) passes when the API returns null or false.
---
Nitpick comments:
In `@tests/unit/test_work_item_attachments.py`:
- Around line 31-34: The cleanup blocks use bare "except Exception: pass" (e.g.,
around client.work_items.delete(workspace_slug, project.id, work_item.id));
replace each bare except with an explicit except Exception as e: that emits a
warning or log containing the exception details (for example via warnings.warn
or process logger) so failures during teardown are visible; also ensure warnings
is imported and apply the same change to the other two cleanup blocks that
currently swallow exceptions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fdd1533d-8dc3-4592-b0c3-0cc738021c49
📒 Files selected for processing (3)
plane/api/work_items/attachments.pyplane/models/work_items.pytests/unit/test_work_item_attachments.py
2fa20f7 to
6df22d9
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
tests/unit/test_work_item_attachments.py (1)
31-34: ⚡ Quick winBlind
except Exception: passin teardown violates Ruff B (BLE001) — same pattern repeats at lines 65–66 and 103–104.Use
contextlib.suppress(Exception)instead, which is idiomatic, readable, and silences the Ruff warning without changing behavior.♻️ Proposed fix (apply the same pattern to lines 65–66 and 103–104)
+from contextlib import suppress ... yield work_item.id - try: + with suppress(Exception): client.work_items.delete(workspace_slug, project.id, work_item.id) - except Exception: - passAs per coding guidelines: "Apply Ruff rules: … B (bugbear)" — BLE001 is part of the bugbear ruleset.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_work_item_attachments.py` around lines 31 - 34, Replace the blind try/except blocks that call client.work_items.delete(...) in the teardown of tests/unit/test_work_item_attachments.py with contextlib.suppress(Exception); specifically, remove the try: ... except Exception: pass patterns around the client.work_items.delete(workspace_slug, project.id, work_item.id) calls (and the two other identical occurrences) and wrap each delete call in a contextlib.suppress(Exception) context manager so Ruff BLE001 is satisfied while preserving behavior.plane/api/work_items/attachments.py (1)
119-129: ⚖️ Poor tradeoff
update()should callretrieve()after PATCH, notlist()+ filter.The docstring itself claims that the list API only returns
is_uploaded=Truerecords — yettest_full_lifecyclecallsretrieve()before settingis_uploaded=Trueand expects it to return the attachment (lines 85–88 of the test file). Ifretrieve()works post-PATCH (which the test implies), using it here is strictly simpler, avoids loading all attachments, and removes the fragile assumption aboutis_uploadedfiltering inlist().♻️ Proposed refactor
- self._patch( - f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/attachments/{attachment_id}", - data.model_dump(exclude_none=True), - ) - for attachment in self.list(workspace_slug, project_id, work_item_id): - if attachment.id == attachment_id: - return attachment - raise ValueError( - f"Attachment {attachment_id} not found after update; " - "Plane only lists attachments with is_uploaded=True." - ) + self._patch( + f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/attachments/{attachment_id}", + data.model_dump(exclude_none=True), + ) + return self.retrieve(workspace_slug, project_id, work_item_id, attachment_id)Please verify whether
GET /attachments/{id}/on the live Plane API returns JSON attachment metadata or performs an S3 redirect. The update docstring claims the latter, butretrieve()exists and the lifecycle test calls it successfully, suggesting the former.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane/api/work_items/attachments.py` around lines 119 - 129, The update() implementation currently calls self._patch(...) then iterates self.list(...) to find the updated attachment which is inefficient and relies on is_uploaded filtering; instead, after self._patch(...) call self.retrieve(workspace_slug, project_id, work_item_id, attachment_id) and return its result (handle/propagate any exceptions), removing the list/filter loop; also verify whether GET /attachments/{id}/ returns JSON metadata or an S3 redirect and adjust retrieve() behavior/docs accordingly if it follows a redirect flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plane/api/work_items/attachments.py`:
- Line 120: The PATCH endpoint URL string in attachments.py currently lacks the
required trailing slash; update the URL used when constructing the PATCH call
(the f-string containing
"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/attachments/{attachment_id}")
to include a trailing "/" at the end so the endpoint becomes
".../attachments/{attachment_id}/" to comply with the API endpoint guideline.
In `@tests/unit/test_work_item_attachments.py`:
- Around line 82-83: The test currently asserts the new attachment appears in
client.work_items.attachments.list(...) immediately, but according to the
update() docstring list() only includes attachments with is_uploaded=True and
the newly created attachment is is_uploaded=False; update the test to reflect
that by removing or moving the assertion: first assert the attachment is NOT
present in the list (or skip the presence check) before calling
client.work_items.attachments.update(..., is_uploaded=True), then after update()
assert any(a.id == attachment_id for a in listed) to confirm it appears;
reference the methods client.work_items.attachments.list and
client.work_items.attachments.update and the attachment_id variable when
adjusting the assertions.
---
Nitpick comments:
In `@plane/api/work_items/attachments.py`:
- Around line 119-129: The update() implementation currently calls
self._patch(...) then iterates self.list(...) to find the updated attachment
which is inefficient and relies on is_uploaded filtering; instead, after
self._patch(...) call self.retrieve(workspace_slug, project_id, work_item_id,
attachment_id) and return its result (handle/propagate any exceptions), removing
the list/filter loop; also verify whether GET /attachments/{id}/ returns JSON
metadata or an S3 redirect and adjust retrieve() behavior/docs accordingly if it
follows a redirect flow.
In `@tests/unit/test_work_item_attachments.py`:
- Around line 31-34: Replace the blind try/except blocks that call
client.work_items.delete(...) in the teardown of
tests/unit/test_work_item_attachments.py with contextlib.suppress(Exception);
specifically, remove the try: ... except Exception: pass patterns around the
client.work_items.delete(workspace_slug, project.id, work_item.id) calls (and
the two other identical occurrences) and wrap each delete call in a
contextlib.suppress(Exception) context manager so Ruff BLE001 is satisfied while
preserving behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8d2a6414-ede1-4895-a14a-7af07b1dfd85
📒 Files selected for processing (3)
plane/api/work_items/attachments.pyplane/models/work_items.pytests/unit/test_work_item_attachments.py
🚧 Files skipped from review as they are similar to previous changes (1)
- plane/models/work_items.py
Adds four MCP tools wrapping the Plane work-item attachment API: - list_work_item_attachments - create_work_item_attachment - update_work_item_attachment - delete_work_item_attachment The tools follow the existing pattern in plane_mcp/tools/work_item_links.py (flat params at the MCP boundary, Pydantic models from plane-sdk). Closes makeplane#118. Two-step upload flow Plane attachments use a two-step asset flow: 1. POST metadata (name, size, type) -> server returns an attachment record plus an S3 multipart-POST policy in 'upload_data'. 2. Caller posts the file as multipart/form-data to upload_data['url'] using upload_data['fields'] plus a 'file' part. 3. PATCH is_uploaded=true on the attachment to mark it ready. create_work_item_attachment returns a WorkItemAttachmentCreated wrapper exposing both the attachment record and the upload_data so the agent can perform the upload itself, then call update_work_item_attachment with is_uploaded=True. Workarounds for plane-sdk bugs (see makeplane/plane-python-sdk#34) The current pinned plane-sdk (0.2.10) has two attachment bugs we work around in this module: - WorkItemAttachments.create() validates the API wrapper response as a flat WorkItemAttachment, raising ValidationError. We bypass via the inherited _post and validate against our local WorkItemAttachmentCreated wrapper. - WorkItemAttachments.update() calls model_validate on the API's 204 No Content response. We bypass via _patch and then list + filter by id to return the updated record. Both workarounds are documented inline. When the upstream SDK fix lands (PR makeplane/plane-python-sdk#34) and is bumped here, the workarounds can collapse to direct SDK calls. retrieve_work_item_attachment is intentionally not exposed: Plane has no metadata-by-id endpoint for attachments (the GET on a single attachment URL serves a download redirect to S3). Agents that need metadata can use list_work_item_attachments and filter. Tests Extends run_integration_test in tests/test_integration.py with the attachment lifecycle (create -> mark uploaded -> list -> delete) on work_item_1, before the epic step. EXPECTED_TOOLS is updated to include the four attachment tools. Verified end-to-end against a live self-hosted Plane instance: all four attachment operations pass.
6df22d9 to
4974557
Compare
Adds four MCP tools wrapping the Plane work-item attachment API: - list_work_item_attachments - create_work_item_attachment - update_work_item_attachment - delete_work_item_attachment The tools follow the existing pattern in plane_mcp/tools/work_item_links.py (flat params at the MCP boundary, Pydantic models from plane-sdk). Closes makeplane#118. Two-step upload flow Plane attachments use a two-step asset flow: 1. POST metadata (name, size, type) -> server returns an attachment record plus an S3 multipart-POST policy in 'upload_data'. 2. Caller posts the file as multipart/form-data to upload_data['url'] using upload_data['fields'] plus a 'file' part. 3. PATCH is_uploaded=true on the attachment to mark it ready. create_work_item_attachment returns a WorkItemAttachmentCreated wrapper exposing both the attachment record and the upload_data so the agent can perform the upload itself, then call update_work_item_attachment with is_uploaded=True. Workarounds for plane-sdk bugs (see makeplane/plane-python-sdk#34) The current pinned plane-sdk (0.2.10) has two attachment bugs we work around in this module: - WorkItemAttachments.create() validates the API wrapper response as a flat WorkItemAttachment, raising ValidationError. We bypass via the inherited _post and validate against our local WorkItemAttachmentCreated wrapper. - WorkItemAttachments.update() calls model_validate on the API's 204 No Content response. We bypass via _patch and then list + filter by id to return the updated record. Both workarounds are documented inline. When the upstream SDK fix lands (PR makeplane/plane-python-sdk#34) and is bumped here, the workarounds can collapse to direct SDK calls. retrieve_work_item_attachment is intentionally not exposed: Plane has no metadata-by-id endpoint for attachments (the GET on a single attachment URL serves a download redirect to S3). Agents that need metadata can use list_work_item_attachments and filter. Tests Extends run_integration_test in tests/test_integration.py with the attachment lifecycle (create -> mark uploaded -> list -> delete) on work_item_1, before the epic step. EXPECTED_TOOLS is updated to include the four attachment tools. Verified end-to-end against a live self-hosted Plane instance: all four attachment operations pass.
4974557 to
82278fe
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
82278fe to
0bb1112
Compare
Adds four MCP tools wrapping the Plane work-item attachment API: - list_work_item_attachments - create_work_item_attachment - update_work_item_attachment - delete_work_item_attachment The tools follow the existing pattern in plane_mcp/tools/work_item_links.py (flat params at the MCP boundary, Pydantic models from plane-sdk). Closes makeplane#118. Two-step upload flow Plane attachments use a two-step asset flow: 1. POST metadata (name, size, type) -> server returns an attachment record plus an S3 multipart-POST policy in 'upload_data'. 2. Caller posts the file as multipart/form-data to upload_data['url'] using upload_data['fields'] plus a 'file' part. 3. PATCH is_uploaded=true on the attachment to mark it ready. create_work_item_attachment returns a WorkItemAttachmentCreated wrapper exposing both the attachment record and the upload_data so the agent can perform the upload itself, then call update_work_item_attachment with is_uploaded=True. Workarounds for plane-sdk bugs (see makeplane/plane-python-sdk#34) The current pinned plane-sdk (0.2.10) has two attachment bugs we work around in this module: - WorkItemAttachments.create() validates the API wrapper response as a flat WorkItemAttachment, raising ValidationError. We bypass via the inherited _post and validate against our local WorkItemAttachmentCreated wrapper. - WorkItemAttachments.update() calls model_validate on the API's 204 No Content response. We bypass via _patch and then list + filter by id to return the updated record. Both workarounds are documented inline. When the upstream SDK fix lands (PR makeplane/plane-python-sdk#34) and is bumped here, the workarounds can collapse to direct SDK calls. retrieve_work_item_attachment is intentionally not exposed: Plane has no metadata-by-id endpoint for attachments (the GET on a single attachment URL serves a download redirect to S3). Agents that need metadata can use list_work_item_attachments and filter. Tests Extends run_integration_test in tests/test_integration.py with the attachment lifecycle (create -> mark uploaded -> list -> delete) on work_item_1, before the epic step. EXPECTED_TOOLS is updated to include the four attachment tools. Verified end-to-end against a live self-hosted Plane instance: all four attachment operations pass.
create(): The Plane API returns a wrapper around the created attachment record, including an S3 multipart-POST policy in 'upload_data' for the file upload. Previously WorkItemAttachments.create() validated this wrapper as a flat WorkItemAttachment, which raised ValidationError on the missing 'asset' field (the actual attachment is nested inside response['attachment']). This change introduces WorkItemAttachmentCreateResponse, a model that captures the full wrapper shape: - attachment: WorkItemAttachment (the created record) - upload_data: dict (S3 multipart-POST policy) - asset_id: str | None - asset_url: str | None WorkItemAttachments.create() is updated to return this new model. Callers can now access the attachment record via result.attachment and the upload data via result.upload_data to perform the actual file upload to object storage, then call update(is_uploaded=True) to mark the attachment ready. update(): The Plane API responds to attachment PATCH with 204 No Content (no body), and exposes no metadata-by-id endpoint (GET on a single attachment URL serves the file via S3 redirect). Previously update() called WorkItemAttachment.model_validate on the empty body, which raised ValidationError. Update now follows the PATCH with a list call and filters by id, returning the updated WorkItemAttachment per CRUD convention. Tests: Adds tests/unit/test_work_item_attachments.py covering list, create (asserting wrapper shape), and full lifecycle (create -> mark uploaded -> list -> retrieve -> delete). Closes makeplane#33
0bb1112 to
c3c8fa9
Compare
Adds four MCP tools wrapping the Plane work-item attachment API: - list_work_item_attachments - create_work_item_attachment - update_work_item_attachment - delete_work_item_attachment The tools follow the existing pattern in plane_mcp/tools/work_item_links.py (flat params at the MCP boundary, Pydantic models from plane-sdk). Closes makeplane#118. Two-step upload flow Plane attachments use a two-step asset flow: 1. POST metadata (name, size, type) -> server returns an attachment record plus an S3 multipart-POST policy in 'upload_data'. 2. Caller posts the file as multipart/form-data to upload_data['url'] using upload_data['fields'] plus a 'file' part. 3. PATCH is_uploaded=true on the attachment to mark it ready. create_work_item_attachment returns a WorkItemAttachmentCreated wrapper exposing both the attachment record and the upload_data so the agent can perform the upload itself, then call update_work_item_attachment with is_uploaded=True. Workarounds for plane-sdk bugs (see makeplane/plane-python-sdk#34) The current pinned plane-sdk (0.2.10) has two attachment bugs we work around in this module: - WorkItemAttachments.create() validates the API wrapper response as a flat WorkItemAttachment, raising ValidationError. We bypass via the inherited _post and validate against our local WorkItemAttachmentCreated wrapper. - WorkItemAttachments.update() calls model_validate on the API's 204 No Content response. We bypass via _patch and then list + filter by id to return the updated record. Both workarounds are documented inline. When the upstream SDK fix lands (PR makeplane/plane-python-sdk#34) and is bumped here, the workarounds can collapse to direct SDK calls. retrieve_work_item_attachment is intentionally not exposed: Plane has no metadata-by-id endpoint for attachments (the GET on a single attachment URL serves a download redirect to S3). Agents that need metadata can use list_work_item_attachments and filter. Tests Extends run_integration_test in tests/test_integration.py with the attachment lifecycle (create -> mark uploaded -> list -> delete) on work_item_1, before the epic step. EXPECTED_TOOLS is updated to include the four attachment tools. Verified end-to-end against a live self-hosted Plane instance: all four attachment operations pass.
|
Re: the suggestion to use
So Updated the test for the previous reorder feedback (mark uploaded → list → delete) so the @coderabbitai review |
|
Tip For best results, initiate chat on the files or code changes.
Kicking off the re-review now. (⊙_⊙)? [review] ✏️ Learnings added
|
Adds four MCP tools wrapping the Plane work-item attachment API: - list_work_item_attachments - create_work_item_attachment - update_work_item_attachment - delete_work_item_attachment The tools follow the existing pattern in plane_mcp/tools/work_item_links.py (flat params at the MCP boundary, Pydantic models from plane-sdk). Closes makeplane#118. Two-step upload flow Plane attachments use a two-step asset flow: 1. POST metadata (name, size, type) -> server returns an attachment record plus an S3 multipart-POST policy in 'upload_data'. 2. Caller posts the file as multipart/form-data to upload_data['url'] using upload_data['fields'] plus a 'file' part. 3. PATCH is_uploaded=true on the attachment to mark it ready. create_work_item_attachment returns a WorkItemAttachmentCreated wrapper exposing both the attachment record and the upload_data so the agent can perform the upload itself, then call update_work_item_attachment with is_uploaded=True. Workarounds for plane-sdk bugs (see makeplane/plane-python-sdk#34) The current pinned plane-sdk (0.2.10) has two attachment bugs we work around in this module: - WorkItemAttachments.create() validates the API wrapper response as a flat WorkItemAttachment, raising ValidationError. We bypass via the inherited _post and validate against our local WorkItemAttachmentCreated wrapper. - WorkItemAttachments.update() calls model_validate on the API's 204 No Content response. We bypass via _patch and then list + filter by id to return the updated record. Both workarounds are documented inline. When the upstream SDK fix lands (PR makeplane/plane-python-sdk#34) and is bumped here, the workarounds can collapse to direct SDK calls. retrieve_work_item_attachment is intentionally not exposed: Plane has no metadata-by-id endpoint for attachments (the GET on a single attachment URL serves a download redirect to S3). Agents that need metadata can use list_work_item_attachments and filter. Tests Extends run_integration_test in tests/test_integration.py with the attachment lifecycle (create -> mark uploaded -> list -> delete) on work_item_1, before the epic step. EXPECTED_TOOLS is updated to include the four attachment tools. Verified end-to-end against a live self-hosted Plane instance: all four attachment operations pass.
Fixes #33.
Summary
The Plane API returns a wrapper around the created attachment record,
including an S3 multipart-POST policy in
upload_datafor the fileupload. Previously
WorkItemAttachments.create()validated this wrapperas a flat
WorkItemAttachment, raisingValidationErroron the missingassetfield (the actual attachment is nested insideresponse["attachment"]).This PR introduces
WorkItemAttachmentCreateResponsecapturing the fullwrapper shape:
attachment: WorkItemAttachment— the created recordupload_data: dict— S3 multipart-POST policyasset_id: str | Noneasset_url: str | NoneWorkItemAttachments.create()is updated to return this new model.Callers access the attachment via
result.attachmentand the uploaddata via
result.upload_data.Test plan
tests/unit/test_work_item_attachments.pycovering:listreturns empty for new work itemcreatereturns wrapper with both attachment record and upload_datais_uploaded→ deleteruff formatclean on changed filesruff checkclean on changed filesBreaking change note
Return type of
WorkItemAttachments.create()changes fromWorkItemAttachmenttoWorkItemAttachmentCreateResponse. Since theprevious return value crashed
model_validateagainst any real Planeresponse, no caller could have been depending on the old shape working.
Summary by CodeRabbit
New Features
Improvements
Tests