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