comfyui-skill-cli 0.2.7__tar.gz → 0.2.8__tar.gz

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 (45) hide show
  1. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/PKG-INFO +4 -2
  2. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/README.md +3 -1
  3. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/PKG-INFO +4 -2
  4. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/SOURCES.txt +3 -4
  5. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/run.py +24 -9
  6. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/workflow.py +10 -1
  7. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/pyproject.toml +1 -1
  8. comfyui_skill_cli-0.2.8/tests/test_client.py +279 -0
  9. comfyui_skill_cli-0.2.8/tests/test_error_hints.py +95 -0
  10. comfyui_skill_cli-0.2.8/tests/test_nodes.py +47 -0
  11. comfyui_skill_cli-0.2.7/tests/test_client_new_apis.py +0 -199
  12. comfyui_skill_cli-0.2.7/tests/test_nodes_and_ws.py +0 -203
  13. comfyui_skill_cli-0.2.7/tests/test_partial_execution.py +0 -56
  14. comfyui_skill_cli-0.2.7/tests/test_server_stats.py +0 -90
  15. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/LICENSE +0 -0
  16. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/dependency_links.txt +0 -0
  17. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/entry_points.txt +0 -0
  18. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/requires.txt +0 -0
  19. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/top_level.txt +0 -0
  20. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/__init__.py +0 -0
  21. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/__main__.py +0 -0
  22. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/client.py +0 -0
  23. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/__init__.py +0 -0
  24. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/cancel.py +0 -0
  25. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/config.py +0 -0
  26. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/deps.py +0 -0
  27. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/free.py +0 -0
  28. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/history.py +0 -0
  29. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/logs.py +0 -0
  30. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/models.py +0 -0
  31. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/nodes.py +0 -0
  32. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/queue.py +0 -0
  33. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/server.py +0 -0
  34. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/skill.py +0 -0
  35. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/templates.py +0 -0
  36. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/upload.py +0 -0
  37. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/config.py +0 -0
  38. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/error_hints.py +0 -0
  39. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/main.py +0 -0
  40. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/output.py +0 -0
  41. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/storage.py +0 -0
  42. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/update_check.py +0 -0
  43. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/utils.py +0 -0
  44. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/setup.cfg +0 -0
  45. {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/tests/test_update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfyui-skill-cli
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: ComfyUI Skill CLI — Agent-friendly workflow management
5
5
  Author: HuangYuChuh
6
6
  License-Expression: MIT
@@ -57,7 +57,9 @@ Dynamic: license-file
57
57
  <strong>English</strong> ·
58
58
  <a href="./README.zh.md">简体中文</a> ·
59
59
  <a href="./README.zh-TW.md">繁體中文</a> ·
60
- <a href="./README.ja.md">日本語</a>
60
+ <a href="./README.ja.md">日本語</a> ·
61
+ <a href="./README.ko.md">한국어</a> ·
62
+ <a href="./README.es.md">Español</a>
61
63
  </p>
62
64
 
63
65
  </div>
@@ -26,7 +26,9 @@
26
26
  <strong>English</strong> ·
27
27
  <a href="./README.zh.md">简体中文</a> ·
28
28
  <a href="./README.zh-TW.md">繁體中文</a> ·
29
- <a href="./README.ja.md">日本語</a>
29
+ <a href="./README.ja.md">日本語</a> ·
30
+ <a href="./README.ko.md">한국어</a> ·
31
+ <a href="./README.es.md">Español</a>
30
32
  </p>
31
33
 
32
34
  </div>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfyui-skill-cli
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: ComfyUI Skill CLI — Agent-friendly workflow management
5
5
  Author: HuangYuChuh
6
6
  License-Expression: MIT
@@ -57,7 +57,9 @@ Dynamic: license-file
57
57
  <strong>English</strong> ·
58
58
  <a href="./README.zh.md">简体中文</a> ·
59
59
  <a href="./README.zh-TW.md">繁體中文</a> ·
60
- <a href="./README.ja.md">日本語</a>
60
+ <a href="./README.ja.md">日本語</a> ·
61
+ <a href="./README.ko.md">한국어</a> ·
62
+ <a href="./README.es.md">Español</a>
61
63
  </p>
62
64
 
63
65
  </div>
@@ -33,8 +33,7 @@ comfyui_skills_cli/commands/skill.py
33
33
  comfyui_skills_cli/commands/templates.py
34
34
  comfyui_skills_cli/commands/upload.py
35
35
  comfyui_skills_cli/commands/workflow.py
36
- tests/test_client_new_apis.py
37
- tests/test_nodes_and_ws.py
38
- tests/test_partial_execution.py
39
- tests/test_server_stats.py
36
+ tests/test_client.py
37
+ tests/test_error_hints.py
38
+ tests/test_nodes.py
40
39
  tests/test_update_check.py
@@ -399,12 +399,25 @@ def _inject_params(
399
399
  return workflow
400
400
 
401
401
 
402
- _MEDIA_KEYS: dict[str, str] = {
403
- "images": "image",
404
- "audio": "audio",
405
- "gifs": "image",
406
- "video": "video",
407
- }
402
+ _MEDIA_KEYS = ("images", "audio")
403
+
404
+ _VIDEO_EXTENSIONS = {".mp4", ".webm", ".mkv", ".avi", ".mov", ".gif"}
405
+ _AUDIO_EXTENSIONS = {".mp3", ".wav", ".flac", ".ogg", ".aac", ".m4a"}
406
+
407
+
408
+ def _infer_media_type(filename: str, fallback: str) -> str:
409
+ """Infer media type from file extension.
410
+
411
+ ComfyUI's SaveVideo/PreviewVideo nodes report video output under
412
+ the ``"images"`` history key, so relying on the key alone would
413
+ misclassify video files as images.
414
+ """
415
+ ext = Path(filename).suffix.lower()
416
+ if ext in _VIDEO_EXTENSIONS:
417
+ return "video"
418
+ if ext in _AUDIO_EXTENSIONS:
419
+ return "audio"
420
+ return fallback
408
421
 
409
422
 
410
423
  def _collect_outputs(outputs: dict[str, Any]) -> list[dict[str, str]]:
@@ -412,13 +425,15 @@ def _collect_outputs(outputs: dict[str, Any]) -> list[dict[str, str]]:
412
425
  for node_output in outputs.values():
413
426
  if not isinstance(node_output, dict):
414
427
  continue
415
- for key, media_type in _MEDIA_KEYS.items():
428
+ for key in _MEDIA_KEYS:
429
+ fallback = "audio" if key == "audio" else "image"
416
430
  for item in node_output.get(key, []):
431
+ filename = item.get("filename", "")
417
432
  collected.append({
418
- "filename": item.get("filename", ""),
433
+ "filename": filename,
419
434
  "subfolder": item.get("subfolder", ""),
420
435
  "type": item.get("type", "output"),
421
- "media_type": media_type,
436
+ "media_type": _infer_media_type(filename, fallback),
422
437
  })
423
438
  return collected
424
439
 
@@ -70,6 +70,14 @@ _MEDIA_TYPE_FIELDS: dict[str, dict[str, dict[str, Any]]] = {
70
70
  "cfg_scale": {"exposed": True, "required": False, "description": "Classifier-free guidance scale"},
71
71
  "temperature": {"exposed": True, "required": False, "description": "Sampling temperature"},
72
72
  },
73
+ "video": {
74
+ "format": {"exposed": True, "required": False, "description": "Output video format"},
75
+ "codec": {"exposed": True, "required": False, "description": "Video codec"},
76
+ "frame_rate": {"exposed": True, "required": False, "description": "Video frame rate"},
77
+ "fps": {"exposed": True, "required": False, "description": "Frames per second"},
78
+ "noise_seed": {"exposed": True, "required": False, "description": "Noise seed for video generation"},
79
+ "cfg": {"exposed": True, "required": False, "description": "Classifier-free guidance scale"},
80
+ },
73
81
  }
74
82
 
75
83
  _LOAD_IMAGE_CLASSES = {"LoadImage", "LoadImageMask"}
@@ -91,6 +99,7 @@ def _extract_schema(workflow_data: dict[str, Any], media_type: str = "image") ->
91
99
  *media_type* selects additional field-exposure rules beyond the base
92
100
  set. ``"image"`` (default) uses only the generic rules.
93
101
  ``"audio"`` adds audio-specific fields like tags, lyrics, bpm, etc.
102
+ ``"video"`` adds video-specific fields like format, codec, fps, etc.
94
103
  """
95
104
  expose_fields = dict(_AUTO_EXPOSE_FIELDS)
96
105
  if media_type in _MEDIA_TYPE_FIELDS:
@@ -384,7 +393,7 @@ def workflow_import(
384
393
  ctx: typer.Context,
385
394
  json_path: str = typer.Argument(None, help="Path to workflow JSON file (omit when using --from-server)"),
386
395
  name: str = typer.Option("", "--name", "-n", help="Workflow ID (default: derived from filename)"),
387
- media_type: str = typer.Option("image", "--type", "-t", help="Media type preset for parameter detection: image (default), audio"),
396
+ media_type: str = typer.Option("image", "--type", "-t", help="Media type preset for parameter detection: image (default), audio, video"),
388
397
  from_server: bool = typer.Option(False, "--from-server", help="Import from ComfyUI server userdata"),
389
398
  preview: bool = typer.Option(False, "--preview", help="Preview only, don't import"),
390
399
  check_deps: bool = typer.Option(False, "--check-deps", help="Check dependencies after import"),
@@ -4,7 +4,7 @@ requires = ["setuptools>=61"]
4
4
 
5
5
  [project]
6
6
  name = "comfyui-skill-cli"
7
- version = "0.2.7"
7
+ version = "0.2.8"
8
8
  description = "ComfyUI Skill CLI — Agent-friendly workflow management"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,279 @@
1
+ """Tests for comfyui_skills_cli.client — all ComfyUIClient methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import unittest
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ from comfyui_skills_cli.client import ComfyUIClient
10
+
11
+
12
+ # -- queue_prompt --
13
+
14
+ class QueuePromptTests(unittest.TestCase):
15
+ def setUp(self) -> None:
16
+ self.client = ComfyUIClient("http://localhost:8188")
17
+
18
+ @patch("comfyui_skills_cli.client.requests.post")
19
+ def test_with_client_id(self, mock_post: MagicMock) -> None:
20
+ mock_post.return_value = MagicMock(status_code=200, json=lambda: {"prompt_id": "p-123"})
21
+ mock_post.return_value.raise_for_status = MagicMock()
22
+ result = self.client.queue_prompt({"1": {}}, client_id="my-client-id")
23
+ self.assertEqual(result["client_id"], "my-client-id")
24
+ self.assertEqual(result["prompt_id"], "p-123")
25
+ self.assertEqual(mock_post.call_args.kwargs["json"]["client_id"], "my-client-id")
26
+
27
+ @patch("comfyui_skills_cli.client.requests.post")
28
+ def test_generates_client_id(self, mock_post: MagicMock) -> None:
29
+ mock_post.return_value = MagicMock(status_code=200, json=lambda: {"prompt_id": "p-456"})
30
+ mock_post.return_value.raise_for_status = MagicMock()
31
+ result = self.client.queue_prompt({"1": {}})
32
+ self.assertIn("client_id", result)
33
+ self.assertTrue(len(result["client_id"]) > 0)
34
+
35
+ @patch("comfyui_skills_cli.client.requests.post")
36
+ def test_with_targets(self, mock_post: MagicMock) -> None:
37
+ mock_post.return_value = MagicMock(status_code=200, json=lambda: {"prompt_id": "p-100"})
38
+ mock_post.return_value.raise_for_status = MagicMock()
39
+ self.client.queue_prompt({"1": {}}, targets=["5", "8"])
40
+ payload = mock_post.call_args.kwargs["json"]
41
+ self.assertEqual(payload["partial_execution_targets"], [["5"], ["8"]])
42
+
43
+ @patch("comfyui_skills_cli.client.requests.post")
44
+ def test_without_targets(self, mock_post: MagicMock) -> None:
45
+ mock_post.return_value = MagicMock(status_code=200, json=lambda: {"prompt_id": "p-101"})
46
+ mock_post.return_value.raise_for_status = MagicMock()
47
+ self.client.queue_prompt({"1": {}}, targets=None)
48
+ payload = mock_post.call_args.kwargs["json"]
49
+ self.assertNotIn("partial_execution_targets", payload)
50
+
51
+ @patch("comfyui_skills_cli.client.requests.post")
52
+ def test_with_empty_targets(self, mock_post: MagicMock) -> None:
53
+ mock_post.return_value = MagicMock(status_code=200, json=lambda: {"prompt_id": "p-102"})
54
+ mock_post.return_value.raise_for_status = MagicMock()
55
+ self.client.queue_prompt({"1": {}}, targets=[])
56
+ payload = mock_post.call_args.kwargs["json"]
57
+ self.assertNotIn("partial_execution_targets", payload)
58
+
59
+
60
+ # -- interrupt / queue management / free --
61
+
62
+ class InterruptTests(unittest.TestCase):
63
+ def setUp(self) -> None:
64
+ self.client = ComfyUIClient("http://localhost:8188")
65
+
66
+ @patch("comfyui_skills_cli.client.requests.post")
67
+ def test_with_prompt_id(self, mock_post: MagicMock) -> None:
68
+ mock_post.return_value = MagicMock(status_code=200)
69
+ result = self.client.interrupt("abc-123")
70
+ self.assertTrue(result["success"])
71
+ self.assertEqual(mock_post.call_args.kwargs["json"], {"prompt_id": "abc-123"})
72
+
73
+ @patch("comfyui_skills_cli.client.requests.post")
74
+ def test_without_prompt_id(self, mock_post: MagicMock) -> None:
75
+ mock_post.return_value = MagicMock(status_code=200)
76
+ result = self.client.interrupt()
77
+ self.assertTrue(result["success"])
78
+ self.assertEqual(mock_post.call_args.kwargs["json"], {})
79
+
80
+
81
+ class QueueManagementTests(unittest.TestCase):
82
+ def setUp(self) -> None:
83
+ self.client = ComfyUIClient("http://localhost:8188")
84
+
85
+ @patch("comfyui_skills_cli.client.requests.post")
86
+ def test_queue_clear(self, mock_post: MagicMock) -> None:
87
+ mock_post.return_value = MagicMock(status_code=200)
88
+ result = self.client.queue_clear()
89
+ self.assertTrue(result["success"])
90
+ self.assertEqual(mock_post.call_args.kwargs["json"], {"clear": True})
91
+
92
+ @patch("comfyui_skills_cli.client.requests.post")
93
+ def test_queue_delete(self, mock_post: MagicMock) -> None:
94
+ mock_post.return_value = MagicMock(status_code=200)
95
+ result = self.client.queue_delete(["id-1", "id-2"])
96
+ self.assertTrue(result["success"])
97
+ self.assertEqual(mock_post.call_args.kwargs["json"], {"delete": ["id-1", "id-2"]})
98
+
99
+
100
+ class FreeMemoryTests(unittest.TestCase):
101
+ def setUp(self) -> None:
102
+ self.client = ComfyUIClient("http://localhost:8188")
103
+
104
+ @patch("comfyui_skills_cli.client.requests.post")
105
+ def test_free_both(self, mock_post: MagicMock) -> None:
106
+ mock_post.return_value = MagicMock(status_code=200)
107
+ result = self.client.free_memory(unload_models=True, free_memory=True)
108
+ self.assertTrue(result["success"])
109
+ self.assertEqual(mock_post.call_args.kwargs["json"], {"unload_models": True, "free_memory": True})
110
+
111
+ @patch("comfyui_skills_cli.client.requests.post")
112
+ def test_free_models_only(self, mock_post: MagicMock) -> None:
113
+ mock_post.return_value = MagicMock(status_code=200)
114
+ self.client.free_memory(unload_models=True, free_memory=False)
115
+ self.assertEqual(mock_post.call_args.kwargs["json"], {"unload_models": True})
116
+
117
+ @patch("comfyui_skills_cli.client.requests.post")
118
+ def test_free_memory_only(self, mock_post: MagicMock) -> None:
119
+ mock_post.return_value = MagicMock(status_code=200)
120
+ self.client.free_memory(unload_models=False, free_memory=True)
121
+ self.assertEqual(mock_post.call_args.kwargs["json"], {"free_memory": True})
122
+
123
+ @patch("comfyui_skills_cli.client.requests.post")
124
+ def test_free_no_flags_sends_empty(self, mock_post: MagicMock) -> None:
125
+ mock_post.return_value = MagicMock(status_code=200)
126
+ self.client.free_memory(unload_models=False, free_memory=False)
127
+ self.assertEqual(mock_post.call_args.kwargs["json"], {})
128
+
129
+
130
+ # -- upload --
131
+
132
+ class UploadFileTests(unittest.TestCase):
133
+ def setUp(self) -> None:
134
+ self.client = ComfyUIClient("http://localhost:8188")
135
+
136
+ @patch("comfyui_skills_cli.client.requests.post")
137
+ def test_upload_file_calls_upload_image_endpoint(self, mock_post: MagicMock) -> None:
138
+ mock_post.return_value = MagicMock(status_code=200)
139
+ mock_post.return_value.json.return_value = {"name": "test.png", "subfolder": "", "type": "input"}
140
+
141
+ import tempfile, os
142
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
143
+ f.write(b"fake png data")
144
+ tmp_path = f.name
145
+
146
+ try:
147
+ result = self.client.upload_file(tmp_path)
148
+ self.assertEqual(result["name"], "test.png")
149
+ self.assertIn("/upload/image", mock_post.call_args.args[0])
150
+ finally:
151
+ os.unlink(tmp_path)
152
+
153
+ def test_upload_image_delegates_to_upload_file(self) -> None:
154
+ with patch.object(self.client, "upload_file", return_value={"name": "x.png"}) as mock:
155
+ result = self.client.upload_image("/fake/path.png")
156
+ mock.assert_called_once_with("/fake/path.png")
157
+ self.assertEqual(result["name"], "x.png")
158
+
159
+
160
+ # -- object_info --
161
+
162
+ class ObjectInfoNodeTests(unittest.TestCase):
163
+ def setUp(self) -> None:
164
+ self.client = ComfyUIClient("http://localhost:8188")
165
+
166
+ @patch("comfyui_skills_cli.client.requests.get")
167
+ def test_found(self, mock_get: MagicMock) -> None:
168
+ mock_get.return_value = MagicMock(
169
+ status_code=200,
170
+ json=lambda: {"KSampler": {"display_name": "KSampler", "category": "sampling"}},
171
+ )
172
+ result = self.client.get_object_info_node("KSampler")
173
+ self.assertIsNotNone(result)
174
+ self.assertEqual(result["display_name"], "KSampler")
175
+
176
+ @patch("comfyui_skills_cli.client.requests.get")
177
+ def test_not_found(self, mock_get: MagicMock) -> None:
178
+ mock_get.return_value = MagicMock(status_code=404)
179
+ result = self.client.get_object_info_node("NonExistentNode")
180
+ self.assertIsNone(result)
181
+
182
+
183
+ # -- system_stats --
184
+
185
+ SAMPLE_STATS = {
186
+ "system": {
187
+ "os": "posix", "ram_total": 68719476736, "ram_free": 32000000000,
188
+ "comfyui_version": "v0.3.10", "python_version": "3.11.5",
189
+ "pytorch_version": "2.1.0", "embedded_python": False,
190
+ },
191
+ "devices": [{
192
+ "name": "cuda:0", "type": "cuda", "index": 0,
193
+ "vram_total": 25769803776, "vram_free": 20000000000,
194
+ "torch_vram_total": 25769803776, "torch_vram_free": 22000000000,
195
+ }],
196
+ }
197
+
198
+
199
+ class SystemStatsTests(unittest.TestCase):
200
+ def setUp(self) -> None:
201
+ self.client = ComfyUIClient("http://localhost:8188")
202
+
203
+ @patch("comfyui_skills_cli.client.requests.get")
204
+ def test_success(self, mock_get: MagicMock) -> None:
205
+ mock_get.return_value = MagicMock(status_code=200, json=lambda: SAMPLE_STATS)
206
+ result = self.client.get_system_stats()
207
+ self.assertEqual(result, SAMPLE_STATS)
208
+
209
+ @patch("comfyui_skills_cli.client.requests.get")
210
+ def test_raises_on_error(self, mock_get: MagicMock) -> None:
211
+ mock_resp = MagicMock(status_code=500)
212
+ mock_resp.raise_for_status.side_effect = Exception("Server error")
213
+ mock_get.return_value = mock_resp
214
+ with self.assertRaises(Exception):
215
+ self.client.get_system_stats()
216
+
217
+ @patch("comfyui_skills_cli.client.requests.get")
218
+ def test_multi_server(self, mock_get: MagicMock) -> None:
219
+ mock_get.return_value = MagicMock(status_code=200, json=lambda: SAMPLE_STATS)
220
+ clients = [ComfyUIClient("http://s1:8188"), ComfyUIClient("http://s2:8188")]
221
+ results = [c.get_system_stats() for c in clients]
222
+ self.assertEqual(len(results), 2)
223
+ self.assertEqual(mock_get.call_count, 2)
224
+
225
+
226
+ # -- ws_events --
227
+
228
+ class WsEventsTests(unittest.TestCase):
229
+ def setUp(self) -> None:
230
+ self.client = ComfyUIClient("http://localhost:8188")
231
+
232
+ @patch("websocket.create_connection")
233
+ def test_yields_matching_events(self, mock_ws_create: MagicMock) -> None:
234
+ import websocket
235
+ mock_ws = MagicMock()
236
+ mock_ws_create.return_value = mock_ws
237
+ events = [
238
+ (websocket.ABNF.OPCODE_TEXT, json.dumps({"type": "execution_start", "data": {"prompt_id": "p-1"}}).encode()),
239
+ (websocket.ABNF.OPCODE_TEXT, json.dumps({"type": "executing", "data": {"node": "5", "prompt_id": "p-1"}}).encode()),
240
+ (websocket.ABNF.OPCODE_BINARY, b"\x01\x00\x00\x00fake-preview"),
241
+ (websocket.ABNF.OPCODE_TEXT, json.dumps({"type": "executing", "data": {"node": None, "prompt_id": "p-1"}}).encode()),
242
+ ]
243
+ mock_ws.recv_data = MagicMock(side_effect=events)
244
+ collected = list(self.client.ws_events("cid-1", "p-1"))
245
+ self.assertEqual(len(collected), 3)
246
+ self.assertEqual(collected[0]["type"], "execution_start")
247
+ self.assertEqual(collected[1]["data"]["node"], "5")
248
+ self.assertIsNone(collected[2]["data"]["node"])
249
+ mock_ws.close.assert_called_once()
250
+
251
+ @patch("websocket.create_connection")
252
+ def test_filters_other_prompts(self, mock_ws_create: MagicMock) -> None:
253
+ import websocket
254
+ mock_ws = MagicMock()
255
+ mock_ws_create.return_value = mock_ws
256
+ events = [
257
+ (websocket.ABNF.OPCODE_TEXT, json.dumps({"type": "executing", "data": {"node": "1", "prompt_id": "other"}}).encode()),
258
+ (websocket.ABNF.OPCODE_TEXT, json.dumps({"type": "executing", "data": {"node": None, "prompt_id": "p-1"}}).encode()),
259
+ ]
260
+ mock_ws.recv_data = MagicMock(side_effect=events)
261
+ collected = list(self.client.ws_events("cid-1", "p-1"))
262
+ self.assertEqual(len(collected), 1)
263
+
264
+ @patch("websocket.create_connection")
265
+ def test_stops_on_error(self, mock_ws_create: MagicMock) -> None:
266
+ import websocket
267
+ mock_ws = MagicMock()
268
+ mock_ws_create.return_value = mock_ws
269
+ events = [
270
+ (websocket.ABNF.OPCODE_TEXT, json.dumps({"type": "execution_error", "data": {"prompt_id": "p-1", "exception_message": "boom"}}).encode()),
271
+ ]
272
+ mock_ws.recv_data = MagicMock(side_effect=events)
273
+ collected = list(self.client.ws_events("cid-1", "p-1"))
274
+ self.assertEqual(len(collected), 1)
275
+ self.assertEqual(collected[0]["type"], "execution_error")
276
+
277
+
278
+ if __name__ == "__main__":
279
+ unittest.main()
@@ -0,0 +1,95 @@
1
+ """Tests for comfyui_skills_cli.error_hints — all error pattern matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import unittest
6
+
7
+ from comfyui_skills_cli.error_hints import match_error_hint
8
+
9
+
10
+ class ErrorHintTests(unittest.TestCase):
11
+ # -- Cloud API --
12
+ def test_unauthorized(self) -> None:
13
+ hint = match_error_hint("Unauthorized: Please login first to use this node.")
14
+ self.assertIn("ComfyUI API Key", hint)
15
+
16
+ # -- Missing models (specific) --
17
+ def test_vae_not_found(self) -> None:
18
+ hint = match_error_hint("Error: vae model not found in models/vae/")
19
+ self.assertIn("VAE", hint)
20
+ self.assertIn("deps check", hint)
21
+
22
+ def test_clip_not_found(self) -> None:
23
+ hint = match_error_hint("clip_vision.safetensors: No such file or directory")
24
+ self.assertIn("CLIP", hint)
25
+
26
+ def test_lora_not_found(self) -> None:
27
+ hint = match_error_hint("LoRA model detail_tweaker.safetensors not found")
28
+ self.assertIn("LoRA", hint)
29
+
30
+ # -- Missing models (generic) --
31
+ def test_ckpt(self) -> None:
32
+ hint = match_error_hint("FileNotFoundError: model_v1.ckpt not found")
33
+ self.assertIn("deps check", hint)
34
+
35
+ def test_safetensors(self) -> None:
36
+ hint = match_error_hint("Could not load sdxl_base.safetensors")
37
+ self.assertIn("deps check", hint)
38
+
39
+ # -- Custom nodes --
40
+ def test_class_type_not_found(self) -> None:
41
+ hint = match_error_hint("class_type not found: IPAdapterApply")
42
+ self.assertIn("custom node", hint)
43
+
44
+ def test_cannot_find_class(self) -> None:
45
+ hint = match_error_hint("Cannot find class for node type 'ControlNetApply'")
46
+ self.assertIn("custom node", hint)
47
+
48
+ # -- Validation --
49
+ def test_invalid_prompt(self) -> None:
50
+ hint = match_error_hint("Error: prompt is not valid")
51
+ self.assertIn("--validate", hint)
52
+
53
+ # -- Connection --
54
+ def test_connection_refused(self) -> None:
55
+ hint = match_error_hint("ConnectionError: Connection refused")
56
+ self.assertIn("not running", hint)
57
+
58
+ def test_connection_timeout(self) -> None:
59
+ hint = match_error_hint("ConnectionError: Connection timed out")
60
+ self.assertIn("timed out", hint)
61
+
62
+ def test_read_timeout(self) -> None:
63
+ hint = match_error_hint("requests.exceptions.ReadTimeout: timeout")
64
+ self.assertIn("timed out", hint)
65
+
66
+ # -- GPU / memory --
67
+ def test_cuda_oom(self) -> None:
68
+ hint = match_error_hint("RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB")
69
+ self.assertIn("comfyui-skill free", hint)
70
+
71
+ def test_mps_oom(self) -> None:
72
+ hint = match_error_hint("RuntimeError: MPS out of memory")
73
+ self.assertIn("comfyui-skill free", hint)
74
+
75
+ def test_cuda_driver_error(self) -> None:
76
+ hint = match_error_hint("RuntimeError: CUDA error: no kernel image is available")
77
+ self.assertIn("GPU driver", hint)
78
+
79
+ def test_no_cuda_gpus(self) -> None:
80
+ hint = match_error_hint("AssertionError: no CUDA GPUs are available")
81
+ self.assertIn("GPU driver", hint)
82
+
83
+ # -- General file --
84
+ def test_file_not_found(self) -> None:
85
+ hint = match_error_hint("FileNotFoundError: [Errno 2] No such file or directory: 'input.png'")
86
+ self.assertIn("required file is missing", hint)
87
+
88
+ # -- No match --
89
+ def test_unknown_error(self) -> None:
90
+ hint = match_error_hint("Some random error message")
91
+ self.assertEqual(hint, "")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ unittest.main()
@@ -0,0 +1,47 @@
1
+ """Tests for comfyui_skills_cli.commands.nodes — helper functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import unittest
6
+
7
+ from comfyui_skills_cli.commands.nodes import _flatten_nodes
8
+
9
+ SAMPLE_OBJECT_INFO = {
10
+ "KSampler": {
11
+ "display_name": "KSampler",
12
+ "description": "Samples latents",
13
+ "category": "sampling",
14
+ },
15
+ "CLIPTextEncode": {
16
+ "display_name": "CLIP Text Encode",
17
+ "description": "",
18
+ "category": "conditioning",
19
+ },
20
+ }
21
+
22
+
23
+ class FlattenNodesTests(unittest.TestCase):
24
+ def test_all(self) -> None:
25
+ rows = _flatten_nodes(SAMPLE_OBJECT_INFO)
26
+ self.assertEqual(len(rows), 2)
27
+ self.assertEqual(rows[0]["category"], "conditioning")
28
+ self.assertEqual(rows[1]["category"], "sampling")
29
+
30
+ def test_with_category_filter(self) -> None:
31
+ rows = _flatten_nodes(SAMPLE_OBJECT_INFO, "sampling")
32
+ self.assertEqual(len(rows), 1)
33
+ self.assertEqual(rows[0]["class_type"], "KSampler")
34
+
35
+ def test_nonexistent_category(self) -> None:
36
+ rows = _flatten_nodes(SAMPLE_OBJECT_INFO, "nonexistent")
37
+ self.assertEqual(len(rows), 0)
38
+
39
+
40
+ class WsAvailableTests(unittest.TestCase):
41
+ def test_returns_bool(self) -> None:
42
+ from comfyui_skills_cli.commands.run import _ws_available
43
+ self.assertIsInstance(_ws_available(), bool)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ unittest.main()
@@ -1,199 +0,0 @@
1
- """Tests for new client methods and error hint matching."""
2
-
3
- from __future__ import annotations
4
-
5
- import unittest
6
- from unittest.mock import MagicMock, patch
7
-
8
- from comfyui_skills_cli.client import ComfyUIClient
9
- from comfyui_skills_cli.error_hints import match_error_hint
10
-
11
-
12
- class InterruptTests(unittest.TestCase):
13
- def setUp(self) -> None:
14
- self.client = ComfyUIClient("http://localhost:8188")
15
-
16
- @patch("comfyui_skills_cli.client.requests.post")
17
- def test_interrupt_with_prompt_id(self, mock_post: MagicMock) -> None:
18
- mock_post.return_value = MagicMock(status_code=200)
19
- result = self.client.interrupt("abc-123")
20
- self.assertTrue(result["success"])
21
- call_kwargs = mock_post.call_args
22
- self.assertIn("/interrupt", call_kwargs.args[0])
23
- self.assertEqual(call_kwargs.kwargs["json"], {"prompt_id": "abc-123"})
24
-
25
- @patch("comfyui_skills_cli.client.requests.post")
26
- def test_interrupt_without_prompt_id(self, mock_post: MagicMock) -> None:
27
- mock_post.return_value = MagicMock(status_code=200)
28
- result = self.client.interrupt()
29
- self.assertTrue(result["success"])
30
- call_kwargs = mock_post.call_args
31
- self.assertEqual(call_kwargs.kwargs["json"], {})
32
-
33
-
34
- class QueueManagementTests(unittest.TestCase):
35
- def setUp(self) -> None:
36
- self.client = ComfyUIClient("http://localhost:8188")
37
-
38
- @patch("comfyui_skills_cli.client.requests.post")
39
- def test_queue_clear(self, mock_post: MagicMock) -> None:
40
- mock_post.return_value = MagicMock(status_code=200)
41
- result = self.client.queue_clear()
42
- self.assertTrue(result["success"])
43
- call_kwargs = mock_post.call_args
44
- self.assertEqual(call_kwargs.kwargs["json"], {"clear": True})
45
-
46
- @patch("comfyui_skills_cli.client.requests.post")
47
- def test_queue_delete(self, mock_post: MagicMock) -> None:
48
- mock_post.return_value = MagicMock(status_code=200)
49
- result = self.client.queue_delete(["id-1", "id-2"])
50
- self.assertTrue(result["success"])
51
- call_kwargs = mock_post.call_args
52
- self.assertEqual(call_kwargs.kwargs["json"], {"delete": ["id-1", "id-2"]})
53
-
54
-
55
- class FreeMemoryTests(unittest.TestCase):
56
- def setUp(self) -> None:
57
- self.client = ComfyUIClient("http://localhost:8188")
58
-
59
- @patch("comfyui_skills_cli.client.requests.post")
60
- def test_free_both(self, mock_post: MagicMock) -> None:
61
- mock_post.return_value = MagicMock(status_code=200)
62
- result = self.client.free_memory(unload_models=True, free_memory=True)
63
- self.assertTrue(result["success"])
64
- call_kwargs = mock_post.call_args
65
- self.assertEqual(call_kwargs.kwargs["json"], {"unload_models": True, "free_memory": True})
66
-
67
- @patch("comfyui_skills_cli.client.requests.post")
68
- def test_free_models_only(self, mock_post: MagicMock) -> None:
69
- mock_post.return_value = MagicMock(status_code=200)
70
- self.client.free_memory(unload_models=True, free_memory=False)
71
- call_kwargs = mock_post.call_args
72
- self.assertEqual(call_kwargs.kwargs["json"], {"unload_models": True})
73
-
74
- @patch("comfyui_skills_cli.client.requests.post")
75
- def test_free_memory_only(self, mock_post: MagicMock) -> None:
76
- mock_post.return_value = MagicMock(status_code=200)
77
- self.client.free_memory(unload_models=False, free_memory=True)
78
- call_kwargs = mock_post.call_args
79
- self.assertEqual(call_kwargs.kwargs["json"], {"free_memory": True})
80
-
81
- @patch("comfyui_skills_cli.client.requests.post")
82
- def test_free_no_flags_sends_empty(self, mock_post: MagicMock) -> None:
83
- mock_post.return_value = MagicMock(status_code=200)
84
- self.client.free_memory(unload_models=False, free_memory=False)
85
- call_kwargs = mock_post.call_args
86
- self.assertEqual(call_kwargs.kwargs["json"], {})
87
-
88
-
89
- class UploadFileTests(unittest.TestCase):
90
- def setUp(self) -> None:
91
- self.client = ComfyUIClient("http://localhost:8188")
92
-
93
- @patch("comfyui_skills_cli.client.requests.post")
94
- def test_upload_file_calls_upload_image_endpoint(self, mock_post: MagicMock) -> None:
95
- mock_post.return_value = MagicMock(status_code=200)
96
- mock_post.return_value.json.return_value = {"name": "test.png", "subfolder": "", "type": "input"}
97
-
98
- import tempfile, os
99
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
100
- f.write(b"fake png data")
101
- tmp_path = f.name
102
-
103
- try:
104
- result = self.client.upload_file(tmp_path)
105
- self.assertEqual(result["name"], "test.png")
106
- call_args = mock_post.call_args
107
- self.assertIn("/upload/image", call_args.args[0])
108
- finally:
109
- os.unlink(tmp_path)
110
-
111
- def test_upload_image_delegates_to_upload_file(self) -> None:
112
- with patch.object(self.client, "upload_file", return_value={"name": "x.png"}) as mock:
113
- result = self.client.upload_image("/fake/path.png")
114
- mock.assert_called_once_with("/fake/path.png")
115
- self.assertEqual(result["name"], "x.png")
116
-
117
-
118
- class ErrorHintTests(unittest.TestCase):
119
- def test_unauthorized_hint(self) -> None:
120
- hint = match_error_hint("Unauthorized: Please login first to use this node.")
121
- self.assertIn("ComfyUI API Key", hint)
122
- self.assertIn("platform.comfy.org", hint)
123
-
124
- def test_cuda_oom_hint(self) -> None:
125
- hint = match_error_hint("RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB")
126
- self.assertIn("comfyui-skill free", hint)
127
-
128
- def test_mps_oom_hint(self) -> None:
129
- hint = match_error_hint("RuntimeError: MPS out of memory")
130
- self.assertIn("comfyui-skill free", hint)
131
-
132
- def test_connection_refused_hint(self) -> None:
133
- hint = match_error_hint("ConnectionError: Connection refused")
134
- self.assertIn("not running", hint)
135
-
136
- def test_missing_model_ckpt_hint(self) -> None:
137
- hint = match_error_hint("FileNotFoundError: model_v1.ckpt not found")
138
- self.assertIn("deps check", hint)
139
-
140
- def test_missing_model_safetensors_hint(self) -> None:
141
- hint = match_error_hint("Could not load sdxl_base.safetensors")
142
- self.assertIn("deps check", hint)
143
-
144
- def test_vae_not_found_hint(self) -> None:
145
- hint = match_error_hint("Error: vae model not found in models/vae/")
146
- self.assertIn("VAE", hint)
147
- self.assertIn("deps check", hint)
148
-
149
- def test_clip_not_found_hint(self) -> None:
150
- hint = match_error_hint("clip_vision.safetensors: No such file or directory")
151
- self.assertIn("CLIP", hint)
152
- self.assertIn("deps check", hint)
153
-
154
- def test_lora_not_found_hint(self) -> None:
155
- hint = match_error_hint("LoRA model detail_tweaker.safetensors not found")
156
- self.assertIn("LoRA", hint)
157
- self.assertIn("deps check", hint)
158
-
159
- def test_custom_node_not_installed_hint(self) -> None:
160
- hint = match_error_hint("class_type not found: IPAdapterApply")
161
- self.assertIn("custom node", hint)
162
- self.assertIn("deps check", hint)
163
-
164
- def test_custom_node_cannot_find_class_hint(self) -> None:
165
- hint = match_error_hint("Cannot find class for node type 'ControlNetApply'")
166
- self.assertIn("custom node", hint)
167
-
168
- def test_connection_timeout_hint(self) -> None:
169
- hint = match_error_hint("ConnectionError: Connection timed out")
170
- self.assertIn("timed out", hint)
171
-
172
- def test_timeout_hint(self) -> None:
173
- hint = match_error_hint("requests.exceptions.ReadTimeout: timeout")
174
- self.assertIn("timed out", hint)
175
-
176
- def test_invalid_prompt_hint(self) -> None:
177
- hint = match_error_hint("Error: prompt is not valid")
178
- self.assertIn("validation errors", hint)
179
- self.assertIn("--validate", hint)
180
-
181
- def test_file_not_found_error_hint(self) -> None:
182
- hint = match_error_hint("FileNotFoundError: [Errno 2] No such file or directory: 'input.png'")
183
- self.assertIn("required file is missing", hint)
184
-
185
- def test_cuda_error_hint(self) -> None:
186
- hint = match_error_hint("RuntimeError: CUDA error: no kernel image is available")
187
- self.assertIn("GPU driver", hint)
188
-
189
- def test_no_cuda_gpus_hint(self) -> None:
190
- hint = match_error_hint("AssertionError: no CUDA GPUs are available")
191
- self.assertIn("GPU driver", hint)
192
-
193
- def test_no_hint_for_unknown_error(self) -> None:
194
- hint = match_error_hint("Some random error message")
195
- self.assertEqual(hint, "")
196
-
197
-
198
- if __name__ == "__main__":
199
- unittest.main()
@@ -1,203 +0,0 @@
1
- """Tests for nodes command helpers and WebSocket client methods."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import unittest
7
- from unittest.mock import MagicMock, patch
8
-
9
- from comfyui_skills_cli.client import ComfyUIClient
10
-
11
-
12
- SAMPLE_OBJECT_INFO = {
13
- "KSampler": {
14
- "input": {
15
- "required": {
16
- "model": ["MODEL"],
17
- "seed": ["INT", {"default": 0, "min": 0, "max": 2**64 - 1}],
18
- "sampler_name": [["euler", "euler_ancestral", "heun"]],
19
- },
20
- "optional": {},
21
- },
22
- "input_order": {"required": ["model", "seed", "sampler_name"], "optional": []},
23
- "output": ["LATENT"],
24
- "output_is_list": [False],
25
- "output_name": ["LATENT"],
26
- "name": "KSampler",
27
- "display_name": "KSampler",
28
- "description": "Samples latents",
29
- "category": "sampling",
30
- },
31
- "CLIPTextEncode": {
32
- "input": {"required": {"text": ["STRING"], "clip": ["CLIP"]}, "optional": {}},
33
- "input_order": {"required": ["text", "clip"], "optional": []},
34
- "output": ["CONDITIONING"],
35
- "output_is_list": [False],
36
- "output_name": ["CONDITIONING"],
37
- "name": "CLIPTextEncode",
38
- "display_name": "CLIP Text Encode",
39
- "description": "",
40
- "category": "conditioning",
41
- },
42
- }
43
-
44
-
45
- class ObjectInfoNodeTests(unittest.TestCase):
46
- def setUp(self) -> None:
47
- self.client = ComfyUIClient("http://localhost:8188")
48
-
49
- @patch("comfyui_skills_cli.client.requests.get")
50
- def test_get_object_info_node_found(self, mock_get: MagicMock) -> None:
51
- mock_get.return_value = MagicMock(
52
- status_code=200,
53
- json=lambda: {"KSampler": SAMPLE_OBJECT_INFO["KSampler"]},
54
- )
55
- result = self.client.get_object_info_node("KSampler")
56
- self.assertIsNotNone(result)
57
- self.assertEqual(result["display_name"], "KSampler")
58
-
59
- @patch("comfyui_skills_cli.client.requests.get")
60
- def test_get_object_info_node_not_found(self, mock_get: MagicMock) -> None:
61
- mock_get.return_value = MagicMock(status_code=404)
62
- result = self.client.get_object_info_node("NonExistentNode")
63
- self.assertIsNone(result)
64
-
65
-
66
- class QueuePromptClientIdTests(unittest.TestCase):
67
- def setUp(self) -> None:
68
- self.client = ComfyUIClient("http://localhost:8188")
69
-
70
- @patch("comfyui_skills_cli.client.requests.post")
71
- def test_queue_prompt_with_client_id(self, mock_post: MagicMock) -> None:
72
- mock_post.return_value = MagicMock(
73
- status_code=200,
74
- json=lambda: {"prompt_id": "p-123"},
75
- )
76
- mock_post.return_value.raise_for_status = MagicMock()
77
- result = self.client.queue_prompt({"1": {}}, client_id="my-client-id")
78
- self.assertEqual(result["client_id"], "my-client-id")
79
- self.assertEqual(result["prompt_id"], "p-123")
80
- payload = mock_post.call_args.kwargs["json"]
81
- self.assertEqual(payload["client_id"], "my-client-id")
82
-
83
- @patch("comfyui_skills_cli.client.requests.post")
84
- def test_queue_prompt_generates_client_id(self, mock_post: MagicMock) -> None:
85
- mock_post.return_value = MagicMock(
86
- status_code=200,
87
- json=lambda: {"prompt_id": "p-456"},
88
- )
89
- mock_post.return_value.raise_for_status = MagicMock()
90
- result = self.client.queue_prompt({"1": {}})
91
- self.assertIn("client_id", result)
92
- self.assertTrue(len(result["client_id"]) > 0)
93
-
94
-
95
- class WsEventsTests(unittest.TestCase):
96
- def setUp(self) -> None:
97
- self.client = ComfyUIClient("http://localhost:8188")
98
-
99
- @patch("websocket.create_connection")
100
- def test_ws_events_yields_matching_events(self, mock_ws_create: MagicMock) -> None:
101
- import websocket
102
-
103
- mock_ws = MagicMock()
104
- mock_ws_create.return_value = mock_ws
105
-
106
- events = [
107
- (websocket.ABNF.OPCODE_TEXT, json.dumps({
108
- "type": "execution_start",
109
- "data": {"prompt_id": "p-1"},
110
- }).encode()),
111
- (websocket.ABNF.OPCODE_TEXT, json.dumps({
112
- "type": "executing",
113
- "data": {"node": "5", "prompt_id": "p-1"},
114
- }).encode()),
115
- (websocket.ABNF.OPCODE_BINARY, b"\x01\x00\x00\x00fake-preview"),
116
- (websocket.ABNF.OPCODE_TEXT, json.dumps({
117
- "type": "executing",
118
- "data": {"node": None, "prompt_id": "p-1"},
119
- }).encode()),
120
- ]
121
- mock_ws.recv_data = MagicMock(side_effect=events)
122
-
123
- collected = list(self.client.ws_events("cid-1", "p-1"))
124
- self.assertEqual(len(collected), 3)
125
- self.assertEqual(collected[0]["type"], "execution_start")
126
- self.assertEqual(collected[1]["type"], "executing")
127
- self.assertEqual(collected[1]["data"]["node"], "5")
128
- self.assertEqual(collected[2]["type"], "executing")
129
- self.assertIsNone(collected[2]["data"]["node"])
130
- mock_ws.close.assert_called_once()
131
-
132
- @patch("websocket.create_connection")
133
- def test_ws_events_filters_other_prompts(self, mock_ws_create: MagicMock) -> None:
134
- import websocket
135
-
136
- mock_ws = MagicMock()
137
- mock_ws_create.return_value = mock_ws
138
-
139
- events = [
140
- (websocket.ABNF.OPCODE_TEXT, json.dumps({
141
- "type": "executing",
142
- "data": {"node": "1", "prompt_id": "other-prompt"},
143
- }).encode()),
144
- (websocket.ABNF.OPCODE_TEXT, json.dumps({
145
- "type": "executing",
146
- "data": {"node": None, "prompt_id": "p-1"},
147
- }).encode()),
148
- ]
149
- mock_ws.recv_data = MagicMock(side_effect=events)
150
-
151
- collected = list(self.client.ws_events("cid-1", "p-1"))
152
- self.assertEqual(len(collected), 1)
153
- self.assertIsNone(collected[0]["data"]["node"])
154
-
155
- @patch("websocket.create_connection")
156
- def test_ws_events_stops_on_error(self, mock_ws_create: MagicMock) -> None:
157
- import websocket
158
-
159
- mock_ws = MagicMock()
160
- mock_ws_create.return_value = mock_ws
161
-
162
- events = [
163
- (websocket.ABNF.OPCODE_TEXT, json.dumps({
164
- "type": "execution_error",
165
- "data": {"prompt_id": "p-1", "exception_message": "boom"},
166
- }).encode()),
167
- ]
168
- mock_ws.recv_data = MagicMock(side_effect=events)
169
-
170
- collected = list(self.client.ws_events("cid-1", "p-1"))
171
- self.assertEqual(len(collected), 1)
172
- self.assertEqual(collected[0]["type"], "execution_error")
173
-
174
-
175
- class NodesFlattenTests(unittest.TestCase):
176
- def test_flatten_all(self) -> None:
177
- from comfyui_skills_cli.commands.nodes import _flatten_nodes
178
- rows = _flatten_nodes(SAMPLE_OBJECT_INFO)
179
- self.assertEqual(len(rows), 2)
180
- self.assertEqual(rows[0]["category"], "conditioning")
181
- self.assertEqual(rows[1]["category"], "sampling")
182
-
183
- def test_flatten_with_category_filter(self) -> None:
184
- from comfyui_skills_cli.commands.nodes import _flatten_nodes
185
- rows = _flatten_nodes(SAMPLE_OBJECT_INFO, "sampling")
186
- self.assertEqual(len(rows), 1)
187
- self.assertEqual(rows[0]["class_type"], "KSampler")
188
-
189
- def test_flatten_with_nonexistent_category(self) -> None:
190
- from comfyui_skills_cli.commands.nodes import _flatten_nodes
191
- rows = _flatten_nodes(SAMPLE_OBJECT_INFO, "nonexistent")
192
- self.assertEqual(len(rows), 0)
193
-
194
-
195
- class WsAvailableTests(unittest.TestCase):
196
- def test_ws_available_true(self) -> None:
197
- from comfyui_skills_cli.commands.run import _ws_available
198
- result = _ws_available()
199
- self.assertIsInstance(result, bool)
200
-
201
-
202
- if __name__ == "__main__":
203
- unittest.main()
@@ -1,56 +0,0 @@
1
- """Tests for partial execution targets in queue_prompt."""
2
-
3
- from __future__ import annotations
4
-
5
- import unittest
6
- from unittest.mock import MagicMock, patch
7
-
8
- from comfyui_skills_cli.client import ComfyUIClient
9
-
10
-
11
- class PartialExecutionTests(unittest.TestCase):
12
- def setUp(self) -> None:
13
- self.client = ComfyUIClient("http://localhost:8188")
14
-
15
- @patch("comfyui_skills_cli.client.requests.post")
16
- def test_queue_prompt_with_targets(self, mock_post: MagicMock) -> None:
17
- mock_post.return_value = MagicMock(
18
- status_code=200,
19
- json=lambda: {"prompt_id": "p-100"},
20
- )
21
- mock_post.return_value.raise_for_status = MagicMock()
22
-
23
- self.client.queue_prompt({"1": {}}, targets=["5", "8"])
24
-
25
- payload = mock_post.call_args.kwargs["json"]
26
- self.assertEqual(payload["partial_execution_targets"], [["5"], ["8"]])
27
-
28
- @patch("comfyui_skills_cli.client.requests.post")
29
- def test_queue_prompt_without_targets(self, mock_post: MagicMock) -> None:
30
- mock_post.return_value = MagicMock(
31
- status_code=200,
32
- json=lambda: {"prompt_id": "p-101"},
33
- )
34
- mock_post.return_value.raise_for_status = MagicMock()
35
-
36
- self.client.queue_prompt({"1": {}}, targets=None)
37
-
38
- payload = mock_post.call_args.kwargs["json"]
39
- self.assertNotIn("partial_execution_targets", payload)
40
-
41
- @patch("comfyui_skills_cli.client.requests.post")
42
- def test_queue_prompt_with_empty_targets(self, mock_post: MagicMock) -> None:
43
- mock_post.return_value = MagicMock(
44
- status_code=200,
45
- json=lambda: {"prompt_id": "p-102"},
46
- )
47
- mock_post.return_value.raise_for_status = MagicMock()
48
-
49
- self.client.queue_prompt({"1": {}}, targets=[])
50
-
51
- payload = mock_post.call_args.kwargs["json"]
52
- self.assertNotIn("partial_execution_targets", payload)
53
-
54
-
55
- if __name__ == "__main__":
56
- unittest.main()
@@ -1,90 +0,0 @@
1
- """Tests for the server stats command and client method."""
2
-
3
- from __future__ import annotations
4
-
5
- import unittest
6
- from unittest.mock import MagicMock, patch
7
-
8
- from comfyui_skills_cli.client import ComfyUIClient
9
-
10
-
11
- SAMPLE_STATS = {
12
- "system": {
13
- "os": "posix",
14
- "ram_total": 68719476736,
15
- "ram_free": 32000000000,
16
- "comfyui_version": "v0.3.10",
17
- "python_version": "3.11.5",
18
- "pytorch_version": "2.1.0",
19
- "embedded_python": False,
20
- },
21
- "devices": [
22
- {
23
- "name": "cuda:0",
24
- "type": "cuda",
25
- "index": 0,
26
- "vram_total": 25769803776,
27
- "vram_free": 20000000000,
28
- "torch_vram_total": 25769803776,
29
- "torch_vram_free": 22000000000,
30
- }
31
- ],
32
- }
33
-
34
-
35
- class GetSystemStatsTests(unittest.TestCase):
36
- def setUp(self) -> None:
37
- self.client = ComfyUIClient("http://localhost:8188")
38
-
39
- @patch("comfyui_skills_cli.client.requests.get")
40
- def test_get_system_stats_success(self, mock_get: MagicMock) -> None:
41
- mock_resp = MagicMock(status_code=200)
42
- mock_resp.json.return_value = SAMPLE_STATS
43
- mock_get.return_value = mock_resp
44
-
45
- result = self.client.get_system_stats()
46
-
47
- self.assertEqual(result, SAMPLE_STATS)
48
- self.assertIn("/system_stats", mock_get.call_args.args[0])
49
-
50
- @patch("comfyui_skills_cli.client.requests.get")
51
- def test_get_system_stats_raises_on_error(self, mock_get: MagicMock) -> None:
52
- mock_resp = MagicMock(status_code=500)
53
- mock_resp.raise_for_status.side_effect = Exception("Server error")
54
- mock_get.return_value = mock_resp
55
-
56
- with self.assertRaises(Exception):
57
- self.client.get_system_stats()
58
-
59
-
60
- class ServerStatsAllTests(unittest.TestCase):
61
- """Test that --all queries multiple servers."""
62
-
63
- @patch("comfyui_skills_cli.client.requests.get")
64
- def test_all_queries_multiple_servers(self, mock_get: MagicMock) -> None:
65
- mock_resp = MagicMock(status_code=200)
66
- mock_resp.json.return_value = SAMPLE_STATS
67
- mock_get.return_value = mock_resp
68
-
69
- # Create two clients (simulating what the command does)
70
- clients = [
71
- ComfyUIClient("http://server1:8188"),
72
- ComfyUIClient("http://server2:8188"),
73
- ]
74
-
75
- results = []
76
- for c in clients:
77
- results.append(c.get_system_stats())
78
-
79
- self.assertEqual(len(results), 2)
80
- self.assertEqual(results[0]["system"]["comfyui_version"], "v0.3.10")
81
- self.assertEqual(results[1]["system"]["comfyui_version"], "v0.3.10")
82
- # Verify both servers were called
83
- self.assertEqual(mock_get.call_count, 2)
84
- urls_called = [call.args[0] for call in mock_get.call_args_list]
85
- self.assertIn("http://server1:8188/system_stats", urls_called)
86
- self.assertIn("http://server2:8188/system_stats", urls_called)
87
-
88
-
89
- if __name__ == "__main__":
90
- unittest.main()