@agent-webui/ai-desk-harness-gimp 1.0.29-beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +4 -0
  2. package/manifest.json +22 -0
  3. package/package.json +11 -0
  4. package/python/agent-harness/GIMP.md +301 -0
  5. package/python/agent-harness/build/lib/cli_anything/gimp/__init__.py +1 -0
  6. package/python/agent-harness/build/lib/cli_anything/gimp/__main__.py +3 -0
  7. package/python/agent-harness/build/lib/cli_anything/gimp/core/__init__.py +1 -0
  8. package/python/agent-harness/build/lib/cli_anything/gimp/core/canvas.py +193 -0
  9. package/python/agent-harness/build/lib/cli_anything/gimp/core/export.py +479 -0
  10. package/python/agent-harness/build/lib/cli_anything/gimp/core/filters.py +382 -0
  11. package/python/agent-harness/build/lib/cli_anything/gimp/core/layers.py +249 -0
  12. package/python/agent-harness/build/lib/cli_anything/gimp/core/media.py +174 -0
  13. package/python/agent-harness/build/lib/cli_anything/gimp/core/project.py +131 -0
  14. package/python/agent-harness/build/lib/cli_anything/gimp/core/session.py +130 -0
  15. package/python/agent-harness/build/lib/cli_anything/gimp/gimp_cli.py +788 -0
  16. package/python/agent-harness/build/lib/cli_anything/gimp/tests/__init__.py +1 -0
  17. package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_core.py +478 -0
  18. package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_full_e2e.py +578 -0
  19. package/python/agent-harness/build/lib/cli_anything/gimp/utils/__init__.py +1 -0
  20. package/python/agent-harness/build/lib/cli_anything/gimp/utils/gimp_backend.py +208 -0
  21. package/python/agent-harness/build/lib/cli_anything/gimp/utils/repl_skin.py +498 -0
  22. package/python/agent-harness/cli_anything/gimp/README.md +202 -0
  23. package/python/agent-harness/cli_anything/gimp/__init__.py +1 -0
  24. package/python/agent-harness/cli_anything/gimp/__main__.py +3 -0
  25. package/python/agent-harness/cli_anything/gimp/core/__init__.py +1 -0
  26. package/python/agent-harness/cli_anything/gimp/core/canvas.py +193 -0
  27. package/python/agent-harness/cli_anything/gimp/core/export.py +479 -0
  28. package/python/agent-harness/cli_anything/gimp/core/filters.py +382 -0
  29. package/python/agent-harness/cli_anything/gimp/core/layers.py +249 -0
  30. package/python/agent-harness/cli_anything/gimp/core/media.py +174 -0
  31. package/python/agent-harness/cli_anything/gimp/core/project.py +131 -0
  32. package/python/agent-harness/cli_anything/gimp/core/session.py +130 -0
  33. package/python/agent-harness/cli_anything/gimp/gimp_cli.py +788 -0
  34. package/python/agent-harness/cli_anything/gimp/tests/TEST.md +137 -0
  35. package/python/agent-harness/cli_anything/gimp/tests/__init__.py +1 -0
  36. package/python/agent-harness/cli_anything/gimp/tests/test_core.py +478 -0
  37. package/python/agent-harness/cli_anything/gimp/tests/test_full_e2e.py +578 -0
  38. package/python/agent-harness/cli_anything/gimp/utils/__init__.py +1 -0
  39. package/python/agent-harness/cli_anything/gimp/utils/gimp_backend.py +208 -0
  40. package/python/agent-harness/cli_anything/gimp/utils/repl_skin.py +498 -0
  41. package/python/agent-harness/cli_anything_gimp.egg-info/PKG-INFO +236 -0
  42. package/python/agent-harness/cli_anything_gimp.egg-info/SOURCES.txt +25 -0
  43. package/python/agent-harness/cli_anything_gimp.egg-info/dependency_links.txt +1 -0
  44. package/python/agent-harness/cli_anything_gimp.egg-info/entry_points.txt +2 -0
  45. package/python/agent-harness/cli_anything_gimp.egg-info/not-zip-safe +1 -0
  46. package/python/agent-harness/cli_anything_gimp.egg-info/requires.txt +7 -0
  47. package/python/agent-harness/cli_anything_gimp.egg-info/top_level.txt +1 -0
  48. package/python/agent-harness/setup.py +54 -0
