1use futures_util::StreamExt;
2use reqwest::Client;
3use serde_json::Value;
4use std::time::Duration;
5
6use crate::error::{ComfyError, Result};
7use crate::types::*;
8
9fn normalize(endpoint: String) -> String {
10 endpoint.trim_end_matches('/').to_string()
11}
12
13#[derive(Debug, Clone)]
30pub struct ComfyClient {
31 http: Client,
32 endpoint: String,
33 client_id: String,
34}
35
36impl ComfyClient {
37 pub fn new(endpoint: impl Into<String>) -> Self {
39 Self {
40 http: Client::new(),
41 endpoint: normalize(endpoint.into()),
42 client_id: "comfyui-rs".to_string(),
43 }
44 }
45
46 pub fn with_http_client(mut self, client: Client) -> Self {
48 self.http = client;
49 self
50 }
51
52 pub fn with_client_id(mut self, id: impl Into<String>) -> Self {
54 self.client_id = id.into();
55 self
56 }
57
58 pub fn endpoint(&self) -> &str {
60 &self.endpoint
61 }
62
63 pub fn client_id(&self) -> &str {
65 &self.client_id
66 }
67
68 pub async fn health(&self) -> Result<bool> {
73 let url = format!("{}/queue", self.endpoint);
74 match self
75 .http
76 .get(&url)
77 .timeout(Duration::from_secs(5))
78 .send()
79 .await
80 {
81 Ok(_) => Ok(true),
82 Err(e) if e.is_connect() || e.is_timeout() => Ok(false),
83 Err(e) => Err(ComfyError::Network {
84 context: format!(
85 "Cannot connect to ComfyUI at {} \u{2014} is the service running?",
86 self.endpoint
87 ),
88 source: e,
89 }),
90 }
91 }
92
93 pub async fn queue_prompt(&self, workflow: &Value) -> Result<String> {
97 let url = format!("{}/prompt", self.endpoint);
98 let body = serde_json::json!({
99 "prompt": workflow,
100 "client_id": self.client_id,
101 });
102
103 let resp = self
104 .http
105 .post(&url)
106 .timeout(Duration::from_secs(30))
107 .json(&body)
108 .send()
109 .await
110 .map_err(|e| ComfyError::Network {
111 context: format!(
112 "Cannot connect to ComfyUI at {} \u{2014} is the service running?",
113 self.endpoint
114 ),
115 source: e,
116 })?;
117
118 if !resp.status().is_success() {
119 let status = resp.status().as_u16();
120 let body_text = resp.text().await.unwrap_or_default();
121 return Err(ComfyError::Http {
122 status,
123 body: body_text,
124 });
125 }
126
127 let json: Value = resp.json().await.map_err(|e| ComfyError::Network {
128 context: "Failed to parse ComfyUI /prompt response".into(),
129 source: e,
130 })?;
131
132 if let Some(errors) = json.get("node_errors") {
134 if let Some(obj) = errors.as_object() {
135 if !obj.is_empty() {
136 return Err(ComfyError::NodeErrors(
137 serde_json::to_string_pretty(errors).unwrap_or_default(),
138 ));
139 }
140 }
141 }
142
143 json.get("prompt_id")
144 .and_then(|v| v.as_str())
145 .map(|s| s.to_string())
146 .ok_or_else(|| ComfyError::InvalidResponse("Response missing prompt_id".into()))
147 }
148
149 pub async fn history(&self, prompt_id: &str) -> Result<Option<PromptHistory>> {
153 let url = format!("{}/history/{}", self.endpoint, prompt_id);
154 let resp = self
155 .http
156 .get(&url)
157 .timeout(Duration::from_secs(10))
158 .send()
159 .await
160 .map_err(|e| ComfyError::Network {
161 context: "Failed to fetch ComfyUI history".into(),
162 source: e,
163 })?;
164
165 if !resp.status().is_success() {
166 return Ok(None);
167 }
168
169 let json: Value = resp.json().await.map_err(|e| ComfyError::Network {
170 context: "Failed to parse ComfyUI history response".into(),
171 source: e,
172 })?;
173
174 let entry = match json.get(prompt_id) {
175 Some(e) => e,
176 None => return Ok(None),
177 };
178
179 let status_str = entry
180 .pointer("/status/status_str")
181 .and_then(|v| v.as_str())
182 .unwrap_or("unknown");
183
184 let completed = entry
185 .pointer("/status/completed")
186 .and_then(|v| v.as_bool())
187 .unwrap_or(false);
188
189 let mut images = Vec::new();
190 if let Some(outputs) = entry.get("outputs").and_then(|o| o.as_object()) {
191 for (_node_id, node_output) in outputs {
192 if let Some(imgs) = node_output.get("images").and_then(|i| i.as_array()) {
193 for img in imgs {
194 if let Some(filename) = img.get("filename").and_then(|f| f.as_str()) {
195 let subfolder =
196 img.get("subfolder").and_then(|s| s.as_str()).unwrap_or("");
197 let img_type =
198 img.get("type").and_then(|t| t.as_str()).unwrap_or("output");
199 images.push(ImageRef {
200 filename: filename.to_string(),
201 subfolder: subfolder.to_string(),
202 img_type: img_type.to_string(),
203 });
204 }
205 }
206 }
207 }
208 }
209
210 Ok(Some(PromptHistory {
211 status: status_str.to_string(),
212 completed,
213 images,
214 }))
215 }
216
217 pub async fn image(&self, img: &ImageRef) -> Result<Vec<u8>> {
221 let url = reqwest::Url::parse_with_params(
222 &format!("{}/view", self.endpoint),
223 &[
224 ("filename", img.filename.as_str()),
225 ("subfolder", img.subfolder.as_str()),
226 ("type", img.img_type.as_str()),
227 ],
228 )
229 .map_err(|e| ComfyError::InvalidResponse(format!("Bad image URL: {}", e)))?;
230
231 let resp = self
232 .http
233 .get(url)
234 .timeout(Duration::from_secs(30))
235 .send()
236 .await
237 .map_err(|e| ComfyError::Network {
238 context: format!("Failed to fetch image {} from ComfyUI", img.filename),
239 source: e,
240 })?;
241
242 if !resp.status().is_success() {
243 return Err(ComfyError::Http {
244 status: resp.status().as_u16(),
245 body: format!("Failed to fetch image {}", img.filename),
246 });
247 }
248
249 let bytes = resp.bytes().await.map_err(|e| ComfyError::Network {
250 context: "Failed to read image bytes".into(),
251 source: e,
252 })?;
253 Ok(bytes.to_vec())
254 }
255
256 pub async fn queue_status(&self) -> Result<QueueStatus> {
260 let url = format!("{}/queue", self.endpoint);
261 let resp = self
262 .http
263 .get(&url)
264 .timeout(Duration::from_secs(5))
265 .send()
266 .await
267 .map_err(|e| ComfyError::Network {
268 context: "Failed to fetch ComfyUI queue status".into(),
269 source: e,
270 })?;
271
272 let json: Value = resp.json().await.map_err(|e| ComfyError::Network {
273 context: "Failed to parse ComfyUI queue response".into(),
274 source: e,
275 })?;
276
277 let running = json
278 .get("queue_running")
279 .and_then(|v| v.as_array())
280 .map(|a| a.len() as u32)
281 .unwrap_or(0);
282 let pending = json
283 .get("queue_pending")
284 .and_then(|v| v.as_array())
285 .map(|a| a.len() as u32)
286 .unwrap_or(0);
287
288 Ok(QueueStatus { running, pending })
289 }
290
291 pub async fn free_memory(&self, unload_models: bool) -> Result<()> {
295 let url = format!("{}/free", self.endpoint);
296 let body = if unload_models {
297 serde_json::json!({"unload_models": true})
298 } else {
299 serde_json::json!({"free_memory": true})
300 };
301 self.http
302 .post(&url)
303 .timeout(Duration::from_secs(30))
304 .json(&body)
305 .send()
306 .await
307 .map_err(|e| ComfyError::Network {
308 context: "Failed to send free memory request".into(),
309 source: e,
310 })?;
311 Ok(())
312 }
313
314 pub async fn interrupt(&self) -> Result<()> {
316 let url = format!("{}/interrupt", self.endpoint);
317 self.http
318 .post(&url)
319 .timeout(Duration::from_secs(5))
320 .send()
321 .await
322 .map_err(|e| ComfyError::Network {
323 context: "Failed to send interrupt".into(),
324 source: e,
325 })?;
326 Ok(())
327 }
328
329 pub async fn upload_image(
334 &self,
335 image_bytes: &[u8],
336 filename: &str,
337 overwrite: bool,
338 ) -> Result<String> {
339 let url = format!("{}/upload/image", self.endpoint);
340
341 let form = reqwest::multipart::Form::new()
342 .part(
343 "image",
344 reqwest::multipart::Part::bytes(image_bytes.to_vec())
345 .file_name(filename.to_string())
346 .mime_str("image/png")
347 .map_err(|e| ComfyError::InvalidResponse(format!("Invalid MIME: {}", e)))?,
348 )
349 .text("type", "input")
350 .text("overwrite", if overwrite { "true" } else { "false" });
351
352 let resp = self
353 .http
354 .post(&url)
355 .timeout(Duration::from_secs(30))
356 .multipart(form)
357 .send()
358 .await
359 .map_err(|e| ComfyError::Network {
360 context: format!("Failed to upload image to ComfyUI at {}", self.endpoint),
361 source: e,
362 })?;
363
364 if !resp.status().is_success() {
365 let status = resp.status().as_u16();
366 let body_text = resp.text().await.unwrap_or_default();
367 return Err(ComfyError::Http {
368 status,
369 body: body_text,
370 });
371 }
372
373 let json: Value = resp.json().await.map_err(|e| ComfyError::Network {
374 context: "Failed to parse ComfyUI /upload/image response".into(),
375 source: e,
376 })?;
377
378 json["name"]
379 .as_str()
380 .map(|s| s.to_string())
381 .ok_or_else(|| ComfyError::InvalidResponse("Upload response missing name field".into()))
382 }
383
384 pub async fn checkpoints(&self) -> Result<Vec<String>> {
388 self.object_info_list(
389 "CheckpointLoaderSimple",
390 "/CheckpointLoaderSimple/input/required/ckpt_name/0",
391 )
392 .await
393 }
394
395 pub async fn samplers(&self) -> Result<Vec<String>> {
397 self.object_info_list("KSampler", "/KSampler/input/required/sampler_name/0")
398 .await
399 }
400
401 pub async fn schedulers(&self) -> Result<Vec<String>> {
403 self.object_info_list("KSampler", "/KSampler/input/required/scheduler/0")
404 .await
405 }
406
407 async fn object_info_list(&self, node: &str, pointer: &str) -> Result<Vec<String>> {
408 let url = format!("{}/object_info/{}", self.endpoint, node);
409 let resp = self
410 .http
411 .get(&url)
412 .timeout(Duration::from_secs(10))
413 .send()
414 .await
415 .map_err(|e| ComfyError::Network {
416 context: format!(
417 "Cannot connect to ComfyUI at {} \u{2014} is the service running?",
418 self.endpoint
419 ),
420 source: e,
421 })?;
422
423 if !resp.status().is_success() {
424 return Ok(Vec::new());
425 }
426
427 let json: Value = resp.json().await.map_err(|e| ComfyError::Network {
428 context: format!("Failed to parse {} object_info", node),
429 source: e,
430 })?;
431
432 Ok(json
433 .pointer(pointer)
434 .and_then(|v| v.as_array())
435 .map(|arr| {
436 arr.iter()
437 .filter_map(|v| v.as_str().map(String::from))
438 .collect()
439 })
440 .unwrap_or_default())
441 }
442
443 pub async fn wait_for_completion(
447 &self,
448 prompt_id: &str,
449 timeout: Duration,
450 ) -> Result<GenerationOutcome> {
451 self.wait_for_completion_poll(prompt_id, Duration::from_secs(2), timeout)
452 .await
453 }
454
455 pub async fn wait_for_completion_ws<F>(
459 &self,
460 prompt_id: &str,
461 timeout: Duration,
462 on_progress: F,
463 ) -> Result<GenerationOutcome>
464 where
465 F: FnMut(ProgressUpdate),
466 {
467 self.wait_ws_inner(prompt_id, timeout, on_progress).await
468 }
469
470 async fn wait_for_completion_poll(
471 &self,
472 prompt_id: &str,
473 poll_interval: Duration,
474 timeout: Duration,
475 ) -> Result<GenerationOutcome> {
476 let start = std::time::Instant::now();
477 loop {
478 if start.elapsed() > timeout {
479 return Ok(GenerationOutcome::TimedOut);
480 }
481 if let Some(history) = self.history(prompt_id).await? {
482 if history.completed {
483 return Ok(GenerationOutcome::Completed {
484 images: history.images,
485 });
486 } else if history.status == "error" {
487 return Ok(GenerationOutcome::Failed {
488 error: "ComfyUI generation failed".into(),
489 });
490 }
491 }
492 tokio::time::sleep(poll_interval).await;
493 }
494 }
495
496 async fn wait_ws_inner<F>(
497 &self,
498 prompt_id: &str,
499 timeout: Duration,
500 mut on_progress: F,
501 ) -> Result<GenerationOutcome>
502 where
503 F: FnMut(ProgressUpdate),
504 {
505 let ws_url = format!(
506 "{}/ws?clientId={}",
507 self.endpoint
508 .replace("http://", "ws://")
509 .replace("https://", "wss://"),
510 self.client_id
511 );
512
513 let (mut ws, _) = match tokio_tungstenite::connect_async(&ws_url).await {
514 Ok(c) => c,
515 Err(e) => {
516 tracing::warn!(error = %e, "WebSocket failed, falling back to polling");
517 return self
518 .wait_for_completion_poll(prompt_id, Duration::from_secs(2), timeout)
519 .await;
520 }
521 };
522
523 let start = std::time::Instant::now();
524 let mut our_msg_count: usize = 0;
525 let mut total_msg_count: usize = 0;
526 const MAX_OUR_MESSAGES: usize = 10_000;
527 const MAX_TOTAL_MESSAGES: usize = 50_000;
528
529 while let Ok(Some(msg)) = tokio::time::timeout(Duration::from_secs(30), ws.next()).await {
530 total_msg_count += 1;
531 if total_msg_count > MAX_TOTAL_MESSAGES {
532 tracing::warn!(
533 count = MAX_TOTAL_MESSAGES,
534 "WebSocket exceeded max total messages, falling back to polling"
535 );
536 break;
537 }
538 if start.elapsed() > timeout {
539 return Ok(GenerationOutcome::TimedOut);
540 }
541
542 let text = match msg {
543 Ok(m) if m.is_text() => m.into_text().unwrap_or_default(),
544 Ok(_) => continue,
545 Err(_) => break,
546 };
547
548 let json: Value = match serde_json::from_str(&text) {
549 Ok(j) => j,
550 Err(_) => continue,
551 };
552
553 let msg_type = json.get("type").and_then(|v| v.as_str()).unwrap_or("");
554 let data = json.get("data");
555 let pid = data
556 .and_then(|d| d.get("prompt_id"))
557 .and_then(|v| v.as_str());
558
559 if pid.is_some() && pid != Some(prompt_id) {
561 continue;
562 }
563
564 if pid == Some(prompt_id) {
565 our_msg_count += 1;
566 if our_msg_count > MAX_OUR_MESSAGES {
567 tracing::warn!(
568 prompt_id = prompt_id,
569 count = MAX_OUR_MESSAGES,
570 "Prompt exceeded max messages, falling back to polling"
571 );
572 break;
573 }
574 }
575
576 match msg_type {
577 "progress" => {
578 if let Some(d) = data {
579 let val = d.get("value").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
580 let max = d.get("max").and_then(|v| v.as_u64()).unwrap_or(1) as u32;
581 on_progress(ProgressUpdate {
582 current_step: val,
583 total_steps: max,
584 });
585 }
586 }
587 "executing"
588 if data
589 .and_then(|d| d.get("node"))
590 .map(|v| v.is_null())
591 .unwrap_or(false) =>
592 {
593 return self.fetch_outcome(prompt_id).await;
595 }
596 "execution_error" => {
597 let err = data
598 .and_then(|d| d.get("exception_message"))
599 .and_then(|v| v.as_str())
600 .unwrap_or("Unknown error");
601 return Ok(GenerationOutcome::Failed {
602 error: format!("ComfyUI error: {}", err),
603 });
604 }
605 _ => {}
606 }
607 }
608
609 self.wait_for_completion_poll(prompt_id, Duration::from_secs(2), timeout)
611 .await
612 }
613
614 pub async fn queue_prompt_traced(
621 &self,
622 workflow: &Value,
623 trace_ctx: Option<&stack_ids::TraceCtx>,
624 ) -> Result<String> {
625 if let Some(ctx) = trace_ctx {
626 tracing::debug!(trace_id = %ctx.trace_id, "Queuing prompt to ComfyUI");
627 }
628 self.queue_prompt(workflow).await
629 }
630
631 pub async fn history_traced(
636 &self,
637 prompt_id: &str,
638 trace_ctx: Option<&stack_ids::TraceCtx>,
639 ) -> Result<Option<PromptHistory>> {
640 if let Some(ctx) = trace_ctx {
641 tracing::debug!(trace_id = %ctx.trace_id, prompt_id = %prompt_id, "Fetching history from ComfyUI");
642 }
643 self.history(prompt_id).await
644 }
645
646 pub async fn image_traced(
651 &self,
652 img: &ImageRef,
653 trace_ctx: Option<&stack_ids::TraceCtx>,
654 ) -> Result<Vec<u8>> {
655 if let Some(ctx) = trace_ctx {
656 tracing::debug!(trace_id = %ctx.trace_id, filename = %img.filename, "Downloading image from ComfyUI");
657 }
658 self.image(img).await
659 }
660
661 pub async fn queue_status_traced(
666 &self,
667 trace_ctx: Option<&stack_ids::TraceCtx>,
668 ) -> Result<QueueStatus> {
669 if let Some(ctx) = trace_ctx {
670 tracing::debug!(trace_id = %ctx.trace_id, "Fetching queue status from ComfyUI");
671 }
672 self.queue_status().await
673 }
674
675 pub async fn upload_image_traced(
680 &self,
681 image_bytes: &[u8],
682 filename: &str,
683 overwrite: bool,
684 trace_ctx: Option<&stack_ids::TraceCtx>,
685 ) -> Result<String> {
686 if let Some(ctx) = trace_ctx {
687 tracing::debug!(trace_id = %ctx.trace_id, filename = %filename, "Uploading image to ComfyUI");
688 }
689 self.upload_image(image_bytes, filename, overwrite).await
690 }
691
692 pub async fn interrupt_traced(&self, trace_ctx: Option<&stack_ids::TraceCtx>) -> Result<()> {
697 if let Some(ctx) = trace_ctx {
698 tracing::debug!(trace_id = %ctx.trace_id, "Interrupting ComfyUI generation");
699 }
700 self.interrupt().await
701 }
702
703 pub async fn free_memory_traced(
708 &self,
709 unload_models: bool,
710 trace_ctx: Option<&stack_ids::TraceCtx>,
711 ) -> Result<()> {
712 if let Some(ctx) = trace_ctx {
713 tracing::debug!(trace_id = %ctx.trace_id, unload_models = %unload_models, "Freeing ComfyUI memory");
714 }
715 self.free_memory(unload_models).await
716 }
717
718 pub async fn wait_for_completion_traced(
723 &self,
724 prompt_id: &str,
725 timeout: Duration,
726 trace_ctx: Option<&stack_ids::TraceCtx>,
727 ) -> Result<GenerationOutcome> {
728 if let Some(ctx) = trace_ctx {
729 tracing::debug!(trace_id = %ctx.trace_id, prompt_id = %prompt_id, "Waiting for ComfyUI completion (polling)");
730 }
731 self.wait_for_completion(prompt_id, timeout).await
732 }
733
734 pub async fn wait_for_completion_ws_traced<F>(
739 &self,
740 prompt_id: &str,
741 timeout: Duration,
742 on_progress: F,
743 trace_ctx: Option<&stack_ids::TraceCtx>,
744 ) -> Result<GenerationOutcome>
745 where
746 F: FnMut(ProgressUpdate),
747 {
748 if let Some(ctx) = trace_ctx {
749 tracing::debug!(trace_id = %ctx.trace_id, prompt_id = %prompt_id, "Waiting for ComfyUI completion (WebSocket)");
750 }
751 self.wait_for_completion_ws(prompt_id, timeout, on_progress)
752 .await
753 }
754
755 async fn fetch_outcome(&self, prompt_id: &str) -> Result<GenerationOutcome> {
756 match self.history(prompt_id).await? {
757 Some(history) if history.completed => Ok(GenerationOutcome::Completed {
758 images: history.images,
759 }),
760 Some(_) => Ok(GenerationOutcome::Failed {
761 error: "Generation failed".into(),
762 }),
763 None => Ok(GenerationOutcome::Failed {
764 error: "No history found after generation".into(),
765 }),
766 }
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 #[test]
775 fn test_normalize_endpoint() {
776 assert_eq!(
777 normalize("http://localhost:8188/".into()),
778 "http://localhost:8188"
779 );
780 assert_eq!(
781 normalize("http://localhost:8188".into()),
782 "http://localhost:8188"
783 );
784 assert_eq!(normalize("http://host:8188///".into()), "http://host:8188");
785 }
786
787 #[test]
788 fn test_client_builder() {
789 let client = ComfyClient::new("http://127.0.0.1:8188").with_client_id("my-app");
790 assert_eq!(client.endpoint(), "http://127.0.0.1:8188");
791 assert_eq!(client.client_id(), "my-app");
792 }
793
794 #[test]
795 fn test_default_client_id() {
796 let client = ComfyClient::new("http://localhost:8188");
797 assert_eq!(client.client_id(), "comfyui-rs");
798 }
799
800 #[test]
801 fn test_parse_history_response() {
802 let json: Value = serde_json::from_str(
803 r#"{
804 "abc123": {
805 "status": {"status_str": "success", "completed": true},
806 "outputs": {
807 "9": {
808 "images": [
809 {"filename": "ComfyUI_00001_.png", "subfolder": "", "type": "output"}
810 ]
811 }
812 }
813 }
814 }"#,
815 )
816 .unwrap();
817
818 let entry = json.get("abc123").unwrap();
819 let status = entry.pointer("/status/status_str").and_then(|v| v.as_str());
820 assert_eq!(status, Some("success"));
821
822 let completed = entry.pointer("/status/completed").and_then(|v| v.as_bool());
823 assert_eq!(completed, Some(true));
824
825 let images = entry
826 .pointer("/outputs/9/images")
827 .and_then(|v| v.as_array());
828 assert!(images.is_some());
829 assert_eq!(images.unwrap()[0]["filename"], "ComfyUI_00001_.png");
830 }
831
832 #[test]
833 fn test_parse_queue_response() {
834 let json: Value = serde_json::from_str(
835 r#"{
836 "queue_running": [["item1"]],
837 "queue_pending": [["item2"], ["item3"]]
838 }"#,
839 )
840 .unwrap();
841
842 let running = json
843 .get("queue_running")
844 .and_then(|v| v.as_array())
845 .map(|a| a.len())
846 .unwrap_or(0);
847 let pending = json
848 .get("queue_pending")
849 .and_then(|v| v.as_array())
850 .map(|a| a.len())
851 .unwrap_or(0);
852 assert_eq!(running, 1);
853 assert_eq!(pending, 2);
854 }
855
856 #[test]
857 fn test_parse_prompt_response() {
858 let json: Value = serde_json::from_str(
859 r#"{
860 "prompt_id": "abc-123-def",
861 "number": 1,
862 "node_errors": {}
863 }"#,
864 )
865 .unwrap();
866
867 let prompt_id = json.get("prompt_id").and_then(|v| v.as_str());
868 assert_eq!(prompt_id, Some("abc-123-def"));
869
870 let errors = json.get("node_errors").and_then(|v| v.as_object());
871 assert!(errors.unwrap().is_empty());
872 }
873
874 #[test]
875 fn test_parse_checkpoint_object_info() {
876 let json: Value = serde_json::from_str(
877 r#"{
878 "CheckpointLoaderSimple": {
879 "input": {
880 "required": {
881 "ckpt_name": [
882 ["dreamshaper_8.safetensors", "deliberate_v3.safetensors"]
883 ]
884 }
885 }
886 }
887 }"#,
888 )
889 .unwrap();
890
891 let checkpoints = json
892 .pointer("/CheckpointLoaderSimple/input/required/ckpt_name/0")
893 .and_then(|v| v.as_array())
894 .map(|arr| {
895 arr.iter()
896 .filter_map(|v| v.as_str().map(String::from))
897 .collect::<Vec<_>>()
898 })
899 .unwrap_or_default();
900
901 assert_eq!(checkpoints.len(), 2);
902 assert_eq!(checkpoints[0], "dreamshaper_8.safetensors");
903 }
904
905 #[test]
906 fn test_parse_sampler_object_info() {
907 let json: Value = serde_json::from_str(
908 r#"{
909 "KSampler": {
910 "input": {
911 "required": {
912 "sampler_name": [["euler", "dpmpp_2m", "dpmpp_sde"]],
913 "scheduler": [["normal", "karras", "exponential"]]
914 }
915 }
916 }
917 }"#,
918 )
919 .unwrap();
920
921 let samplers = json
922 .pointer("/KSampler/input/required/sampler_name/0")
923 .and_then(|v| v.as_array())
924 .map(|arr| {
925 arr.iter()
926 .filter_map(|v| v.as_str().map(String::from))
927 .collect::<Vec<_>>()
928 })
929 .unwrap_or_default();
930
931 assert_eq!(samplers.len(), 3);
932 assert!(samplers.contains(&"dpmpp_2m".to_string()));
933 }
934
935 #[test]
936 fn test_empty_object_info() {
937 let json: Value = serde_json::from_str(r#"{}"#).unwrap();
938
939 let checkpoints = json
940 .pointer("/CheckpointLoaderSimple/input/required/ckpt_name/0")
941 .and_then(|v| v.as_array())
942 .map(|arr| {
943 arr.iter()
944 .filter_map(|v| v.as_str().map(String::from))
945 .collect::<Vec<_>>()
946 })
947 .unwrap_or_default();
948
949 assert!(checkpoints.is_empty());
950 }
951
952 #[test]
953 fn test_image_ref() {
954 let img = ImageRef {
955 filename: "test.png".to_string(),
956 subfolder: "".to_string(),
957 img_type: "output".to_string(),
958 };
959 assert_eq!(img.filename, "test.png");
960
961 let json = serde_json::to_string(&img).unwrap();
962 assert!(json.contains("\"filename\":\"test.png\""));
963 }
964
965 #[test]
966 fn test_queue_status_serialization() {
967 let status = QueueStatus {
968 running: 1,
969 pending: 3,
970 };
971 let json = serde_json::to_string(&status).unwrap();
972 assert!(json.contains("\"running\":1"));
973 assert!(json.contains("\"pending\":3"));
974 }
975
976 #[allow(dead_code)]
981 async fn _assert_traced_signatures_compile(client: &ComfyClient) {
982 let ctx = stack_ids::TraceCtx::generate();
983 let workflow = serde_json::json!({});
984 let img = ImageRef {
985 filename: "test.png".into(),
986 subfolder: "".into(),
987 img_type: "output".into(),
988 };
989
990 let _ = client.queue_prompt_traced(&workflow, Some(&ctx)).await;
991 let _ = client.queue_prompt_traced(&workflow, None).await;
992 let _ = client.history_traced("id", Some(&ctx)).await;
993 let _ = client.history_traced("id", None).await;
994 let _ = client.image_traced(&img, Some(&ctx)).await;
995 let _ = client.image_traced(&img, None).await;
996 let _ = client.queue_status_traced(Some(&ctx)).await;
997 let _ = client.queue_status_traced(None).await;
998 let _ = client
999 .upload_image_traced(b"png", "f.png", false, Some(&ctx))
1000 .await;
1001 let _ = client
1002 .upload_image_traced(b"png", "f.png", false, None)
1003 .await;
1004 let _ = client.interrupt_traced(Some(&ctx)).await;
1005 let _ = client.interrupt_traced(None).await;
1006 let _ = client.free_memory_traced(true, Some(&ctx)).await;
1007 let _ = client.free_memory_traced(false, None).await;
1008 let _ = client
1009 .wait_for_completion_traced("id", Duration::from_secs(10), Some(&ctx))
1010 .await;
1011 let _ = client
1012 .wait_for_completion_ws_traced("id", Duration::from_secs(10), |_| {}, Some(&ctx))
1013 .await;
1014 }
1015
1016 #[test]
1017 fn test_trace_ctx_creation() {
1018 let ctx = stack_ids::TraceCtx::generate();
1020 assert!(!ctx.trace_id.is_empty());
1021
1022 let ctx2 = stack_ids::TraceCtx::from_legacy_trace_id("test-trace-123");
1023 assert_eq!(ctx2.trace_id, "test-trace-123");
1024 }
1025}