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.
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/PKG-INFO +4 -2
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/README.md +3 -1
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/PKG-INFO +4 -2
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/SOURCES.txt +3 -4
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/run.py +24 -9
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/workflow.py +10 -1
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/pyproject.toml +1 -1
- comfyui_skill_cli-0.2.8/tests/test_client.py +279 -0
- comfyui_skill_cli-0.2.8/tests/test_error_hints.py +95 -0
- comfyui_skill_cli-0.2.8/tests/test_nodes.py +47 -0
- comfyui_skill_cli-0.2.7/tests/test_client_new_apis.py +0 -199
- comfyui_skill_cli-0.2.7/tests/test_nodes_and_ws.py +0 -203
- comfyui_skill_cli-0.2.7/tests/test_partial_execution.py +0 -56
- comfyui_skill_cli-0.2.7/tests/test_server_stats.py +0 -90
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/LICENSE +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/dependency_links.txt +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/entry_points.txt +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/requires.txt +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/top_level.txt +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/__init__.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/__main__.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/client.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/__init__.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/cancel.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/config.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/deps.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/free.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/history.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/logs.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/models.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/nodes.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/queue.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/server.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/skill.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/templates.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/upload.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/config.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/error_hints.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/main.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/output.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/storage.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/update_check.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/utils.py +0 -0
- {comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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/
|
|
37
|
-
tests/
|
|
38
|
-
tests/
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
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":
|
|
433
|
+
"filename": filename,
|
|
419
434
|
"subfolder": item.get("subfolder", ""),
|
|
420
435
|
"type": item.get("type", "output"),
|
|
421
|
-
"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"),
|
|
@@ -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()
|
|
File without changes
|
{comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skill_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{comfyui_skill_cli-0.2.7 → comfyui_skill_cli-0.2.8}/comfyui_skills_cli/commands/templates.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|