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::{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(), std::slice::from_ref(solid))
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 solid.sectional,
149 &NamedCapTags {
150 start: start_tag.as_ref(),
151 end: end_tag.as_ref(),
152 },
153 kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
154 exec_state,
155 args,
156 None,
157 )
158 .await?;
159
160 *solid = new_solid;
161 }
162 }
163
164 Ok(())
165}
166
167async fn get_old_new_child_map(
168 new_geometry_id: uuid::Uuid,
169 old_geometry_id: uuid::Uuid,
170 exec_state: &mut ExecState,
171 args: &Args,
172) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
173 let response = exec_state
175 .send_modeling_cmd(
176 args.into(),
177 ModelingCmd::from(mcmd::EntityGetAllChildUuids {
178 entity_id: old_geometry_id,
179 }),
180 )
181 .await?;
182 let OkWebSocketResponseData::Modeling {
183 modeling_response:
184 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
185 entity_ids: old_entity_ids,
186 }),
187 } = response
188 else {
189 anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
190 };
191
192 let response = exec_state
194 .send_modeling_cmd(
195 args.into(),
196 ModelingCmd::from(mcmd::EntityGetAllChildUuids {
197 entity_id: new_geometry_id,
198 }),
199 )
200 .await?;
201 let OkWebSocketResponseData::Modeling {
202 modeling_response:
203 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
204 entity_ids: new_entity_ids,
205 }),
206 } = response
207 else {
208 anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
209 };
210
211 Ok(HashMap::from_iter(
213 old_entity_ids
214 .iter()
215 .zip(new_entity_ids.iter())
216 .map(|(old_id, new_id)| (*old_id, *new_id)),
217 ))
218}
219
220async fn fix_sketch_tags_and_references(
222 new_sketch: &mut Sketch,
223 entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
224 exec_state: &mut ExecState,
225) -> Result<()> {
226 for path in new_sketch.paths.as_mut_slice() {
228 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
229 path.set_id(*new_path_id);
230 } else {
231 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
234 }
235 }
236
237 for path in new_sketch.paths.clone() {
241 if let Some(tag) = path.get_tag() {
243 new_sketch.add_tag(&tag, &path, exec_state);
244 }
245 }
246
247 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
249 new_sketch.start.geo_meta.id = *new_base_path;
250 } else {
251 crate::log::logln!(
252 "Failed to find new base path id for old base path id: {:?}",
253 new_sketch.start.geo_meta.id
254 );
255 }
256
257 Ok(())
258}
259
260fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
262 let mut start_tag = None;
263 let mut end_tag = None;
264 if let Some(start_cap_id) = solid.start_cap_id {
266 for value in &solid.value {
268 if value.get_id() == start_cap_id {
269 start_tag = value.get_tag().clone();
270 break;
271 }
272 }
273 }
274
275 if let Some(end_cap_id) = solid.end_cap_id {
277 for value in &solid.value {
279 if value.get_id() == end_cap_id {
280 end_tag = value.get_tag().clone();
281 break;
282 }
283 }
284 }
285
286 (start_tag, end_tag)
287}
288
289#[cfg(test)]
290mod tests {
291 use pretty_assertions::{assert_eq, assert_ne};
292
293 use crate::exec::KclValue;
294
295 #[tokio::test(flavor = "multi_thread")]
298 async fn kcl_test_clone_sketch() {
299 let code = r#"cube = startSketchOn(XY)
300 |> startProfile(at = [0,0])
301 |> line(end = [0, 10])
302 |> line(end = [10, 0])
303 |> line(end = [0, -10])
304 |> close()
305
306clonedCube = clone(cube)
307"#;
308 let ctx = crate::test_server::new_context(true, None).await.unwrap();
309 let program = crate::Program::parse_no_errs(code).unwrap();
310
311 let result = ctx.run_with_caching(program.clone()).await.unwrap();
313 let cube = result.variables.get("cube").unwrap();
314 let cloned_cube = result.variables.get("clonedCube").unwrap();
315
316 assert_ne!(cube, cloned_cube);
317
318 let KclValue::Sketch { value: cube } = cube else {
319 panic!("Expected a sketch, got: {cube:?}");
320 };
321 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
322 panic!("Expected a sketch, got: {cloned_cube:?}");
323 };
324
325 assert_ne!(cube.id, cloned_cube.id);
326 assert_ne!(cube.original_id, cloned_cube.original_id);
327 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
328
329 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
330 assert_eq!(cloned_cube.original_id, cloned_cube.id);
331
332 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
333 assert_ne!(path.get_id(), cloned_path.get_id());
334 assert_eq!(path.get_tag(), cloned_path.get_tag());
335 }
336
337 assert_eq!(cube.tags.len(), 0);
338 assert_eq!(cloned_cube.tags.len(), 0);
339
340 ctx.close().await;
341 }
342
343 #[tokio::test(flavor = "multi_thread")]
346 async fn kcl_test_clone_solid() {
347 let code = r#"cube = startSketchOn(XY)
348 |> startProfile(at = [0,0])
349 |> line(end = [0, 10])
350 |> line(end = [10, 0])
351 |> line(end = [0, -10])
352 |> close()
353 |> extrude(length = 5)
354
355clonedCube = clone(cube)
356"#;
357 let ctx = crate::test_server::new_context(true, None).await.unwrap();
358 let program = crate::Program::parse_no_errs(code).unwrap();
359
360 let result = ctx.run_with_caching(program.clone()).await.unwrap();
362 let cube = result.variables.get("cube").unwrap();
363 let cloned_cube = result.variables.get("clonedCube").unwrap();
364
365 assert_ne!(cube, cloned_cube);
366
367 let KclValue::Solid { value: cube } = cube else {
368 panic!("Expected a solid, got: {cube:?}");
369 };
370 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
371 panic!("Expected a solid, got: {cloned_cube:?}");
372 };
373
374 assert_ne!(cube.id, cloned_cube.id);
375 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
376 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
377 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
378 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
379
380 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
381
382 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
383 assert_ne!(path.get_id(), cloned_path.get_id());
384 assert_eq!(path.get_tag(), cloned_path.get_tag());
385 }
386
387 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
388 assert_ne!(value.get_id(), cloned_value.get_id());
389 assert_eq!(value.get_tag(), cloned_value.get_tag());
390 }
391
392 assert_eq!(cube.sketch.tags.len(), 0);
393 assert_eq!(cloned_cube.sketch.tags.len(), 0);
394
395 assert_eq!(cube.edge_cuts.len(), 0);
396 assert_eq!(cloned_cube.edge_cuts.len(), 0);
397
398 ctx.close().await;
399 }
400
401 #[tokio::test(flavor = "multi_thread")]
405 async fn kcl_test_clone_sketch_with_tags() {
406 let code = r#"cube = startSketchOn(XY)
407 |> startProfile(at = [0,0]) // tag this one
408 |> line(end = [0, 10], tag = $tag02)
409 |> line(end = [10, 0], tag = $tag03)
410 |> line(end = [0, -10], tag = $tag04)
411 |> close(tag = $tag05)
412
413clonedCube = clone(cube)
414"#;
415 let ctx = crate::test_server::new_context(true, None).await.unwrap();
416 let program = crate::Program::parse_no_errs(code).unwrap();
417
418 let result = ctx.run_with_caching(program.clone()).await.unwrap();
420 let cube = result.variables.get("cube").unwrap();
421 let cloned_cube = result.variables.get("clonedCube").unwrap();
422
423 assert_ne!(cube, cloned_cube);
424
425 let KclValue::Sketch { value: cube } = cube else {
426 panic!("Expected a sketch, got: {cube:?}");
427 };
428 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
429 panic!("Expected a sketch, got: {cloned_cube:?}");
430 };
431
432 assert_ne!(cube.id, cloned_cube.id);
433 assert_ne!(cube.original_id, cloned_cube.original_id);
434
435 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
436 assert_ne!(path.get_id(), cloned_path.get_id());
437 assert_eq!(path.get_tag(), cloned_path.get_tag());
438 }
439
440 for (tag_name, tag) in &cube.tags {
441 let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
442
443 let tag_info = tag.get_cur_info().unwrap();
444 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
445
446 assert_ne!(tag_info.id, cloned_tag_info.id);
447 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
448 assert_ne!(tag_info.path, cloned_tag_info.path);
449 assert_eq!(tag_info.surface, None);
450 assert_eq!(cloned_tag_info.surface, None);
451 }
452
453 ctx.close().await;
454 }
455
456 #[tokio::test(flavor = "multi_thread")]
460 async fn kcl_test_clone_solid_with_tags() {
461 let code = r#"cube = startSketchOn(XY)
462 |> startProfile(at = [0,0]) // tag this one
463 |> line(end = [0, 10], tag = $tag02)
464 |> line(end = [10, 0], tag = $tag03)
465 |> line(end = [0, -10], tag = $tag04)
466 |> close(tag = $tag05)
467 |> extrude(length = 5) // TODO: Tag these
468
469clonedCube = clone(cube)
470"#;
471 let ctx = crate::test_server::new_context(true, None).await.unwrap();
472 let program = crate::Program::parse_no_errs(code).unwrap();
473
474 let result = ctx.run_with_caching(program.clone()).await.unwrap();
476 let cube = result.variables.get("cube").unwrap();
477 let cloned_cube = result.variables.get("clonedCube").unwrap();
478
479 assert_ne!(cube, cloned_cube);
480
481 let KclValue::Solid { value: cube } = cube else {
482 panic!("Expected a solid, got: {cube:?}");
483 };
484 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
485 panic!("Expected a solid, got: {cloned_cube:?}");
486 };
487
488 assert_ne!(cube.id, cloned_cube.id);
489 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
490 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
491 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
492 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
493
494 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
495
496 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
497 assert_ne!(path.get_id(), cloned_path.get_id());
498 assert_eq!(path.get_tag(), cloned_path.get_tag());
499 }
500
501 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
502 assert_ne!(value.get_id(), cloned_value.get_id());
503 assert_eq!(value.get_tag(), cloned_value.get_tag());
504 }
505
506 for (tag_name, tag) in &cube.sketch.tags {
507 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
508
509 let tag_info = tag.get_cur_info().unwrap();
510 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
511
512 assert_ne!(tag_info.id, cloned_tag_info.id);
513 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
514 assert_ne!(tag_info.path, cloned_tag_info.path);
515 assert_ne!(tag_info.surface, cloned_tag_info.surface);
516 }
517
518 assert_eq!(cube.edge_cuts.len(), 0);
519 assert_eq!(cloned_cube.edge_cuts.len(), 0);
520
521 ctx.close().await;
522 }
523
524 #[tokio::test(flavor = "multi_thread")]
526 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
527 async fn kcl_test_clone_cube_already_closed_sketch() {
528 let code = r#"// Clone a basic solid and move it.
529
530exampleSketch = startSketchOn(XY)
531 |> startProfile(at = [0, 0])
532 |> line(end = [10, 0])
533 |> line(end = [0, 10])
534 |> line(end = [-10, 0])
535 |> line(end = [0, -10])
536 |> close()
537
538cube = extrude(exampleSketch, length = 5)
539clonedCube = clone(cube)
540 |> translate(
541 x = 25.0,
542 )"#;
543 let ctx = crate::test_server::new_context(true, None).await.unwrap();
544 let program = crate::Program::parse_no_errs(code).unwrap();
545
546 let result = ctx.run_with_caching(program.clone()).await.unwrap();
548 let cube = result.variables.get("cube").unwrap();
549 let cloned_cube = result.variables.get("clonedCube").unwrap();
550
551 assert_ne!(cube, cloned_cube);
552
553 let KclValue::Solid { value: cube } = cube else {
554 panic!("Expected a solid, got: {cube:?}");
555 };
556 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
557 panic!("Expected a solid, got: {cloned_cube:?}");
558 };
559
560 assert_ne!(cube.id, cloned_cube.id);
561 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
562 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
563 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
564 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
565
566 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
567
568 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
569 assert_ne!(path.get_id(), cloned_path.get_id());
570 assert_eq!(path.get_tag(), cloned_path.get_tag());
571 }
572
573 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
574 assert_ne!(value.get_id(), cloned_value.get_id());
575 assert_eq!(value.get_tag(), cloned_value.get_tag());
576 }
577
578 for (tag_name, tag) in &cube.sketch.tags {
579 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
580
581 let tag_info = tag.get_cur_info().unwrap();
582 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
583
584 assert_ne!(tag_info.id, cloned_tag_info.id);
585 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
586 assert_ne!(tag_info.path, cloned_tag_info.path);
587 assert_ne!(tag_info.surface, cloned_tag_info.surface);
588 }
589
590 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
591 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
592 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
593 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
594 }
595
596 ctx.close().await;
597 }
598
599 #[tokio::test(flavor = "multi_thread")]
603 #[ignore] async fn kcl_test_clone_solid_with_edge_cuts() {
605 let code = r#"cube = startSketchOn(XY)
606 |> startProfile(at = [0,0]) // tag this one
607 |> line(end = [0, 10], tag = $tag02)
608 |> line(end = [10, 0], tag = $tag03)
609 |> line(end = [0, -10], tag = $tag04)
610 |> close(tag = $tag05)
611 |> extrude(length = 5) // TODO: Tag these
612 |> fillet(
613 radius = 2,
614 tags = [
615 getNextAdjacentEdge(tag02),
616 ],
617 tag = $fillet01,
618 )
619 |> fillet(
620 radius = 2,
621 tags = [
622 getNextAdjacentEdge(tag04),
623 ],
624 tag = $fillet02,
625 )
626 |> chamfer(
627 length = 2,
628 tags = [
629 getNextAdjacentEdge(tag03),
630 ],
631 tag = $chamfer01,
632 )
633 |> chamfer(
634 length = 2,
635 tags = [
636 getNextAdjacentEdge(tag05),
637 ],
638 tag = $chamfer02,
639 )
640
641clonedCube = clone(cube)
642"#;
643 let ctx = crate::test_server::new_context(true, None).await.unwrap();
644 let program = crate::Program::parse_no_errs(code).unwrap();
645
646 let result = ctx.run_with_caching(program.clone()).await.unwrap();
648 let cube = result.variables.get("cube").unwrap();
649 let cloned_cube = result.variables.get("clonedCube").unwrap();
650
651 assert_ne!(cube, cloned_cube);
652
653 let KclValue::Solid { value: cube } = cube else {
654 panic!("Expected a solid, got: {cube:?}");
655 };
656 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
657 panic!("Expected a solid, got: {cloned_cube:?}");
658 };
659
660 assert_ne!(cube.id, cloned_cube.id);
661 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
662 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
663 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
664 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
665
666 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
667
668 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
669 assert_ne!(value.get_id(), cloned_value.get_id());
670 assert_eq!(value.get_tag(), cloned_value.get_tag());
671 }
672
673 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
674 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
675 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
676 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
677 }
678
679 ctx.close().await;
680 }
681}