1use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::{
7 ModelingCmd, each_cmd as mcmd,
8 ok_response::{OkModelingCmdResponse, output::EntityGetAllChildUuids},
9 websocket::OkWebSocketResponseData,
10};
11use kittycad_modeling_cmds::{self as kcmc};
12
13use super::extrude::do_post_extrude;
14use crate::{
15 errors::{KclError, KclErrorDetails},
16 execution::{
17 ExecState, GeometryWithImportedGeometry, KclValue, ModelingCmdMeta, Sketch, Solid,
18 types::{NumericType, PrimitiveType, RuntimeType},
19 },
20 parsing::ast::types::TagNode,
21 std::{Args, extrude::NamedCapTags},
22};
23
24pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
28 let geometry = args.get_unlabeled_kw_arg(
29 "geometry",
30 &RuntimeType::Union(vec![
31 RuntimeType::Primitive(PrimitiveType::Sketch),
32 RuntimeType::Primitive(PrimitiveType::Solid),
33 RuntimeType::imported(),
34 ]),
35 exec_state,
36 )?;
37
38 let cloned = inner_clone(geometry, exec_state, args).await?;
39 Ok(cloned.into())
40}
41
42async fn inner_clone(
43 geometry: GeometryWithImportedGeometry,
44 exec_state: &mut ExecState,
45 args: Args,
46) -> Result<GeometryWithImportedGeometry, KclError> {
47 let new_id = exec_state.next_uuid();
48 let mut geometry = geometry.clone();
49 let old_id = geometry.id(&args.ctx).await?;
50
51 let mut new_geometry = match &geometry {
52 GeometryWithImportedGeometry::ImportedGeometry(imported) => {
53 let mut new_imported = imported.clone();
54 new_imported.id = new_id;
55 GeometryWithImportedGeometry::ImportedGeometry(new_imported)
56 }
57 GeometryWithImportedGeometry::Sketch(sketch) => {
58 let mut new_sketch = sketch.clone();
59 new_sketch.id = new_id;
60 new_sketch.original_id = new_id;
61 new_sketch.artifact_id = new_id.into();
62 GeometryWithImportedGeometry::Sketch(new_sketch)
63 }
64 GeometryWithImportedGeometry::Solid(solid) => {
65 exec_state
67 .flush_batch_for_solids((&args).into(), &[solid.clone()])
68 .await?;
69
70 let mut new_solid = solid.clone();
71 new_solid.id = new_id;
72 new_solid.sketch.original_id = new_id;
73 new_solid.artifact_id = new_id.into();
74 GeometryWithImportedGeometry::Solid(new_solid)
75 }
76 };
77
78 if args.ctx.no_engine_commands().await {
79 return Ok(new_geometry);
80 }
81
82 exec_state
83 .batch_modeling_cmd(
84 ModelingCmdMeta::from_args_id(&args, new_id),
85 ModelingCmd::from(mcmd::EntityClone { entity_id: old_id }),
86 )
87 .await?;
88
89 fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
90 .await
91 .map_err(|e| {
92 KclError::new_internal(KclErrorDetails::new(
93 format!("failed to fix tags and references: {e:?}"),
94 vec![args.source_range],
95 ))
96 })?;
97
98 Ok(new_geometry)
99}
100async fn fix_tags_and_references(
102 new_geometry: &mut GeometryWithImportedGeometry,
103 old_geometry_id: uuid::Uuid,
104 exec_state: &mut ExecState,
105 args: &Args,
106) -> Result<()> {
107 let new_geometry_id = new_geometry.id(&args.ctx).await?;
108 let entity_id_map = get_old_new_child_map(new_geometry_id, old_geometry_id, exec_state, args).await?;
109
110 match new_geometry {
112 GeometryWithImportedGeometry::ImportedGeometry(_) => {}
113 GeometryWithImportedGeometry::Sketch(sketch) => {
114 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state).await?;
115 }
116 GeometryWithImportedGeometry::Solid(solid) => {
117 solid.sketch.id = new_geometry_id;
119 solid.sketch.original_id = new_geometry_id;
120 solid.sketch.artifact_id = new_geometry_id.into();
121
122 fix_sketch_tags_and_references(&mut solid.sketch, &entity_id_map, exec_state).await?;
123
124 let (start_tag, end_tag) = get_named_cap_tags(solid);
125
126 for edge_cut in solid.edge_cuts.iter_mut() {
128 if let Some(id) = entity_id_map.get(&edge_cut.id()) {
129 edge_cut.set_id(*id);
130 } else {
131 crate::log::logln!(
132 "Failed to find new edge cut id for old edge cut id: {:?}",
133 edge_cut.id()
134 );
135 }
136 if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
137 edge_cut.set_edge_id(*new_edge_id);
138 } else {
139 crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
140 }
141 }
142
143 let new_solid = do_post_extrude(
146 &solid.sketch,
147 new_geometry_id.into(),
148 crate::std::args::TyF64::new(
149 solid.height,
150 NumericType::Known(crate::execution::types::UnitType::Length(solid.units)),
151 ),
152 solid.sectional,
153 &NamedCapTags {
154 start: start_tag.as_ref(),
155 end: end_tag.as_ref(),
156 },
157 exec_state,
158 args,
159 None,
160 )
161 .await?;
162
163 *solid = new_solid;
164 }
165 }
166
167 Ok(())
168}
169
170async fn get_old_new_child_map(
171 new_geometry_id: uuid::Uuid,
172 old_geometry_id: uuid::Uuid,
173 exec_state: &mut ExecState,
174 args: &Args,
175) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
176 let response = exec_state
178 .send_modeling_cmd(
179 args.into(),
180 ModelingCmd::from(mcmd::EntityGetAllChildUuids {
181 entity_id: old_geometry_id,
182 }),
183 )
184 .await?;
185 let OkWebSocketResponseData::Modeling {
186 modeling_response:
187 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
188 entity_ids: old_entity_ids,
189 }),
190 } = response
191 else {
192 anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
193 };
194
195 let response = exec_state
197 .send_modeling_cmd(
198 args.into(),
199 ModelingCmd::from(mcmd::EntityGetAllChildUuids {
200 entity_id: new_geometry_id,
201 }),
202 )
203 .await?;
204 let OkWebSocketResponseData::Modeling {
205 modeling_response:
206 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
207 entity_ids: new_entity_ids,
208 }),
209 } = response
210 else {
211 anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
212 };
213
214 Ok(HashMap::from_iter(
216 old_entity_ids
217 .iter()
218 .zip(new_entity_ids.iter())
219 .map(|(old_id, new_id)| (*old_id, *new_id)),
220 ))
221}
222
223async fn fix_sketch_tags_and_references(
225 new_sketch: &mut Sketch,
226 entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
227 exec_state: &mut ExecState,
228) -> Result<()> {
229 for path in new_sketch.paths.as_mut_slice() {
231 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
232 path.set_id(*new_path_id);
233 } else {
234 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
237 }
238 }
239
240 for path in new_sketch.paths.clone() {
244 if let Some(tag) = path.get_tag() {
246 new_sketch.add_tag(&tag, &path, exec_state);
247 }
248 }
249
250 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
252 new_sketch.start.geo_meta.id = *new_base_path;
253 } else {
254 crate::log::logln!(
255 "Failed to find new base path id for old base path id: {:?}",
256 new_sketch.start.geo_meta.id
257 );
258 }
259
260 Ok(())
261}
262
263fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
265 let mut start_tag = None;
266 let mut end_tag = None;
267 if let Some(start_cap_id) = solid.start_cap_id {
269 for value in &solid.value {
271 if value.get_id() == start_cap_id {
272 start_tag = value.get_tag().clone();
273 break;
274 }
275 }
276 }
277
278 if let Some(end_cap_id) = solid.end_cap_id {
280 for value in &solid.value {
282 if value.get_id() == end_cap_id {
283 end_tag = value.get_tag().clone();
284 break;
285 }
286 }
287 }
288
289 (start_tag, end_tag)
290}
291
292#[cfg(test)]
293mod tests {
294 use pretty_assertions::{assert_eq, assert_ne};
295
296 use crate::exec::KclValue;
297
298 #[tokio::test(flavor = "multi_thread")]
301 async fn kcl_test_clone_sketch() {
302 let code = r#"cube = startSketchOn(XY)
303 |> startProfile(at = [0,0])
304 |> line(end = [0, 10])
305 |> line(end = [10, 0])
306 |> line(end = [0, -10])
307 |> close()
308
309clonedCube = clone(cube)
310"#;
311 let ctx = crate::test_server::new_context(true, None).await.unwrap();
312 let program = crate::Program::parse_no_errs(code).unwrap();
313
314 let result = ctx.run_with_caching(program.clone()).await.unwrap();
316 let cube = result.variables.get("cube").unwrap();
317 let cloned_cube = result.variables.get("clonedCube").unwrap();
318
319 assert_ne!(cube, cloned_cube);
320
321 let KclValue::Sketch { value: cube } = cube else {
322 panic!("Expected a sketch, got: {cube:?}");
323 };
324 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
325 panic!("Expected a sketch, got: {cloned_cube:?}");
326 };
327
328 assert_ne!(cube.id, cloned_cube.id);
329 assert_ne!(cube.original_id, cloned_cube.original_id);
330 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
331
332 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
333 assert_eq!(cloned_cube.original_id, cloned_cube.id);
334
335 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
336 assert_ne!(path.get_id(), cloned_path.get_id());
337 assert_eq!(path.get_tag(), cloned_path.get_tag());
338 }
339
340 assert_eq!(cube.tags.len(), 0);
341 assert_eq!(cloned_cube.tags.len(), 0);
342
343 ctx.close().await;
344 }
345
346 #[tokio::test(flavor = "multi_thread")]
349 async fn kcl_test_clone_solid() {
350 let code = r#"cube = startSketchOn(XY)
351 |> startProfile(at = [0,0])
352 |> line(end = [0, 10])
353 |> line(end = [10, 0])
354 |> line(end = [0, -10])
355 |> close()
356 |> extrude(length = 5)
357
358clonedCube = clone(cube)
359"#;
360 let ctx = crate::test_server::new_context(true, None).await.unwrap();
361 let program = crate::Program::parse_no_errs(code).unwrap();
362
363 let result = ctx.run_with_caching(program.clone()).await.unwrap();
365 let cube = result.variables.get("cube").unwrap();
366 let cloned_cube = result.variables.get("clonedCube").unwrap();
367
368 assert_ne!(cube, cloned_cube);
369
370 let KclValue::Solid { value: cube } = cube else {
371 panic!("Expected a solid, got: {cube:?}");
372 };
373 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
374 panic!("Expected a solid, got: {cloned_cube:?}");
375 };
376
377 assert_ne!(cube.id, cloned_cube.id);
378 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
379 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
380 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
381 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
382
383 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
384
385 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
386 assert_ne!(path.get_id(), cloned_path.get_id());
387 assert_eq!(path.get_tag(), cloned_path.get_tag());
388 }
389
390 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
391 assert_ne!(value.get_id(), cloned_value.get_id());
392 assert_eq!(value.get_tag(), cloned_value.get_tag());
393 }
394
395 assert_eq!(cube.sketch.tags.len(), 0);
396 assert_eq!(cloned_cube.sketch.tags.len(), 0);
397
398 assert_eq!(cube.edge_cuts.len(), 0);
399 assert_eq!(cloned_cube.edge_cuts.len(), 0);
400
401 ctx.close().await;
402 }
403
404 #[tokio::test(flavor = "multi_thread")]
408 async fn kcl_test_clone_sketch_with_tags() {
409 let code = r#"cube = startSketchOn(XY)
410 |> startProfile(at = [0,0]) // tag this one
411 |> line(end = [0, 10], tag = $tag02)
412 |> line(end = [10, 0], tag = $tag03)
413 |> line(end = [0, -10], tag = $tag04)
414 |> close(tag = $tag05)
415
416clonedCube = clone(cube)
417"#;
418 let ctx = crate::test_server::new_context(true, None).await.unwrap();
419 let program = crate::Program::parse_no_errs(code).unwrap();
420
421 let result = ctx.run_with_caching(program.clone()).await.unwrap();
423 let cube = result.variables.get("cube").unwrap();
424 let cloned_cube = result.variables.get("clonedCube").unwrap();
425
426 assert_ne!(cube, cloned_cube);
427
428 let KclValue::Sketch { value: cube } = cube else {
429 panic!("Expected a sketch, got: {cube:?}");
430 };
431 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
432 panic!("Expected a sketch, got: {cloned_cube:?}");
433 };
434
435 assert_ne!(cube.id, cloned_cube.id);
436 assert_ne!(cube.original_id, cloned_cube.original_id);
437
438 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
439 assert_ne!(path.get_id(), cloned_path.get_id());
440 assert_eq!(path.get_tag(), cloned_path.get_tag());
441 }
442
443 for (tag_name, tag) in &cube.tags {
444 let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
445
446 let tag_info = tag.get_cur_info().unwrap();
447 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
448
449 assert_ne!(tag_info.id, cloned_tag_info.id);
450 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
451 assert_ne!(tag_info.path, cloned_tag_info.path);
452 assert_eq!(tag_info.surface, None);
453 assert_eq!(cloned_tag_info.surface, None);
454 }
455
456 ctx.close().await;
457 }
458
459 #[tokio::test(flavor = "multi_thread")]
463 async fn kcl_test_clone_solid_with_tags() {
464 let code = r#"cube = startSketchOn(XY)
465 |> startProfile(at = [0,0]) // tag this one
466 |> line(end = [0, 10], tag = $tag02)
467 |> line(end = [10, 0], tag = $tag03)
468 |> line(end = [0, -10], tag = $tag04)
469 |> close(tag = $tag05)
470 |> extrude(length = 5) // TODO: Tag these
471
472clonedCube = clone(cube)
473"#;
474 let ctx = crate::test_server::new_context(true, None).await.unwrap();
475 let program = crate::Program::parse_no_errs(code).unwrap();
476
477 let result = ctx.run_with_caching(program.clone()).await.unwrap();
479 let cube = result.variables.get("cube").unwrap();
480 let cloned_cube = result.variables.get("clonedCube").unwrap();
481
482 assert_ne!(cube, cloned_cube);
483
484 let KclValue::Solid { value: cube } = cube else {
485 panic!("Expected a solid, got: {cube:?}");
486 };
487 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
488 panic!("Expected a solid, got: {cloned_cube:?}");
489 };
490
491 assert_ne!(cube.id, cloned_cube.id);
492 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
493 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
494 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
495 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
496
497 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
498
499 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
500 assert_ne!(path.get_id(), cloned_path.get_id());
501 assert_eq!(path.get_tag(), cloned_path.get_tag());
502 }
503
504 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
505 assert_ne!(value.get_id(), cloned_value.get_id());
506 assert_eq!(value.get_tag(), cloned_value.get_tag());
507 }
508
509 for (tag_name, tag) in &cube.sketch.tags {
510 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
511
512 let tag_info = tag.get_cur_info().unwrap();
513 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
514
515 assert_ne!(tag_info.id, cloned_tag_info.id);
516 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
517 assert_ne!(tag_info.path, cloned_tag_info.path);
518 assert_ne!(tag_info.surface, cloned_tag_info.surface);
519 }
520
521 assert_eq!(cube.edge_cuts.len(), 0);
522 assert_eq!(cloned_cube.edge_cuts.len(), 0);
523
524 ctx.close().await;
525 }
526
527 #[tokio::test(flavor = "multi_thread")]
529 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
530 async fn kcl_test_clone_cube_already_closed_sketch() {
531 let code = r#"// Clone a basic solid and move it.
532
533exampleSketch = startSketchOn(XY)
534 |> startProfile(at = [0, 0])
535 |> line(end = [10, 0])
536 |> line(end = [0, 10])
537 |> line(end = [-10, 0])
538 |> line(end = [0, -10])
539 |> close()
540
541cube = extrude(exampleSketch, length = 5)
542clonedCube = clone(cube)
543 |> translate(
544 x = 25.0,
545 )"#;
546 let ctx = crate::test_server::new_context(true, None).await.unwrap();
547 let program = crate::Program::parse_no_errs(code).unwrap();
548
549 let result = ctx.run_with_caching(program.clone()).await.unwrap();
551 let cube = result.variables.get("cube").unwrap();
552 let cloned_cube = result.variables.get("clonedCube").unwrap();
553
554 assert_ne!(cube, cloned_cube);
555
556 let KclValue::Solid { value: cube } = cube else {
557 panic!("Expected a solid, got: {cube:?}");
558 };
559 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
560 panic!("Expected a solid, got: {cloned_cube:?}");
561 };
562
563 assert_ne!(cube.id, cloned_cube.id);
564 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
565 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
566 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
567 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
568
569 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
570
571 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
572 assert_ne!(path.get_id(), cloned_path.get_id());
573 assert_eq!(path.get_tag(), cloned_path.get_tag());
574 }
575
576 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
577 assert_ne!(value.get_id(), cloned_value.get_id());
578 assert_eq!(value.get_tag(), cloned_value.get_tag());
579 }
580
581 for (tag_name, tag) in &cube.sketch.tags {
582 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
583
584 let tag_info = tag.get_cur_info().unwrap();
585 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
586
587 assert_ne!(tag_info.id, cloned_tag_info.id);
588 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
589 assert_ne!(tag_info.path, cloned_tag_info.path);
590 assert_ne!(tag_info.surface, cloned_tag_info.surface);
591 }
592
593 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
594 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
595 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
596 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
597 }
598
599 ctx.close().await;
600 }
601
602 #[tokio::test(flavor = "multi_thread")]
606 #[ignore] async fn kcl_test_clone_solid_with_edge_cuts() {
608 let code = r#"cube = startSketchOn(XY)
609 |> startProfile(at = [0,0]) // tag this one
610 |> line(end = [0, 10], tag = $tag02)
611 |> line(end = [10, 0], tag = $tag03)
612 |> line(end = [0, -10], tag = $tag04)
613 |> close(tag = $tag05)
614 |> extrude(length = 5) // TODO: Tag these
615 |> fillet(
616 radius = 2,
617 tags = [
618 getNextAdjacentEdge(tag02),
619 ],
620 tag = $fillet01,
621 )
622 |> fillet(
623 radius = 2,
624 tags = [
625 getNextAdjacentEdge(tag04),
626 ],
627 tag = $fillet02,
628 )
629 |> chamfer(
630 length = 2,
631 tags = [
632 getNextAdjacentEdge(tag03),
633 ],
634 tag = $chamfer01,
635 )
636 |> chamfer(
637 length = 2,
638 tags = [
639 getNextAdjacentEdge(tag05),
640 ],
641 tag = $chamfer02,
642 )
643
644clonedCube = clone(cube)
645"#;
646 let ctx = crate::test_server::new_context(true, None).await.unwrap();
647 let program = crate::Program::parse_no_errs(code).unwrap();
648
649 let result = ctx.run_with_caching(program.clone()).await.unwrap();
651 let cube = result.variables.get("cube").unwrap();
652 let cloned_cube = result.variables.get("clonedCube").unwrap();
653
654 assert_ne!(cube, cloned_cube);
655
656 let KclValue::Solid { value: cube } = cube else {
657 panic!("Expected a solid, got: {cube:?}");
658 };
659 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
660 panic!("Expected a solid, got: {cloned_cube:?}");
661 };
662
663 assert_ne!(cube.id, cloned_cube.id);
664 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
665 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
666 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
667 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
668
669 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
670
671 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
672 assert_ne!(value.get_id(), cloned_value.get_id());
673 assert_eq!(value.get_tag(), cloned_value.get_tag());
674 }
675
676 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
677 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
678 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
679 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
680 }
681
682 ctx.close().await;
683 }
684}