-
Notifications
You must be signed in to change notification settings - Fork 15
Fix add_text placeholder metadata on GIMP 3.2 (#15) #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vicArc
wants to merge
8
commits into
maorcc:main
Choose a base branch
from
vicArc:fix/issue-15-add-text-metadata
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+300
−23
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7bc9d4c
Fix add_text to return real layer metadata (#15)
vicArc a71430b
add_text: prevent duplicate layer when native path partially fails
vicArc 255f62d
test_add_text_metadata: select image by image_id, not path suffix
vicArc d434bba
add_text + test: split into small documented helpers for reviewability
vicArc 03b3b4b
test_add_text_metadata: require layer_id > 0 and id+name match
vicArc ff1247c
test_add_text_metadata: close image in finally to keep GIMP session c…
vicArc 4f7b9b0
add_text: trim docstrings and comments to match CLAUDE.md style
vicArc 821761c
test_add_text_metadata: coerce error to string in cleanup log
vicArc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| #!/usr/bin/env python3 | ||
| """Regression test for issue #15 — add_text must return real layer metadata. | ||
|
|
||
| Requires the GIMP MCP plugin running on localhost:9877. | ||
| Exits 0 on pass, 1 on fail. | ||
| """ | ||
| import json | ||
| import os | ||
| import socket | ||
| import sys | ||
|
|
||
| HOST, PORT = '127.0.0.1', 9877 | ||
| HERE = os.path.dirname(os.path.abspath(__file__)) | ||
| TEST_IMAGE = os.path.join(HERE, 'continuous_edit_test', 'files', 'winry_joy.png') | ||
|
|
||
|
|
||
| # ── transport ───────────────────────────────────────────────────────────── | ||
|
|
||
| def send(msg, timeout=30): | ||
| """Send one JSON message to the MCP socket and return the reply.""" | ||
| s = socket.socket() | ||
| s.settimeout(timeout) | ||
| s.connect((HOST, PORT)) | ||
| s.send(json.dumps(msg).encode() + b'\n') | ||
| buf = b'' | ||
| while True: | ||
| try: | ||
| chunk = s.recv(65536) | ||
| if not chunk: | ||
| break | ||
| buf += chunk | ||
| try: | ||
| json.loads(buf.decode()) | ||
| break | ||
| except json.JSONDecodeError: | ||
| continue | ||
| except socket.timeout: | ||
| break | ||
| s.close() | ||
| try: | ||
| return json.loads(buf.decode().strip()) | ||
| except json.JSONDecodeError: | ||
| return {'status': 'error', 'error': 'parse: ' + buf.decode()[:200]} | ||
|
|
||
|
|
||
| def cmd(t, params=None): | ||
| """Send a {type, params} command.""" | ||
| return send({'type': t, 'params': params or {}}) | ||
|
|
||
|
|
||
| def fail(msg): | ||
| """Abort with a failure banner on stderr.""" | ||
| print(f"FAIL: {msg}", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| # ── test steps ──────────────────────────────────────────────────────────── | ||
|
|
||
| def open_test_image(): | ||
| """Open the test image and return (target_index, opened_id).""" | ||
| if not os.path.exists(TEST_IMAGE): | ||
| fail(f"test image missing: {TEST_IMAGE}") | ||
|
|
||
| print(f"Opening test image: {TEST_IMAGE}") | ||
| r = cmd('open_image', {'file_path': TEST_IMAGE}) | ||
| if r.get('status') != 'success': | ||
| fail(f"open_image: {r.get('error', '')}") | ||
|
|
||
| opened_id = r.get('image_id') or (r.get('results') or {}).get('image_id') | ||
|
|
||
| li = cmd('list_images', {}) | ||
| images = (li.get('results') or {}).get('images') or [] | ||
| if not images: | ||
| fail("no images after open_image") | ||
|
|
||
| # Match by image_id (authoritative) — path-suffix can hit a stale | ||
| # duplicate in a persistent GIMP session. | ||
| target_index = None | ||
| if opened_id is not None: | ||
| target_index = _find_index_by(images, 'image_id', opened_id) | ||
| if target_index is None: | ||
| target_index = _find_index_by_suffix(images, 'winry_joy.png') | ||
| if target_index is None: | ||
| target_index = 0 | ||
|
|
||
| print(f"Using image_index={target_index} (image_id={opened_id})") | ||
| return target_index, opened_id | ||
|
|
||
|
|
||
| def _find_index_by(images, key, value): | ||
| for i, info in enumerate(images): | ||
| if isinstance(info, dict) and info.get(key) == value: | ||
| return i | ||
| return None | ||
|
|
||
|
|
||
| def _find_index_by_suffix(images, suffix): | ||
| for i, info in enumerate(images): | ||
| if isinstance(info, dict) and info.get('file_path', '').endswith(suffix): | ||
| return i | ||
| return None | ||
|
|
||
|
|
||
| def call_add_text(target_index): | ||
| """Invoke add_text and return the response results dict.""" | ||
| print("Calling add_text with real parameters...") | ||
| r = cmd('add_text', { | ||
| 'image_index': target_index, | ||
| 'text': 'Issue15', | ||
| 'x': 10, | ||
| 'y': 10, | ||
| 'font': 'Sans', | ||
| 'size': 24, | ||
| 'color': '#ff0000', | ||
| }) | ||
| print(f" response: {json.dumps(r)[:300]}") | ||
| if r.get('status') != 'success': | ||
| fail(f"add_text reported error: {r.get('error', '')}") | ||
| return r.get('results') or {} | ||
|
|
||
|
|
||
| def assert_real_metadata(results): | ||
| """Verify add_text returned real values, not placeholders.""" | ||
| layer_id = results.get('layer_id') | ||
| layer_name = results.get('layer_name') | ||
| text_w = results.get('text_width') | ||
| text_h = results.get('text_height') | ||
|
|
||
| # layer_id must be strictly positive — a real Gimp.Drawable id is never 0, | ||
| # so accepting 0 would let a default-int slip through like the old -1 did. | ||
| if not isinstance(layer_id, int) or layer_id <= 0: | ||
| fail(f"layer_id is placeholder: {layer_id!r} (expected positive int)") | ||
| if not layer_name or layer_name == 'unknown': | ||
| fail(f"layer_name is placeholder: {layer_name!r}") | ||
| if not isinstance(text_w, int) or text_w <= 0: | ||
| fail(f"text_width is placeholder/zero: {text_w!r}") | ||
| if not isinstance(text_h, int) or text_h <= 0: | ||
| fail(f"text_height is placeholder/zero: {text_h!r}") | ||
| return layer_id, layer_name, text_w, text_h | ||
|
|
||
|
|
||
| def assert_chainable(target_index, layer_id, layer_name): | ||
| """Verify the returned handle is visible to list_layers.""" | ||
| ll = cmd('list_layers', {'image_index': target_index}) | ||
| if ll.get('status') != 'success': | ||
| fail(f"list_layers failed: {ll.get('error', '')}") | ||
|
|
||
| # Require both id AND name to match — id-or-name could pass against an | ||
| # unrelated layer that happens to share a non-unique name. | ||
| layers = (ll.get('results') or {}).get('layers') or [] | ||
| match = next((lyr for lyr in layers | ||
| if lyr.get('id') == layer_id and lyr.get('name') == layer_name), | ||
| None) | ||
| if match is None: | ||
| fail(f"returned layer (id={layer_id}, name={layer_name!r}) not in list_layers: {layers}") | ||
| return match | ||
|
|
||
|
|
||
| # ── entry point ─────────────────────────────────────────────────────────── | ||
|
|
||
| def close_image_if_open(target_index): | ||
| """Close the test image so a persistent GIMP session stays clean.""" | ||
| if target_index is None: | ||
| return | ||
| try: | ||
| r = cmd('close_image', {'image_index': target_index, 'save_first': False}) | ||
| if r.get('status') != 'success': | ||
| err = str(r.get('error') or '')[:200] | ||
| print(f" (cleanup) close_image non-success: {err}", file=sys.stderr) | ||
| except Exception as e: | ||
| print(f" (cleanup) close_image transport error: {e}", file=sys.stderr) | ||
|
|
||
|
|
||
| def main(): | ||
| target_index = None | ||
| try: | ||
| target_index, _opened_id = open_test_image() | ||
| results = call_add_text(target_index) | ||
| layer_id, layer_name, w, h = assert_real_metadata(results) | ||
| match = assert_chainable(target_index, layer_id, layer_name) | ||
|
|
||
| print(f"PASS add_text: layer_id={layer_id} name={layer_name!r} size={w}x{h}") | ||
| print(f"PASS list_layers confirms layer is chainable: {match}") | ||
| sys.exit(0) | ||
| finally: | ||
| close_image_if_open(target_index) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.