@@ -0,0 +1 @@
1
+ """GIMP CLI - Tests package."""
@@ -0,0 +1,478 @@
1
+ """Unit tests for GIMP CLI core modules.
2
+
3
+ Tests use synthetic data only — no real images or external dependencies.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ import pytest
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
13
+
14
+ from cli_anything.gimp.core.project import create_project, open_project, save_project, get_project_info, list_profiles
15
+ from cli_anything.gimp.core.layers import (
16
+ add_layer, add_from_file, remove_layer, duplicate_layer, move_layer,
17
+ set_layer_property, get_layer, list_layers, BLEND_MODES,
18
+ )
19
+ from cli_anything.gimp.core.filters import (
20
+ list_available, get_filter_info, validate_params, add_filter,
21
+ remove_filter, set_filter_param, list_filters, FILTER_REGISTRY,
22
+ )
23
+ from cli_anything.gimp.core.canvas import (
24
+ resize_canvas, scale_canvas, crop_canvas, set_mode, set_dpi, get_canvas_info,
25
+ )
26
+ from cli_anything.gimp.core.session import Session
27
+
28
+
29
+ # ── Project Tests ────────────────────────────────────────────────
30
+
31
+ class TestProject:
32
+ def test_create_default(self):
33
+ proj = create_project()
34
+ assert proj["canvas"]["width"] == 1920
35
+ assert proj["canvas"]["height"] == 1080
36
+ assert proj["canvas"]["color_mode"] == "RGB"
37
+ assert proj["version"] == "1.0"
38
+
39
+ def test_create_with_dimensions(self):
40
+ proj = create_project(width=800, height=600, dpi=150)
41
+ assert proj["canvas"]["width"] == 800
42
+ assert proj["canvas"]["height"] == 600
43
+ assert proj["canvas"]["dpi"] == 150
44
+
45
+ def test_create_with_profile(self):
46
+ proj = create_project(profile="hd720p")
47
+ assert proj["canvas"]["width"] == 1280
48
+ assert proj["canvas"]["height"] == 720
49
+
50
+ def test_create_invalid_mode(self):
51
+ with pytest.raises(ValueError, match="Invalid color mode"):
52
+ create_project(color_mode="XYZ")
53
+
54
+ def test_create_invalid_dimensions(self):
55
+ with pytest.raises(ValueError, match="must be positive"):
56
+ create_project(width=0, height=100)
57
+
58
+ def test_save_and_open(self):
59
+ proj = create_project(name="test_project")
60
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as f:
61
+ path = f.name
62
+ try:
63
+ save_project(proj, path)
64
+ loaded = open_project(path)
65
+ assert loaded["name"] == "test_project"
66
+ assert loaded["canvas"]["width"] == 1920
67
+ finally:
68
+ os.unlink(path)
69
+
70
+ def test_open_nonexistent(self):
71
+ with pytest.raises(FileNotFoundError):
72
+ open_project("/nonexistent/path.json")
73
+
74
+ def test_get_info(self):
75
+ proj = create_project(name="info_test")
76
+ info = get_project_info(proj)
77
+ assert info["name"] == "info_test"
78
+ assert info["layer_count"] == 0
79
+ assert "canvas" in info
80
+
81
+ def test_list_profiles(self):
82
+ profiles = list_profiles()
83
+ assert len(profiles) > 0
84
+ names = [p["name"] for p in profiles]
85
+ assert "hd1080p" in names
86
+ assert "4k" in names
87
+
88
+
89
+ # ── Layer Tests ──────────────────────────────────────────────────
90
+
91
+ class TestLayers:
92
+ def _make_project(self):
93
+ return create_project()
94
+
95
+ def test_add_layer(self):
96
+ proj = self._make_project()
97
+ layer = add_layer(proj, name="Test", layer_type="image")
98
+ assert layer["name"] == "Test"
99
+ assert layer["type"] == "image"
100
+ assert len(proj["layers"]) == 1
101
+
102
+ def test_add_multiple_layers(self):
103
+ proj = self._make_project()
104
+ add_layer(proj, name="Bottom")
105
+ add_layer(proj, name="Top")
106
+ assert len(proj["layers"]) == 2
107
+ assert proj["layers"][0]["name"] == "Top" # Top of stack
108
+
109
+ def test_add_layer_with_position(self):
110
+ proj = self._make_project()
111
+ add_layer(proj, name="First")
112
+ add_layer(proj, name="Second", position=1)
113
+ assert proj["layers"][1]["name"] == "Second"
114
+
115
+ def test_add_layer_invalid_mode(self):
116
+ proj = self._make_project()
117
+ with pytest.raises(ValueError, match="Invalid blend mode"):
118
+ add_layer(proj, blend_mode="invalid")
119
+
120
+ def test_add_layer_invalid_opacity(self):
121
+ proj = self._make_project()
122
+ with pytest.raises(ValueError, match="Opacity"):
123
+ add_layer(proj, opacity=1.5)
124
+
125
+ def test_remove_layer(self):
126
+ proj = self._make_project()
127
+ add_layer(proj, name="A")
128
+ add_layer(proj, name="B")
129
+ removed = remove_layer(proj, 0)
130
+ assert removed["name"] == "B"
131
+ assert len(proj["layers"]) == 1
132
+
133
+ def test_remove_layer_invalid_index(self):
134
+ proj = self._make_project()
135
+ with pytest.raises(ValueError, match="No layers"):
136
+ remove_layer(proj, 0)
137
+
138
+ def test_duplicate_layer(self):
139
+ proj = self._make_project()
140
+ add_layer(proj, name="Original")
141
+ dup = duplicate_layer(proj, 0)
142
+ assert dup["name"] == "Original copy"
143
+ assert len(proj["layers"]) == 2
144
+
145
+ def test_move_layer(self):
146
+ proj = self._make_project()
147
+ add_layer(proj, name="A")
148
+ add_layer(proj, name="B")
149
+ add_layer(proj, name="C")
150
+ move_layer(proj, 0, 2)
151
+ assert proj["layers"][2]["name"] == "C"
152
+
153
+ def test_set_property_opacity(self):
154
+ proj = self._make_project()
155
+ add_layer(proj, name="Test")
156
+ set_layer_property(proj, 0, "opacity", 0.5)
157
+ assert proj["layers"][0]["opacity"] == 0.5
158
+
159
+ def test_set_property_visible(self):
160
+ proj = self._make_project()
161
+ add_layer(proj, name="Test")
162
+ set_layer_property(proj, 0, "visible", "false")
163
+ assert proj["layers"][0]["visible"] is False
164
+
165
+ def test_set_property_name(self):
166
+ proj = self._make_project()
167
+ add_layer(proj, name="Old")
168
+ set_layer_property(proj, 0, "name", "New")
169
+ assert proj["layers"][0]["name"] == "New"
170
+
171
+ def test_set_property_invalid(self):
172
+ proj = self._make_project()
173
+ add_layer(proj, name="Test")
174
+ with pytest.raises(ValueError, match="Unknown property"):
175
+ set_layer_property(proj, 0, "bogus", "value")
176
+
177
+ def test_get_layer(self):
178
+ proj = self._make_project()
179
+ add_layer(proj, name="Test")
180
+ layer = get_layer(proj, 0)
181
+ assert layer["name"] == "Test"
182
+
183
+ def test_list_layers(self):
184
+ proj = self._make_project()
185
+ add_layer(proj, name="A")
186
+ add_layer(proj, name="B")
187
+ result = list_layers(proj)
188
+ assert len(result) == 2
189
+ assert result[0]["name"] == "B"
190
+
191
+ def test_layer_ids_unique(self):
192
+ proj = self._make_project()
193
+ l1 = add_layer(proj, name="A")
194
+ l2 = add_layer(proj, name="B")
195
+ assert l1["id"] != l2["id"]
196
+
197
+ def test_solid_layer(self):
198
+ proj = self._make_project()
199
+ layer = add_layer(proj, name="Red", layer_type="solid", fill="#ff0000")
200
+ assert layer["type"] == "solid"
201
+ assert layer["fill"] == "#ff0000"
202
+
203
+ def test_text_layer(self):
204
+ proj = self._make_project()
205
+ layer = add_layer(proj, name="Title", layer_type="text")
206
+ assert layer["type"] == "text"
207
+ assert "text" in layer
208
+ assert "font_size" in layer
209
+
210
+
211
+ # ── Filter Tests ─────────────────────────────────────────────────
212
+
213
+ class TestFilters:
214
+ def _make_project_with_layer(self):
215
+ proj = create_project()
216
+ add_layer(proj, name="Test")
217
+ return proj
218
+
219
+ def test_list_available(self):
220
+ filters = list_available()
221
+ assert len(filters) > 10
222
+ names = [f["name"] for f in filters]
223
+ assert "brightness" in names
224
+ assert "gaussian_blur" in names
225
+
226
+ def test_list_by_category(self):
227
+ blurs = list_available(category="blur")
228
+ assert all(f["category"] == "blur" for f in blurs)
229
+ assert len(blurs) >= 3
230
+
231
+ def test_get_filter_info(self):
232
+ info = get_filter_info("brightness")
233
+ assert info["name"] == "brightness"
234
+ assert "factor" in info["params"]
235
+
236
+ def test_get_filter_info_unknown(self):
237
+ with pytest.raises(ValueError, match="Unknown filter"):
238
+ get_filter_info("nonexistent")
239
+
240
+ def test_validate_params(self):
241
+ params = validate_params("brightness", {"factor": 1.5})
242
+ assert params["factor"] == 1.5
243
+
244
+ def test_validate_params_defaults(self):
245
+ params = validate_params("brightness", {})
246
+ assert params["factor"] == 1.0
247
+
248
+ def test_validate_params_out_of_range(self):
249
+ with pytest.raises(ValueError, match="maximum"):
250
+ validate_params("brightness", {"factor": 100.0})
251
+
252
+ def test_validate_params_unknown(self):
253
+ with pytest.raises(ValueError, match="Unknown parameters"):
254
+ validate_params("brightness", {"bogus": 1.0})
255
+
256
+ def test_add_filter(self):
257
+ proj = self._make_project_with_layer()
258
+ result = add_filter(proj, "brightness", 0, {"factor": 1.2})
259
+ assert result["name"] == "brightness"
260
+ assert proj["layers"][0]["filters"][0]["name"] == "brightness"
261
+
262
+ def test_add_filter_invalid_layer(self):
263
+ proj = self._make_project_with_layer()
264
+ with pytest.raises(IndexError):
265
+ add_filter(proj, "brightness", 5, {})
266
+
267
+ def test_add_filter_unknown(self):
268
+ proj = self._make_project_with_layer()
269
+ with pytest.raises(ValueError, match="Unknown filter"):
270
+ add_filter(proj, "nonexistent", 0, {})
271
+
272
+ def test_remove_filter(self):
273
+ proj = self._make_project_with_layer()
274
+ add_filter(proj, "brightness", 0, {"factor": 1.2})
275
+ removed = remove_filter(proj, 0, 0)
276
+ assert removed["name"] == "brightness"
277
+ assert len(proj["layers"][0]["filters"]) == 0
278
+
279
+ def test_set_filter_param(self):
280
+ proj = self._make_project_with_layer()
281
+ add_filter(proj, "brightness", 0, {"factor": 1.0})
282
+ set_filter_param(proj, 0, "factor", 1.5, 0)
283
+ assert proj["layers"][0]["filters"][0]["params"]["factor"] == 1.5
284
+
285
+ def test_list_filters(self):
286
+ proj = self._make_project_with_layer()
287
+ add_filter(proj, "brightness", 0, {"factor": 1.2})
288
+ add_filter(proj, "contrast", 0, {"factor": 1.1})
289
+ result = list_filters(proj, 0)
290
+ assert len(result) == 2
291
+ assert result[0]["name"] == "brightness"
292
+ assert result[1]["name"] == "contrast"
293
+
294
+ def test_all_filters_have_valid_engine(self):
295
+ valid_engines = {"pillow_enhance", "pillow_ops", "pillow_filter",
296
+ "pillow_transform", "custom"}
297
+ for name, spec in FILTER_REGISTRY.items():
298
+ assert spec["engine"] in valid_engines, f"Filter '{name}' has invalid engine"
299
+
300
+
301
+ # ── Canvas Tests ─────────────────────────────────────────────────
302
+
303
+ class TestCanvas:
304
+ def _make_project(self):
305
+ return create_project(width=800, height=600)
306
+
307
+ def test_resize_canvas(self):
308
+ proj = self._make_project()
309
+ result = resize_canvas(proj, 1000, 800)
310
+ assert proj["canvas"]["width"] == 1000
311
+ assert proj["canvas"]["height"] == 800
312
+ assert "old_size" in result
313
+
314
+ def test_resize_canvas_with_anchor(self):
315
+ proj = self._make_project()
316
+ add_layer(proj, name="Test")
317
+ resize_canvas(proj, 1000, 800, anchor="top-left")
318
+ assert proj["layers"][0]["offset_x"] == 0
319
+ assert proj["layers"][0]["offset_y"] == 0
320
+
321
+ def test_resize_canvas_invalid_size(self):
322
+ proj = self._make_project()
323
+ with pytest.raises(ValueError, match="must be positive"):
324
+ resize_canvas(proj, 0, 100)
325
+
326
+ def test_scale_canvas(self):
327
+ proj = self._make_project()
328
+ add_layer(proj, name="Test", width=800, height=600)
329
+ result = scale_canvas(proj, 400, 300)
330
+ assert proj["canvas"]["width"] == 400
331
+ assert proj["canvas"]["height"] == 300
332
+ assert proj["layers"][0]["width"] == 400
333
+ assert proj["layers"][0]["height"] == 300
334
+
335
+ def test_crop_canvas(self):
336
+ proj = self._make_project()
337
+ result = crop_canvas(proj, 100, 100, 500, 400)
338
+ assert proj["canvas"]["width"] == 400
339
+ assert proj["canvas"]["height"] == 300
340
+
341
+ def test_crop_canvas_out_of_bounds(self):
342
+ proj = self._make_project()
343
+ with pytest.raises(ValueError, match="exceeds canvas"):
344
+ crop_canvas(proj, 0, 0, 1000, 1000)
345
+
346
+ def test_crop_canvas_invalid_region(self):
347
+ proj = self._make_project()
348
+ with pytest.raises(ValueError, match="Invalid crop"):
349
+ crop_canvas(proj, 500, 500, 100, 100)
350
+
351
+ def test_set_mode(self):
352
+ proj = self._make_project()
353
+ result = set_mode(proj, "RGBA")
354
+ assert proj["canvas"]["color_mode"] == "RGBA"
355
+ assert result["old_mode"] == "RGB"
356
+
357
+ def test_set_mode_invalid(self):
358
+ proj = self._make_project()
359
+ with pytest.raises(ValueError, match="Invalid color mode"):
360
+ set_mode(proj, "XYZ")
361
+
362
+ def test_set_dpi(self):
363
+ proj = self._make_project()
364
+ result = set_dpi(proj, 300)
365
+ assert proj["canvas"]["dpi"] == 300
366
+
367
+ def test_get_canvas_info(self):
368
+ proj = self._make_project()
369
+ info = get_canvas_info(proj)
370
+ assert info["width"] == 800
371
+ assert info["height"] == 600
372
+ assert "megapixels" in info
373
+
374
+
375
+ # ── Session Tests ────────────────────────────────────────────────
376
+
377
+ class TestSession:
378
+ def test_create_session(self):
379
+ sess = Session()
380
+ assert not sess.has_project()
381
+
382
+ def test_set_project(self):
383
+ sess = Session()
384
+ proj = create_project()
385
+ sess.set_project(proj)
386
+ assert sess.has_project()
387
+
388
+ def test_get_project_no_project(self):
389
+ sess = Session()
390
+ with pytest.raises(RuntimeError, match="No project loaded"):
391
+ sess.get_project()
392
+
393
+ def test_undo_redo(self):
394
+ sess = Session()
395
+ proj = create_project(name="original")
396
+ sess.set_project(proj)
397
+
398
+ sess.snapshot("change name")
399
+ proj["name"] = "modified"
400
+
401
+ assert proj["name"] == "modified"
402
+ sess.undo()
403
+ assert sess.get_project()["name"] == "original"
404
+ sess.redo()
405
+ assert sess.get_project()["name"] == "modified"
406
+
407
+ def test_undo_empty(self):
408
+ sess = Session()
409
+ sess.set_project(create_project())
410
+ with pytest.raises(RuntimeError, match="Nothing to undo"):
411
+ sess.undo()
412
+
413
+ def test_redo_empty(self):
414
+ sess = Session()
415
+ sess.set_project(create_project())
416
+ with pytest.raises(RuntimeError, match="Nothing to redo"):
417
+ sess.redo()
418
+
419
+ def test_snapshot_clears_redo(self):
420
+ sess = Session()
421
+ proj = create_project(name="v1")
422
+ sess.set_project(proj)
423
+
424
+ sess.snapshot("v2")
425
+ proj["name"] = "v2"
426
+
427
+ sess.undo()
428
+ assert sess.get_project()["name"] == "v1"
429
+
430
+ # New snapshot should clear redo stack
431
+ sess.snapshot("v3")
432
+ sess.get_project()["name"] = "v3"
433
+
434
+ with pytest.raises(RuntimeError, match="Nothing to redo"):
435
+ sess.redo()
436
+
437
+ def test_status(self):
438
+ sess = Session()
439
+ proj = create_project(name="test")
440
+ sess.set_project(proj, "/tmp/test.json")
441
+ status = sess.status()
442
+ assert status["has_project"] is True
443
+ assert status["project_path"] == "/tmp/test.json"
444
+ assert status["undo_count"] == 0
445
+
446
+ def test_save_session(self):
447
+ sess = Session()
448
+ proj = create_project(name="save_test")
449
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
450
+ path = f.name
451
+ try:
452
+ sess.set_project(proj, path)
453
+ saved = sess.save_session()
454
+ assert os.path.exists(saved)
455
+ with open(saved) as f:
456
+ loaded = json.load(f)
457
+ assert loaded["name"] == "save_test"
458
+ finally:
459
+ os.unlink(path)
460
+
461
+ def test_list_history(self):
462
+ sess = Session()
463
+ proj = create_project()
464
+ sess.set_project(proj)
465
+ sess.snapshot("action 1")
466
+ sess.snapshot("action 2")
467
+ history = sess.list_history()
468
+ assert len(history) == 2
469
+ assert history[0]["description"] == "action 2"
470
+
471
+ def test_max_undo(self):
472
+ sess = Session()
473
+ sess.MAX_UNDO = 5
474
+ proj = create_project()
475
+ sess.set_project(proj)
476
+ for i in range(10):
477
+ sess.snapshot(f"action {i}")
478
+ assert len(sess._undo_stack) == 5