Fix add_text placeholder metadata on GIMP 3.2 (#15)#21
Fix add_text placeholder metadata on GIMP 3.2 (#15)#21vicArc wants to merge 8 commits intomaorcc:mainfrom
Conversation
WalkthroughResolve fonts to Gimp.Font and rework text insertion: attempt native Gimp.TextLayer creation with PDB fallback, detect newly created layer by snapshot diff, and return actual layer metadata or explicit error. Added an integration test that validates returned layer metadata and chainability. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant MCP as MCP_Server
participant Plugin as Plugin
participant Native as GIMP_Native
participant PDB as GIMP_PDB
Client->>MCP: add_text request
MCP->>Plugin: dispatch add_text
Plugin->>Plugin: snapshot before_ids
Plugin->>Native: attempt Gimp.TextLayer.new + image.insert_layer
alt native success
Native-->>Plugin: layer inserted
Plugin->>Plugin: apply color/offset, snapshot after_ids
else native fail
Plugin->>PDB: call gimp-text-font with Gimp.Font
PDB-->>Plugin: procedure return
Plugin->>Plugin: snapshot after_ids
end
Plugin->>Plugin: _find_new_layer(diff before/after)
Plugin-->>MCP: return status + layer metadata
MCP-->>Client: respond (status, layer_id, layer_name, width, height)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 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: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@gimp-mcp-plugin.py`:
- Around line 3048-3067: The code currently clears text_layer to None on any
exception after creating and inserting tl (via Gimp.TextLayer.new and
image.insert_layer), which can cause a duplicate layer when the PDB fallback
(pdb.lookup_procedure("gimp-text-font")) runs; change the flow so that you set
text_layer = tl immediately after a successful insert (image.insert_layer) and
do NOT reset it to None on exceptions from tl.set_offsets or the color call
(pdb.lookup_procedure("gimp-text-layer-set-color")); instead treat offset/color
operations as best-effort (catch their exceptions locally, log or ignore) so the
presence of an inserted layer prevents the PDB fallback from executing.
In `@tests/test_add_text_metadata.py`:
- Around line 72-88: The test currently picks an image by path suffix which can
select a stale instance; instead use the image_id returned from
cmd('open_image', {'file_path': TEST_IMAGE}) to find the exact image index in
the list returned by cmd('list_images', {}): read r.get('image_id') from the
open_image response and then iterate the images list to find the entry whose
'image_id' matches that value (use that index as target_index); keep the
fallback to path-suffix only if image_id is missing to preserve compatibility.
- Around line 111-129: The test currently allows layer_id == 0 and treats a
listed layer as a match if either id OR name matches; tighten it by requiring a
strictly positive integer id (change the check in the layer_id assertion to
require layer_id > 0) and change the search that builds match (the next(...)
over layers) to require both the id and name match the returned values (use
lyr.get('id') == layer_id AND lyr.get('name') == layer_name) so the same listed
layer must have both the returned ID and name when calling cmd('list_layers',
...).
- Around line 133-135: The test leaves a GIMP image open; ensure you track the
created image index (target_index) and close the image in a finally block to
avoid polluting the persistent GIMP session. Wrap the main test body so
target_index is set when the image is created (where you currently print PASS
add_text...) and add a try/finally that calls the GIMP close function (or
appropriate cleanup function used elsewhere in tests) on target_index in the
finally clause; ensure the close is conditional (only if target_index is not
None/valid) so tests that fail before creation still clean up.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0d57617b-7a02-4f65-a7be-909032524a7b
📒 Files selected for processing (2)
gimp-mcp-plugin.pytests/test_add_text_metadata.py
The previous native-path block only assigned text_layer = tl AFTER set_offsets and the color PDB call succeeded. Any exception from those post-create steps would reset text_layer to None, causing the PDB fallback (gimp-text-font) below to run and insert a *second* text layer on top of the already-inserted one. Commit the layer immediately once image.insert_layer returns, and treat offset + color as best-effort (caught locally) so the fallback only runs when no layer exists yet. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
open_image returns an image_id that uniquely identifies the image it just created. Using it to find the matching entry in list_images is unambiguous even when prior runs have left other winry_joy.png instances open in the persistent GIMP session — path-suffix matching would pick the first (stale) one. Keep a path-suffix fallback for the case where image_id is missing from the open_image response. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor-only, no behaviour change. The previous _add_text body was one long function with nested try/except blocks; splitting it out lets a human reviewer read each stage in isolation, and addresses the CodeRabbit docstring-coverage warning on PR maorcc#21. Plugin (gimp-mcp-plugin.py): - _resolve_font(name): font-name → Gimp.Font with documented fallback chain (exact → aliases → well-known 3.2 fonts → first available). - _create_text_layer_native(): Gimp.TextLayer.new path; layer is committed the moment insert_layer succeeds, post-insert styling (offsets, color) is best-effort, so the PDB fallback can never add a second layer. - _create_text_layer_pdb(): gimp-text-font PDB call (3.x replacement for gimp-text-fontname). - _find_new_layer(): before/after id-diff to locate a layer inserted by a PDB procedure. - _add_text is now a short orchestrator with a docstring describing the overall strategy. Test (tests/test_add_text_metadata.py): - open_test_image(): opens image, picks the right image_index. - call_add_text(): issues the add_text command. - assert_real_metadata(): validates no placeholder values. - assert_chainable(): proves list_layers sees the returned id/name. - main() reads top-to-bottom as four short named steps. All docstrings explain the "why" of each helper (what behaviour it guarantees, what failure mode it exists to prevent), not the "what". Verification on GIMP 3.2.2: tests/test_add_text_metadata.py PASS, run_tests.py 56/56 PASSED, continuous_edit_test.py SUCCESS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten regression assertions: - layer_id must be strictly positive. A real Gimp.Drawable id is never 0, and accepting 0 would let an uninitialised/default-int return slip through the check just as the old -1 sentinel did. - The list_layers cross-check now requires BOTH id AND name of a single listed layer to match the returned metadata. Matching on either-or could succeed against an unrelated layer that happens to share a (non-unique) name, masking a real bug where the id we hand back does not actually resolve to the new text layer. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lean Add close_image_if_open() and wrap main() in try/finally so the opened test image is closed on every exit path (pass, fail, or exception). Repeated runs no longer accumulate duplicate winry_joy.png images in the persistent GIMP session. Cleanup is best-effort: any non-success response from close_image (including pre-existing server-side display-enumeration quirks) is logged to stderr and does not mask the actual test result that triggered the finally. A None target_index (image opening itself failed) short-circuits cleanup safely. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hi @maorcc , |
Comment verbosity vs. project conventions (CLAUDE.md)Finding: The new helpers introduced in this PR carry significantly more documentation than the rest of the codebase, and that excess conflicts with the project's own stated guidelines. What CLAUDE.md says
What the existing codebase doesEvery other method in What this PR introducesThe four new helpers each carry multi-paragraph RST-style docstrings, and Most of the docstring content explains what the code does (restating the function name, parameters, and return value), not why — e.g. Genuinely useful commentary (worth keeping)A few sentences do capture non-obvious WHYs that belong in the code:
Those could be preserved as short inline comments; the rest could be trimmed to one-liners. @maorcc — do you want these addressed before merge? Happy to submit a follow-up that trims the docstrings down to the project style while keeping the three genuinely informative notes above. Generated by Claude Code |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
gimp-mcp-plugin.py (1)
3178-3184: Nit: broaden the error wording — the cause may not be PDB.
"no text layer was created (no PDB procedure succeeded)"is misleading when the actual failure mode is_resolve_fontreturningNone(no fonts loaded at all) or the nativeGimp.TextLayer.newpath raising. Consider a more neutral message so the log points readers in the right direction when debugging.✏️ Suggested wording
if text_layer is None: return { "status": "error", - "error": "add_text: no text layer was created (no PDB procedure succeeded)", + "error": "add_text: no text layer was created (font resolution or text-layer creation failed)", }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gimp-mcp-plugin.py` around lines 3178 - 3184, The error message returned when no text layer is found is too specific about PDB procedures; update the message in the add_text path that checks text_layer and _find_new_layer to use a neutral wording (e.g., "no text layer was created; possible causes include missing fonts or layer creation failure") so it covers failures from _resolve_font returning None, exceptions from Gimp.TextLayer.new, or other creation issues; keep the same return structure and only change the error string around the text_layer check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@gimp-mcp-plugin.py`:
- Around line 3178-3184: The error message returned when no text layer is found
is too specific about PDB procedures; update the message in the add_text path
that checks text_layer and _find_new_layer to use a neutral wording (e.g., "no
text layer was created; possible causes include missing fonts or layer creation
failure") so it covers failures from _resolve_font returning None, exceptions
from Gimp.TextLayer.new, or other creation issues; keep the same return
structure and only change the error string around the text_layer check.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6d2ecbd9-9888-43e3-8b67-8002f56eaee3
📒 Files selected for processing (2)
gimp-mcp-plugin.pytests/test_add_text_metadata.py
_add_text was looking up gimp-text-fontname (GIMP 2.x procedure, not
present in GIMP 3.2) and silently returning placeholder metadata
(layer_id=-1, layer_name='unknown', text_width/height=0) with
status=success. Any client attempting to chain operations on the
returned handle would fail to locate the layer.
Rewrite to use the GIMP 3.2-native path:
1. Resolve font name to a Gimp.Font object, with fallbacks for
legacy aliases ("Sans" -> "Sans-serif") and first-available
as last resort, so stale font names no longer break the call.
2. Primary: Gimp.TextLayer.new + insert_layer + set_offsets.
3. Fallback: gimp-text-font PDB procedure (GIMP 3.x replacement
for gimp-text-fontname; takes a GimpFont object).
4. Identify the new layer via before/after layer-id diff, which
is resilient to PDB return-tuple shape differences across
point releases.
5. Never return placeholder metadata — if no layer was created,
status=error is returned instead of success + fake values.
Add tests/test_add_text_metadata.py which opens winry_joy.png,
calls add_text, and asserts layer_id>0, non-'unknown' name,
positive rendered size, and that the returned handle is visible
to list_layers (proving it is chainable — the core of issue maorcc#15).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous native-path block only assigned text_layer = tl AFTER set_offsets and the color PDB call succeeded. Any exception from those post-create steps would reset text_layer to None, causing the PDB fallback (gimp-text-font) below to run and insert a *second* text layer on top of the already-inserted one. Commit the layer immediately once image.insert_layer returns, and treat offset + color as best-effort (caught locally) so the fallback only runs when no layer exists yet. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
open_image returns an image_id that uniquely identifies the image it just created. Using it to find the matching entry in list_images is unambiguous even when prior runs have left other winry_joy.png instances open in the persistent GIMP session — path-suffix matching would pick the first (stale) one. Keep a path-suffix fallback for the case where image_id is missing from the open_image response. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor-only, no behaviour change. The previous _add_text body was one long function with nested try/except blocks; splitting it out lets a human reviewer read each stage in isolation, and addresses the CodeRabbit docstring-coverage warning on PR maorcc#21. Plugin (gimp-mcp-plugin.py): - _resolve_font(name): font-name → Gimp.Font with documented fallback chain (exact → aliases → well-known 3.2 fonts → first available). - _create_text_layer_native(): Gimp.TextLayer.new path; layer is committed the moment insert_layer succeeds, post-insert styling (offsets, color) is best-effort, so the PDB fallback can never add a second layer. - _create_text_layer_pdb(): gimp-text-font PDB call (3.x replacement for gimp-text-fontname). - _find_new_layer(): before/after id-diff to locate a layer inserted by a PDB procedure. - _add_text is now a short orchestrator with a docstring describing the overall strategy. Test (tests/test_add_text_metadata.py): - open_test_image(): opens image, picks the right image_index. - call_add_text(): issues the add_text command. - assert_real_metadata(): validates no placeholder values. - assert_chainable(): proves list_layers sees the returned id/name. - main() reads top-to-bottom as four short named steps. All docstrings explain the "why" of each helper (what behaviour it guarantees, what failure mode it exists to prevent), not the "what". Verification on GIMP 3.2.2: tests/test_add_text_metadata.py PASS, run_tests.py 56/56 PASSED, continuous_edit_test.py SUCCESS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten regression assertions: - layer_id must be strictly positive. A real Gimp.Drawable id is never 0, and accepting 0 would let an uninitialised/default-int return slip through the check just as the old -1 sentinel did. - The list_layers cross-check now requires BOTH id AND name of a single listed layer to match the returned metadata. Matching on either-or could succeed against an unrelated layer that happens to share a (non-unique) name, masking a real bug where the id we hand back does not actually resolve to the new text layer. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lean Add close_image_if_open() and wrap main() in try/finally so the opened test image is closed on every exit path (pass, fail, or exception). Repeated runs no longer accumulate duplicate winry_joy.png images in the persistent GIMP session. Cleanup is best-effort: any non-success response from close_image (including pre-existing server-side display-enumeration quirks) is logged to stderr and does not mask the actual test result that triggered the finally. A None target_index (image opening itself failed) short-circuits cleanup safely. Addresses CodeRabbit review on PR maorcc#21. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
757a8b3 to
ff1247c
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/test_add_text_metadata.py (1)
2-29: Trim docstrings to match project convention.Per the discussion on this PR (maorcc) and the CLAUDE.md style note (prefer minimal comments explaining WHY, single-line docstrings), the module header and every helper here carry multi-paragraph docstrings that duplicate what the code already says. The genuinely useful "why" notes worth keeping as short inline comments / one-liners are:
assert_real_metadata: rationale forlayer_id > 0(0 would let a default-int slip through like the old-1).assert_chainable: rationale for id and name (either-or would match an unrelated same-named layer).open_test_image: whyimage_idis preferred over path-suffix (stale duplicates in persistent session).close_image_if_open: why cleanup is best-effort (don't mask real test result).Everything else (module history of the old sentinels, the "What this test proves" list, transport details in
send) can be collapsed to a single line each or dropped.Also applies to: 43-47, 86-94, 157-165, 183-194, 211-221
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_add_text_metadata.py` around lines 2 - 29, Trim multi-paragraph module and function docstrings in tests/test_add_text_metadata.py to single-line docstrings per project convention, preserving only concise one-line rationales for assert_real_metadata (why layer_id > 0), assert_chainable (why require both id and name), open_test_image (why prefer image_id over path-suffix), and close_image_if_open (why cleanup is best-effort); collapse or remove other explanatory blocks (module header history, "What this test proves" list, transport/send details) into single-line docstrings or inline comments as appropriate, and update the docstrings around the indicated ranges (including the lines covering the module header and the helpers assert_real_metadata, assert_chainable, open_test_image, close_image_if_open) so tests remain functionally unchanged but comments match the CLAUDE.md style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/test_add_text_metadata.py`:
- Around line 225-230: The cleanup log can raise a TypeError when r.get('error')
is None because the code slices it (None[:200]); update the cleanup block around
the cmd('close_image', ...) call to coerce the error to a string before
slicing—e.g., use str(r.get('error', '')) or (r.get('error') or '') and then
take the first 200 chars—so the print in the except/failure path always handles
None safely; change the reference in this block that currently calls
r.get('error', '') to the coerced-string form.
---
Nitpick comments:
In `@tests/test_add_text_metadata.py`:
- Around line 2-29: Trim multi-paragraph module and function docstrings in
tests/test_add_text_metadata.py to single-line docstrings per project
convention, preserving only concise one-line rationales for assert_real_metadata
(why layer_id > 0), assert_chainable (why require both id and name),
open_test_image (why prefer image_id over path-suffix), and close_image_if_open
(why cleanup is best-effort); collapse or remove other explanatory blocks
(module header history, "What this test proves" list, transport/send details)
into single-line docstrings or inline comments as appropriate, and update the
docstrings around the indicated ranges (including the lines covering the module
header and the helpers assert_real_metadata, assert_chainable, open_test_image,
close_image_if_open) so tests remain functionally unchanged but comments match
the CLAUDE.md style.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 749d928d-d039-4811-9b61-454f76bf1029
📒 Files selected for processing (2)
gimp-mcp-plugin.pytests/test_add_text_metadata.py
🚧 Files skipped from review as they are similar to previous changes (1)
- gimp-mcp-plugin.py
Per @maorcc review on PR maorcc#21: the new add_text helpers carried multi-paragraph RST docstrings, a 14-line ASCII flow diagram, and a 12-line docstring on _add_text, all of which clash with CLAUDE.md's one-short-line-max rule. Trim every docstring to one line and delete the ASCII block. Preserve the three genuinely useful WHY notes as short inline comments: - _create_text_layer_native: why post-insert errors are swallowed (prevents the PDB fallback from inserting a second layer) - _resolve_font: why "Sans" no longer resolves in GIMP 3.2 - _add_text: why it returns status=error instead of a placeholder success (the core guard for issue maorcc#15) Also trim tests/test_add_text_metadata.py along the same lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hi @maorcc |
Socket may return {'error': None}; dict.get's default only kicks in when
the key is absent, so None[:200] raised TypeError inside finally and
masked the real test outcome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
gimp-mcp-plugin.py (1)
3087-3087: Consider defaultingfontto"Sans-serif"instead of"Sans".Per the PR description,
"Sans"no longer resolves on GIMP 3.2 and"Sans-serif"is the native equivalent._resolve_fonthandles this via the alias list, so this works today — but defaulting to the name that actually resolves on the target platform avoids an extra lookup on every call and makes the intent clearer to readers.♻️ Proposed change
- font = params.get("font", "Sans") + font = params.get("font", "Sans-serif")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gimp-mcp-plugin.py` at line 3087, The default font parameter currently uses params.get("font", "Sans") which no longer resolves on GIMP 3.2; change the default to "Sans-serif" so the code (e.g., the font variable assignment and subsequent _resolve_font usage) uses a name that resolves natively and avoids an extra alias lookup on every call—update the params.get default and any related documentation/comments to "Sans-serif".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@gimp-mcp-plugin.py`:
- Line 3087: The default font parameter currently uses params.get("font",
"Sans") which no longer resolves on GIMP 3.2; change the default to "Sans-serif"
so the code (e.g., the font variable assignment and subsequent _resolve_font
usage) uses a name that resolves natively and avoids an extra alias lookup on
every call—update the params.get default and any related documentation/comments
to "Sans-serif".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 80ae1246-a939-46ad-8c64-80187e2ff0cd
📒 Files selected for processing (2)
gimp-mcp-plugin.pytests/test_add_text_metadata.py
✅ Files skipped from review due to trivial changes (1)
- tests/test_add_text_metadata.py
Fixes #15.
Problem
add_textwas returning placeholder metadata while reportingstatus=success:Root cause surfaced while probing a live GIMP 3.2.2 runtime:
_add_textwas looking upgimp-text-fontname— a GIMP 2.x PDB procedure. On GIMP 3.2pdb.lookup_procedure("gimp-text-fontname")returnsNone, so the whole branch was skipped andtext_layerstayedNone."unknown"/-1/0defaults but still reportedstatus=success, so callers had no signal that anything was wrong.Gimp.Font.get_by_name("Sans")returnsNoneon 3.2 (the matching font is"Sans-serif") — so even the native API path would have failed on the default font value.Because the returned
layer_id/layer_namewere fake, any follow-up operation that tried to chain on the new text layer (move, rotate, reorder, recolor) would target the wrong layer or fail lookup entirely.Fix
Rewrite
_add_textfor GIMP 3.2 (gimp-mcp-plugin.py):"Sans"→"Sans-serif","<name> Regular", hyphenated), then"Sans-serif"/"Serif"/"Monospace", and finally the first available font fromGimp.fonts_get_list(""). Stale font names no longer cause silent failure.Gimp.TextLayer.new(image, text, font_obj, size, Gimp.Unit.pixel())+image.insert_layer()+set_offsets()+gimp-text-layer-set-colorPDB proc.gimp-text-fontPDB procedure (the 3.x replacement forgimp-text-fontname; takes aGimpFontobject rather than a string).status=errorwith an explanatory message instead ofstatus=successwith fake values.Test
Added
tests/test_add_text_metadata.py. It openstests/continuous_edit_test/files/winry_joy.png, callsadd_text, and asserts:status == "success"layer_idis a positive int (not-1)layer_nameis not"unknown"and not emptytext_width > 0andtext_height > 0(the layer actually rendered)list_layerssees the returned id/name — proving the handle is chainable (the core requirement of the issue)Verification on GIMP 3.2.2
tests/test_add_text_metadata.py— PASS (layer_id=3 name='Issue15' size=95x31, cross-checked bylist_layers)run_tests.py— 56/56 PASSED, including Cat 7add_textwhich now showslayer_name: 'Hello', layer_id: 13, text_width: 29, ...tests/continuous_edit_test/continuous_edit_test.py— SUCCESS (full BG removal → recolor → composite → export pipeline)Quality gates
ruff check .— cleanpython -m py_compile— clean🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests