Skip to main content

kcl_lib/std/
clone.rs

1//! Standard library clone.
2
3use std::collections::HashMap;
4use std::collections::HashSet;
5
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kcmc::ok_response::OkModelingCmdResponse;
9use kcmc::websocket::OkWebSocketResponseData;
10use kittycad_modeling_cmds::{self as kcmc};
11
12use super::extrude::do_post_extrude;
13use crate::errors::KclError;
14use crate::errors::KclErrorDetails;
15use crate::execution::CreatorFace;
16use crate::execution::EntityCloneInfo;
17use crate::execution::ExecState;
18use crate::execution::ExtrudeSurface;
19use crate::execution::Geometry;
20use crate::execution::GeometryWithImportedGeometry;
21use crate::execution::KclValue;
22use crate::execution::Metadata;
23use crate::execution::ModelingCmdMeta;
24use crate::execution::NewBodyKind;
25use crate::execution::Sketch;
26use crate::execution::Solid;
27use crate::execution::SolidCreator;
28use crate::execution::TagEngineInfo;
29use crate::execution::TagIdentifier;
30use crate::execution::types::ArrayLen;
31use crate::execution::types::PrimitiveType;
32use crate::execution::types::RuntimeType;
33use crate::parsing::ast::types::TagNode;
34use crate::std::Args;
35use crate::std::extrude::BeingExtruded;
36use crate::std::extrude::NamedCapTags;
37
38type Result<T> = std::result::Result<T, KclError>;
39
40/// Clone a sketch or solid.
41///
42/// This works essentially like a copy-paste operation.
43pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
44    let geometries = args.get_unlabeled_kw_arg(
45        "geometry",
46        &RuntimeType::Array(
47            Box::new(RuntimeType::Union(vec![
48                RuntimeType::Primitive(PrimitiveType::Sketch),
49                RuntimeType::Primitive(PrimitiveType::Solid),
50                RuntimeType::imported(),
51            ])),
52            ArrayLen::Minimum(1),
53        ),
54        exec_state,
55    )?;
56
57    let cloned = inner_clone(geometries, exec_state, args).await?;
58    Ok(cloned.into())
59}
60
61async fn inner_clone(
62    geometries: Vec<GeometryWithImportedGeometry>,
63    exec_state: &mut ExecState,
64    args: Args,
65) -> Result<Vec<GeometryWithImportedGeometry>> {
66    let mut res = vec![];
67
68    for g in geometries {
69        let new_id = exec_state.next_uuid();
70        let mut geometry = g.clone();
71        let old_id = geometry.id(&args.ctx).await?;
72        // Pattern copies have a new top-level entity ID, but their KCL
73        // geometry still describes the source topology. Map that source
74        // topology directly to the clone so paths and tagged faces receive
75        // the clone's child IDs.
76        let source_topology_id = match &geometry {
77            GeometryWithImportedGeometry::Sketch(sketch) => sketch.original_id,
78            GeometryWithImportedGeometry::Solid(solid) => solid.topology_id(),
79            GeometryWithImportedGeometry::ImportedGeometry(_) => old_id,
80        };
81        let mut entity_clone_info = None;
82
83        let mut new_geometry = match &geometry {
84            GeometryWithImportedGeometry::ImportedGeometry(imported) => {
85                let mut new_imported = imported.clone();
86                new_imported.id = new_id;
87                GeometryWithImportedGeometry::ImportedGeometry(new_imported)
88            }
89            GeometryWithImportedGeometry::Sketch(sketch) => {
90                let mut new_sketch = sketch.clone();
91                new_sketch.id = new_id;
92                new_sketch.original_id = new_id;
93                new_sketch.artifact_id = new_id.into();
94                GeometryWithImportedGeometry::Sketch(new_sketch)
95            }
96            GeometryWithImportedGeometry::Solid(solid) => {
97                // We flush before the clone so all the shit exists.
98                exec_state
99                    .flush_batch_for_solids(
100                        ModelingCmdMeta::from_args(exec_state, &args),
101                        std::slice::from_ref(solid),
102                    )
103                    .await?;
104
105                let mut new_solid = solid.clone();
106                // Sweep-backed solids have separate engine entity and body
107                // artifact IDs. Preserve that split so the cloned Path and
108                // Sweep can coexist. Pattern copies replace `artifact_id`
109                // with their own entity ID, so consult their retained source
110                // artifact to recover the same distinction.
111                let source_artifact_id = solid.pattern_source_artifact_id.unwrap_or(solid.artifact_id);
112                let result_artifact_id = if source_artifact_id == source_topology_id.into() {
113                    new_id.into()
114                } else {
115                    exec_state.next_artifact_id()
116                };
117                entity_clone_info = Some(EntityCloneInfo {
118                    source_artifact_id,
119                    result_artifact_id,
120                    source_topology_id: source_topology_id.into(),
121                });
122                new_solid.id = new_id;
123                new_solid.value_id = new_id;
124                new_solid.become_new_body(new_id, result_artifact_id, NewBodyKind::IndependentCopy);
125                if let Some(sketch) = new_solid.sketch_mut() {
126                    sketch.original_id = new_id;
127                }
128                GeometryWithImportedGeometry::Solid(new_solid)
129            }
130        };
131
132        if args.ctx.no_engine_commands().await {
133            res.push(new_geometry);
134        } else {
135            exec_state
136                .batch_modeling_cmd_with_entity_clone_info(
137                    ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
138                    ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
139                    entity_clone_info,
140                )
141                .await?;
142
143            fix_tags_and_references(&mut new_geometry, old_id, source_topology_id, exec_state, &args).await?;
144            res.push(new_geometry)
145        }
146    }
147
148    Ok(res)
149}
150/// Fix the tags and references of the cloned geometry.
151pub(super) async fn fix_tags_and_references(
152    new_geometry: &mut GeometryWithImportedGeometry,
153    old_geometry_id: uuid::Uuid,
154    source_topology_id: uuid::Uuid,
155    exec_state: &mut ExecState,
156    args: &Args,
157) -> Result<()> {
158    let new_geometry_id = new_geometry.id(&args.ctx).await?;
159    let entity_id_map =
160        get_old_new_child_map(new_geometry_id, old_geometry_id, source_topology_id, exec_state, args).await?;
161
162    // Fix the path references in the new geometry.
163    match new_geometry {
164        GeometryWithImportedGeometry::ImportedGeometry(_) => {}
165        GeometryWithImportedGeometry::Sketch(sketch) => {
166            sketch.clone = Some(source_topology_id);
167            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, None).await?;
168        }
169        GeometryWithImportedGeometry::Solid(solid) => {
170            let body_type = match solid.best_guess_body_type {
171                Some(body_type) => body_type,
172                None => super::surfaces::query_body_type(solid, exec_state, args).await?,
173            };
174            solid.best_guess_body_type = Some(body_type);
175
176            let (start_tag, end_tag) = get_named_cap_tags(solid);
177            let solid_value = solid.value.clone();
178            let solid_artifact_id = solid.artifact_id;
179            let old_face_tag_names = solid.faces.keys().cloned().collect::<Vec<_>>();
180            let face_creator = match &solid.creator {
181                SolidCreator::Face(face) => Some((
182                    remap_id(face.face_id, &entity_id_map),
183                    remap_id(face.solid_id, &entity_id_map),
184                )),
185                _ => None,
186            };
187
188            if solid.sketch().is_none() {
189                remap_edge_cuts(solid, &entity_id_map);
190                remap_sketchless_solid(solid, &entity_id_map);
191                solid.faces.clear();
192                restore_face_tags(solid, &old_face_tag_names, exec_state);
193                return Ok(());
194            }
195
196            // Make the sketch id the new geometry id.
197            let sketch = solid.sketch_mut().ok_or_else(|| {
198                KclError::new_internal(KclErrorDetails::new(
199                    "A sketch-backed clone lost its creator sketch during metadata reconstruction.".to_owned(),
200                    vec![args.source_range],
201                ))
202            })?;
203            sketch.id = new_geometry_id;
204            sketch.original_id = new_geometry_id;
205            sketch.artifact_id = new_geometry_id.into();
206            sketch.clone = Some(source_topology_id);
207
208            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, Some(solid_value)).await?;
209            let sketch_for_post = sketch.clone();
210
211            // Do the after extrude things to update those ids, based on the new sketch
212            // information.
213            let mut new_solid = do_post_extrude(
214                &sketch_for_post,
215                solid_artifact_id,
216                solid.sectional,
217                &NamedCapTags {
218                    start: start_tag.as_ref(),
219                    end: end_tag.as_ref(),
220                },
221                kittycad_modeling_cmds::shared::ExtrudeMethod::New,
222                exec_state,
223                args,
224                None,
225                Some(&entity_id_map.clone()),
226                body_type,
227                BeingExtruded::Sketch,
228            )
229            .await?;
230
231            if !old_face_tag_names.is_empty() {
232                // Preserve tagged faces when edge cuts leave the extrusion query empty.
233                // Untagged solids keep the existing reconstruction behavior.
234                let mut seen_ids = new_solid
235                    .value
236                    .iter()
237                    .map(|surface| surface.get_id())
238                    .collect::<HashSet<_>>();
239                for surface in solid.value.iter().filter(|surface| surface.get_tag().is_some()) {
240                    let (Some(id), Some(face_id)) = (
241                        entity_id_map.get(&surface.get_id()).copied(),
242                        entity_id_map.get(&surface.face_id()).copied(),
243                    ) else {
244                        continue;
245                    };
246                    if !seen_ids.insert(id) {
247                        continue;
248                    }
249                    if solid.start_cap_id == Some(surface.face_id()) {
250                        new_solid.start_cap_id.get_or_insert(face_id);
251                    }
252                    if solid.end_cap_id == Some(surface.face_id()) {
253                        new_solid.end_cap_id.get_or_insert(face_id);
254                    }
255                    let mut surface = surface.clone();
256                    surface.set_id(id);
257                    surface.set_face_id(face_id);
258                    new_solid.value.push(surface);
259                }
260            }
261
262            if let Some((face_id, solid_id)) = face_creator {
263                let rebuilt_sketch = new_solid.sketch().cloned().ok_or_else(|| {
264                    KclError::new_internal(KclErrorDetails::new(
265                        "A face-created clone lost its creator sketch during metadata reconstruction.".to_owned(),
266                        vec![args.source_range],
267                    ))
268                })?;
269                new_solid.creator = SolidCreator::Face(CreatorFace {
270                    face_id,
271                    solid_id,
272                    sketch: rebuilt_sketch,
273                });
274            }
275
276            restore_sketch_tag_surfaces(&mut new_solid);
277            *solid = new_solid;
278
279            restore_face_tags(solid, &old_face_tag_names, exec_state);
280        }
281    }
282
283    Ok(())
284}
285
286fn remap_id(id: uuid::Uuid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) -> uuid::Uuid {
287    entity_id_map.get(&id).copied().unwrap_or(id)
288}
289
290fn remap_edge_cuts(solid: &mut Solid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) {
291    for edge_cut in &mut solid.edge_cuts {
292        edge_cut.set_id(remap_id(edge_cut.id(), entity_id_map));
293        edge_cut.set_edge_id(remap_id(edge_cut.edge_id(), entity_id_map));
294    }
295    for id in &mut solid.pending_edge_cut_ids {
296        *id = remap_id(*id, entity_id_map);
297    }
298}
299
300fn remap_sketchless_solid(solid: &mut Solid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) {
301    for surface in &mut solid.value {
302        surface.set_id(remap_id(surface.get_id(), entity_id_map));
303        surface.set_face_id(remap_id(surface.face_id(), entity_id_map));
304    }
305
306    solid.start_cap_id = solid.start_cap_id.map(|id| remap_id(id, entity_id_map));
307    solid.end_cap_id = solid.end_cap_id.map(|id| remap_id(id, entity_id_map));
308
309    if let SolidCreator::Edge(creator) = &mut solid.creator {
310        creator.edge_id = remap_id(creator.edge_id, entity_id_map);
311        creator.body_id = remap_id(creator.body_id, entity_id_map);
312    }
313}
314
315/// Restore any sketch tag surfaces that could not be mapped from stale source
316/// metadata before [`do_post_extrude`] rebuilt the cloned solid's surfaces.
317fn restore_sketch_tag_surfaces(solid: &mut Solid) {
318    let surfaces_by_tag = solid
319        .value
320        .iter()
321        .filter_map(|surface| surface.get_tag().map(|tag| (tag.name.clone(), surface.clone())))
322        .collect::<HashMap<_, _>>();
323    let Some(sketch) = solid.sketch_mut() else {
324        return;
325    };
326
327    for (name, tag) in &mut sketch.tags {
328        let Some(surface) = surfaces_by_tag.get(name) else {
329            continue;
330        };
331        let Some((_, info)) = tag.info.last_mut() else {
332            continue;
333        };
334        if info.surface.is_none() {
335            info.surface = Some(surface.clone());
336        }
337    }
338}
339
340/// Rebuild the face tag map of a cloned solid from its new surfaces.
341///
342/// [`do_post_extrude`] leaves `faces` empty, and we can't reuse the sketch's
343/// tags like tagging at creation time does, because the cloned sketch still
344/// carries the original solid's face info. Build fresh tag identifiers from
345/// the new surfaces, which have the clone's face ids.
346fn restore_face_tags(solid: &mut Solid, face_tag_names: &[String], exec_state: &ExecState) {
347    let surfaces = solid.value.clone();
348    for surface in surfaces {
349        let Some(tag) = surface.get_tag() else {
350            continue;
351        };
352        if !face_tag_names.iter().any(|tag_name| tag_name == &tag.name) {
353            continue;
354        }
355
356        let mut solid_copy = solid.clone();
357        if let Some(sketch) = solid_copy.sketch_mut() {
358            // Avoid recursive tags.
359            sketch.tags.clear();
360        }
361        solid_copy.faces.clear();
362
363        let tag_id = TagIdentifier {
364            value: tag.name.clone(),
365            info: vec![(
366                exec_state.stack().current_epoch(),
367                TagEngineInfo {
368                    id: surface.get_id(),
369                    surface: Some(surface.clone()),
370                    path: None,
371                    geometry: Geometry::Solid(solid_copy),
372                },
373            )],
374            meta: vec![Metadata {
375                source_range: tag.clone().into(),
376            }],
377        };
378
379        match solid.faces.get_mut(&tag.name) {
380            Some(existing_tag) => existing_tag.merge_info(&tag_id),
381            None => {
382                solid.faces.insert(tag.name.clone(), tag_id);
383            }
384        }
385    }
386
387    for name in face_tag_names {
388        if !solid.faces.contains_key(name) {
389            crate::log::logln!("Failed to find new face for face tag: {name:?}");
390        }
391    }
392}
393
394async fn get_old_new_child_map(
395    new_geometry_id: uuid::Uuid,
396    old_geometry_id: uuid::Uuid,
397    source_topology_id: uuid::Uuid,
398    exec_state: &mut ExecState,
399    args: &Args,
400) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
401    // Artifact graph ID management expects the cloned entity's own children
402    // to be queried first. Pattern copies retain the source topology in KCL,
403    // though, so use that topology for the runtime old-to-new ID map.
404    if old_geometry_id != source_topology_id {
405        get_all_child_uuids(old_geometry_id, exec_state, args).await?;
406    }
407
408    // Get the old geometries entity ids.
409    let old_entity_ids = get_all_child_uuids(source_topology_id, exec_state, args).await?;
410
411    // Get the new geometries entity ids.
412    let new_entity_ids = get_all_child_uuids(new_geometry_id, exec_state, args).await?;
413
414    // Create a map of old entity ids to new entity ids.
415    let mut entity_id_map = HashMap::from_iter(
416        old_entity_ids
417            .iter()
418            .zip(new_entity_ids.iter())
419            .map(|(old_id, new_id)| (*old_id, *new_id)),
420    );
421    entity_id_map.insert(old_geometry_id, new_geometry_id);
422    entity_id_map.insert(source_topology_id, new_geometry_id);
423    Ok(entity_id_map)
424}
425
426async fn get_all_child_uuids(
427    geometry_id: uuid::Uuid,
428    exec_state: &mut ExecState,
429    args: &Args,
430) -> Result<Vec<uuid::Uuid>> {
431    let response = exec_state
432        .send_modeling_cmd(
433            ModelingCmdMeta::from_args(exec_state, args),
434            ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(geometry_id).build()),
435        )
436        .await?;
437    let OkWebSocketResponseData::Modeling {
438        modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(resp),
439    } = response
440    else {
441        return Err(KclError::new_engine(KclErrorDetails::new(
442            format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
443            vec![args.source_range],
444        )));
445    };
446    Ok(resp.entity_ids)
447}
448
449/// Fix the tags and references of a sketch.
450async fn fix_sketch_tags_and_references(
451    new_sketch: &mut Sketch,
452    entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
453    exec_state: &mut ExecState,
454    surfaces: Option<Vec<ExtrudeSurface>>,
455) -> Result<()> {
456    // Fix the path references in the sketch.
457    for path in &mut new_sketch.paths {
458        if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
459            path.set_id(*new_path_id);
460        } else {
461            // We log on these because we might have already flushed and the id is no longer
462            // relevant since filleted or something.
463            crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
464        }
465    }
466
467    // Map the surface tags to the new surface ids.
468    let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
469    let surfaces = surfaces.unwrap_or_default();
470    for surface in surfaces.iter() {
471        if let Some(tag) = surface.get_tag() {
472            surface_id_map.insert(tag.name.clone(), surface);
473        }
474    }
475
476    // Fix the tags
477    // This is annoying, in order to fix the tags we need to iterate over the paths again, but not
478    // mutable borrow the paths.
479    for path in new_sketch.paths.clone() {
480        // Check if this path has a tag.
481        if let Some(tag) = path.get_tag() {
482            let mut surface = None;
483            if let Some(found_surface) = surface_id_map.get(&tag.name) {
484                let mut new_surface = (*found_surface).clone();
485                if let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() {
486                    new_surface.set_face_id(new_face_id);
487                    surface = Some(new_surface);
488                } else {
489                    // A boolean can retain a tagged path while replacing or
490                    // removing its old face. `do_post_extrude` queries the
491                    // live topology and rebuilds this optional surface data.
492                    crate::log::logln!(
493                        "Failed to find new face id for stale old face id: {:?}",
494                        new_surface.face_id()
495                    );
496                }
497            }
498
499            new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
500        }
501    }
502
503    // Fix the base path.
504    if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
505        new_sketch.start.geo_meta.id = *new_base_path;
506    } else {
507        crate::log::logln!(
508            "Failed to find new base path id for old base path id: {:?}",
509            new_sketch.start.geo_meta.id
510        );
511    }
512
513    Ok(())
514}
515
516// Return the named cap tags for the original solid.
517fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
518    let mut start_tag = None;
519    let mut end_tag = None;
520    // Check the start cap.
521    if let Some(start_cap_id) = solid.start_cap_id {
522        // Check if we had a value for that cap.
523        for value in &solid.value {
524            if value.get_id() == start_cap_id {
525                start_tag = value.get_tag();
526                break;
527            }
528        }
529    }
530
531    // Check the end cap.
532    if let Some(end_cap_id) = solid.end_cap_id {
533        // Check if we had a value for that cap.
534        for value in &solid.value {
535            if value.get_id() == end_cap_id {
536                end_tag = value.get_tag();
537                break;
538            }
539        }
540    }
541
542    (start_tag, end_tag)
543}
544
545#[cfg(test)]
546mod tests {
547    use kcl_api::SolidCreatorView;
548    use kcl_api::SolidView;
549    use kcl_api::artifact::SweepSubType;
550    use kittycad_modeling_cmds::shared::BodyType;
551    use pretty_assertions::assert_eq;
552    use pretty_assertions::assert_ne;
553
554    use crate::exec::KclValueView;
555    use crate::execution::Artifact;
556    use crate::execution::ArtifactGraph;
557    use crate::execution::EdgeCutViewExt;
558    use crate::execution::ExecOutcome;
559    use crate::execution::ExtrudeSurfaceViewExt;
560    use crate::execution::Geometry;
561    use crate::execution::KclValue;
562    use crate::execution::PathViewExt;
563    use crate::execution::Solid;
564    use crate::execution::SolidViewExt;
565    use crate::execution::parse_execute;
566
567    async fn assert_mock_clone_preserves_source_region_tags(body_expression: &str) {
568        let code = format!(
569            r#"@settings(kclVersion = 3.0)
570lower = sketch(on = XY) {{
571  edge = circle(start = [var 10mm, var 0mm], center = [var 0mm, var 0mm])
572}}
573upper = sketch(on = offsetPlane(XY, offset = 10mm)) {{
574  edge = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
575}}
576lowerRegion = region(segments = [lower.edge])
577upperRegion = region(segments = [upper.edge])
578body = {body_expression}
579beforeClone = lowerRegion.tags.edge
580copied = clone(body)
581afterClone = lowerRegion.tags.edge
582"#
583        );
584        let result = parse_execute(&code).await.expect("mock clone executes");
585        let KclValue::Solid { value: body } = result.variable("body") else {
586            panic!("body is not a solid");
587        };
588        let KclValue::Solid { value: copied } = result.variable("copied") else {
589            panic!("copied is not a solid");
590        };
591        assert_ne!(body.id, copied.id);
592
593        for name in ["beforeClone", "afterClone"] {
594            let KclValue::TagIdentifier(tag) = result.variable(name) else {
595                panic!("{name} is not a tag");
596            };
597            let info = tag.get_cur_info().expect("region tag has geometry info");
598            assert!(info.surface.is_some(), "{name} should still identify a face");
599            let Geometry::Solid(tagged_body) = &info.geometry else {
600                panic!("{name} is not a solid face tag");
601            };
602            assert_eq!(
603                tagged_body.id, body.id,
604                "{name} should identify the source body, not its independent clone"
605            );
606        }
607    }
608
609    #[tokio::test(flavor = "multi_thread")]
610    async fn clone_does_not_retarget_extruded_region_tags_in_mock_execution() {
611        assert_mock_clone_preserves_source_region_tags("extrude(lowerRegion, length = 10mm)").await;
612    }
613
614    #[tokio::test(flavor = "multi_thread")]
615    async fn clone_does_not_retarget_loft_region_tags_in_mock_execution() {
616        assert_mock_clone_preserves_source_region_tags("loft([lowerRegion, upperRegion])").await;
617    }
618
619    #[tokio::test(flavor = "multi_thread")]
620    async fn kcl_test_clone_chamfered_cap_tags() {
621        for version in ["2.0", "3.0"] {
622            let code = r#"@settings(kclVersion = VERSION, experimentalFeatures = allow)
623profile = sketch(on = XZ) {
624  bottom = line(start = [-4mm, -4mm], end = [4mm, -4mm])
625  right = line(start = [4mm, -4mm], end = [4mm, 4mm])
626  top = line(start = [4mm, 4mm], end = [-4mm, 4mm])
627  left = line(start = [-4mm, 4mm], end = [-4mm, -4mm])
628}
629profileRegion = region(segments = [profile.bottom, profile.right, profile.top, profile.left])
630blank = extrude(profileRegion, length = 4mm, symmetric = true, tagStart = $startCap, tagEnd = $endCap)
631source = chamfer(blank, length = 0.25mm, edges = [
632  {sideFaces = [profileRegion.tags.right, blank.faces.startCap]},
633  {sideFaces = [profileRegion.tags.right, blank.faces.endCap]},
634  {sideFaces = [profileRegion.tags.left, blank.faces.startCap]},
635  {sideFaces = [profileRegion.tags.left, blank.faces.endCap]}
636])
637cloned = clone(source)
638startFace = cloned.faces.startCap
639endFace = cloned.faces.endCap
640clonedAgain = clone(cloned)
641result = chamfer(clonedAgain, length = 0.1mm, edges = [
642  {sideFaces = [clonedAgain.sketch.tags.top, clonedAgain.faces.startCap]},
643  {sideFaces = [clonedAgain.sketch.tags.top, clonedAgain.faces.endCap]}
644])
645"#
646            .replace("VERSION", version);
647            let program = crate::Program::parse_no_errs(&code).unwrap();
648            let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
649                .await
650                .unwrap();
651            let outcome = ctx.run_with_caching(program).await;
652            ctx.close().await;
653            let outcome = outcome.unwrap_or_else(|error| panic!("KCL {version}: {}", error.error));
654            let source = runtime_solid(&outcome, "source");
655            for name in ["cloned", "clonedAgain"] {
656                let cloned = runtime_solid(&outcome, name);
657                assert_eq!(cloned.faces.len(), 2);
658                assert_ne!(source.start_cap_id, cloned.start_cap_id);
659                assert_ne!(source.end_cap_id, cloned.end_cap_id);
660                assert!(cloned.start_cap_id.is_some());
661                assert!(cloned.end_cap_id.is_some());
662            }
663        }
664    }
665
666    fn runtime_solid<'a>(outcome: &'a ExecOutcome, name: &str) -> &'a Solid {
667        let value = outcome
668            .test_program_memory
669            .get(name)
670            .unwrap_or_else(|| panic!("Expected runtime value for {name}"));
671        let KclValue::Solid { value } = value else {
672            panic!("Expected {name} to be a runtime solid, got: {value:?}");
673        };
674        value
675    }
676
677    fn assert_cloned_composite_topology(artifact_graph: &ArtifactGraph, cloned_composite: &SolidView) {
678        let Some(Artifact::CompositeSolid(cloned_artifact)) = artifact_graph.get(&cloned_composite.artifact_id) else {
679            panic!("Expected a cloned composite solid artifact at the engine entity ID");
680        };
681        assert_eq!(cloned_artifact.id, cloned_composite.artifact_id);
682        assert!(!cloned_artifact.consumed);
683
684        let cloned_face_sweep_ids = artifact_graph
685            .values()
686            .filter_map(|artifact| match artifact {
687                Artifact::Wall(wall) if wall.cmd_id == cloned_composite.id => Some(wall.sweep_id),
688                Artifact::Cap(cap) if cap.cmd_id == cloned_composite.id => Some(cap.sweep_id),
689                _ => None,
690            })
691            .collect::<Vec<_>>();
692        assert!(!cloned_face_sweep_ids.is_empty());
693
694        for sweep_id in cloned_face_sweep_ids {
695            let Some(Artifact::Sweep(sweep)) = artifact_graph.get(&sweep_id) else {
696                panic!("Expected every cloned composite face to reference a sweep");
697            };
698            assert_eq!(sweep.code_ref, cloned_artifact.code_ref);
699            let source_sweep_id = sweep.source_sweep_id.expect("Expected cloned sweep provenance");
700            assert_ne!(sweep.id, source_sweep_id);
701            assert!(matches!(artifact_graph.get(&source_sweep_id), Some(Artifact::Sweep(_))));
702        }
703    }
704
705    // Ensure the clone function returns a sketch with different ids for all the internal paths and
706    // the resulting sketch.
707    #[tokio::test(flavor = "multi_thread")]
708    async fn kcl_test_clone_sketch() {
709        let code = r#"cube = startSketchOn(XY)
710    |> startProfile(at = [0,0])
711    |> line(end = [0, 10])
712    |> line(end = [10, 0])
713    |> line(end = [0, -10])
714    |> close()
715
716clonedCube = clone(cube)
717"#;
718        let program = crate::Program::parse_no_errs(code).unwrap();
719        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
720            .await
721            .unwrap();
722
723        // Execute the program.
724        let result = ctx.run_with_caching(program.clone()).await.unwrap();
725        let cube = result.variables.get("cube").unwrap();
726        let cloned_cube = result.variables.get("clonedCube").unwrap();
727
728        assert_ne!(cube, cloned_cube);
729
730        let KclValueView::Sketch { value: cube } = cube else {
731            panic!("Expected a sketch, got: {cube:?}");
732        };
733        let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
734            panic!("Expected a sketch, got: {cloned_cube:?}");
735        };
736
737        assert_ne!(cube.id, cloned_cube.id);
738        assert_ne!(cube.original_id, cloned_cube.original_id);
739        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
740
741        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
742        assert_eq!(cloned_cube.original_id, cloned_cube.id);
743
744        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
745            assert_ne!(path.get_id(), cloned_path.get_id());
746            assert_eq!(path.get_tag(), cloned_path.get_tag());
747        }
748
749        assert_eq!(cube.tags.len(), 0);
750        assert_eq!(cloned_cube.tags.len(), 0);
751
752        ctx.close().await;
753    }
754
755    // Ensure the clone function returns a solid with different ids for all the internal paths and
756    // references.
757    #[tokio::test(flavor = "multi_thread")]
758    async fn kcl_test_clone_solid() {
759        let code = r#"cube = startSketchOn(XY)
760    |> startProfile(at = [0,0])
761    |> line(end = [0, 10])
762    |> line(end = [10, 0])
763    |> line(end = [0, -10])
764    |> close()
765    |> extrude(length = 5)
766
767clonedCube = clone(cube)
768"#;
769        let program = crate::Program::parse_no_errs(code).unwrap();
770        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
771            .await
772            .unwrap();
773
774        // Execute the program.
775        let result = ctx.run_with_caching(program.clone()).await.unwrap();
776        let cube = result.variables.get("cube").unwrap();
777        let cloned_cube = result.variables.get("clonedCube").unwrap();
778
779        assert_ne!(cube, cloned_cube);
780
781        let KclValueView::Solid { value: cube } = cube else {
782            panic!("Expected a solid, got: {cube:?}");
783        };
784        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
785            panic!("Expected a solid, got: {cloned_cube:?}");
786        };
787        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
788        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
789
790        assert_ne!(cube.id, cloned_cube.id);
791        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
792        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
793        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
794        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
795
796        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
797
798        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
799            assert_ne!(path.get_id(), cloned_path.get_id());
800            assert_eq!(path.get_tag(), cloned_path.get_tag());
801        }
802
803        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
804            assert_ne!(value.get_id(), cloned_value.get_id());
805            assert_eq!(value.get_tag(), cloned_value.get_tag());
806        }
807
808        assert_eq!(cube_sketch.tags.len(), 0);
809        assert_eq!(cloned_cube_sketch.tags.len(), 0);
810
811        assert_eq!(cube.edge_cuts.len(), 0);
812        assert_eq!(cloned_cube.edge_cuts.len(), 0);
813
814        ctx.close().await;
815    }
816
817    #[tokio::test(flavor = "multi_thread")]
818    async fn kcl_test_clone_sketch_backed_surface_operations() {
819        let code = r#"extrudeProfile = startSketchOn(XZ)
820  |> startProfile(at = [0, 0])
821  |> line(end = [4, 0])
822  |> line(end = [2, 3])
823surfaceExtrude = extrude(extrudeProfile, length = 2, bodyType = SURFACE)
824surfaceExtrudeClone = clone(surfaceExtrude)
825
826revolveProfile = startSketchOn(XZ)
827  |> startProfile(at = [5, 0])
828  |> line(end = [2, 3])
829surfaceRevolve = revolve(revolveProfile, axis = Y, angle = 180deg, bodyType = SURFACE)
830surfaceRevolveClone = clone(surfaceRevolve)
831
832sweepProfile = startSketchOn(XY)
833  |> startProfile(at = [-10, 10])
834  |> line(end = [4, 0])
835sweepPath = startSketchOn(XY)
836  |> startProfile(at = [0, 0])
837  |> line(end = [10, 0])
838  |> tangentialArc(end = [4, -4])
839surfaceSweep = sweep(sweepProfile, path = sweepPath, bodyType = SURFACE)
840surfaceSweepClone = clone(surfaceSweep)
841
842loftProfileA = startSketchOn(offsetPlane(XZ, offset = -10))
843  |> startProfile(at = [-2, -2])
844  |> line(end = [4, 0])
845loftProfileB = startSketchOn(offsetPlane(XZ, offset = -15))
846  |> startProfile(at = [-1, -1])
847  |> line(end = [2, 0])
848surfaceLoft = loft([loftProfileA, loftProfileB], bodyType = SURFACE)
849surfaceLoftClone = clone(surfaceLoft)
850"#;
851        let program = crate::Program::parse_no_errs(code).unwrap();
852        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
853            .await
854            .unwrap();
855
856        let result = ctx.run_with_caching(program).await.unwrap();
857        for (source_name, clone_name) in [
858            ("surfaceExtrude", "surfaceExtrudeClone"),
859            ("surfaceRevolve", "surfaceRevolveClone"),
860            ("surfaceSweep", "surfaceSweepClone"),
861            ("surfaceLoft", "surfaceLoftClone"),
862        ] {
863            let KclValueView::Solid { value: source } = result.variables.get(source_name).unwrap() else {
864                panic!("Expected {source_name} to be a surface body");
865            };
866            let KclValueView::Solid { value: cloned } = result.variables.get(clone_name).unwrap() else {
867                panic!("Expected {clone_name} to be a surface body");
868            };
869
870            assert_ne!(source.id, cloned.id);
871            assert_eq!(
872                runtime_solid(&result, clone_name).best_guess_body_type,
873                Some(BodyType::Surface)
874            );
875            assert!(matches!(cloned.creator, SolidCreatorView::Sketch(_)));
876            assert!(!cloned.value.is_empty());
877        }
878
879        ctx.close().await;
880    }
881
882    #[tokio::test(flavor = "multi_thread")]
883    async fn kcl_test_clone_edge_created_surface() {
884        let code = r#"@settings(kclVersion = 2.0)
885
886baseSketch = sketch(on = XY) {
887  bottom = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
888  right = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
889  top = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
890  left = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
891  coincident([bottom.end, right.start])
892  coincident([right.end, top.start])
893  coincident([top.end, left.start])
894  coincident([left.end, bottom.start])
895}
896base = extrude(region(segments = [baseSketch.bottom, baseSketch.right]), length = 5mm)
897source = extrude(
898  getOppositeEdge(base.sketch.tags.bottom),
899  length = 3mm,
900  bodyType = SURFACE,
901  method = NEW,
902)
903cloned = clone(source)
904"#;
905        let program = crate::Program::parse_no_errs(code).unwrap();
906        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
907            .await
908            .unwrap();
909
910        let result = ctx.run_with_caching(program).await.unwrap();
911        let KclValueView::Solid { value: source } = result.variables.get("source").unwrap() else {
912            panic!("Expected an edge-created source surface");
913        };
914        let KclValueView::Solid { value: cloned } = result.variables.get("cloned").unwrap() else {
915            panic!("Expected a cloned edge-created surface");
916        };
917        let SolidCreatorView::Edge {
918            edge_id: source_edge_id,
919            ..
920        } = &source.creator
921        else {
922            panic!("Expected the source surface to retain its edge creator");
923        };
924        let SolidCreatorView::Edge {
925            edge_id: cloned_edge_id,
926            body_id: cloned_body_id,
927        } = &cloned.creator
928        else {
929            panic!("Expected the cloned surface to retain its edge creator");
930        };
931
932        assert_ne!(source.id, cloned.id);
933        assert_eq!(
934            runtime_solid(&result, "cloned").best_guess_body_type,
935            Some(BodyType::Surface)
936        );
937        assert_eq!(*cloned_body_id, cloned.id);
938        assert_ne!(source_edge_id, cloned_edge_id);
939        assert_ne!(source.value[0].get_id(), cloned.value[0].get_id());
940        assert_ne!(source.value[0].face_id(), cloned.value[0].face_id());
941
942        ctx.close().await;
943    }
944
945    #[tokio::test(flavor = "multi_thread")]
946    async fn kcl_test_clone_preserves_face_creator() {
947        let code = r#"profile = startSketchOn(XY)
948  |> startProfile(at = [0, 0])
949  |> yLine(length = 1, tag = $a)
950  |> xLine(length = 1, tag = $b)
951  |> close(tag = $c)
952base = extrude(profile, length = 1)
953source = extrude(c, length = 4, method = NEW)
954cloned = clone(source)
955"#;
956        let program = crate::Program::parse_no_errs(code).unwrap();
957        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
958            .await
959            .unwrap();
960
961        let result = ctx.run_with_caching(program).await.unwrap();
962        let KclValueView::Solid { value: source } = result.variables.get("source").unwrap() else {
963            panic!("Expected a face-created source body");
964        };
965        let KclValueView::Solid { value: cloned } = result.variables.get("cloned").unwrap() else {
966            panic!("Expected a cloned face-created body");
967        };
968        let SolidCreatorView::Face {
969            face_id: source_face_id,
970            solid_id: source_solid_id,
971            sketch: source_sketch,
972        } = &source.creator
973        else {
974            panic!("Expected the source body to retain its face creator");
975        };
976        let SolidCreatorView::Face {
977            face_id: cloned_face_id,
978            solid_id: cloned_solid_id,
979            sketch: cloned_sketch,
980        } = &cloned.creator
981        else {
982            panic!("Expected the cloned body to retain its face creator");
983        };
984
985        assert_ne!(source.id, cloned.id);
986        assert_ne!(source_face_id, cloned_face_id);
987        assert_ne!(source_solid_id, cloned_solid_id);
988        assert_ne!(source_sketch.id, cloned_sketch.id);
989        assert_ne!(source_sketch.original_id, cloned_sketch.original_id);
990
991        ctx.close().await;
992    }
993
994    #[tokio::test(flavor = "multi_thread")]
995    async fn kcl_test_clone_procedural_blend_surface() {
996        let code = r#"@settings(defaultLengthUnit = mm, kclVersion = 2.0)
997
998sketchA = sketch(on = YZ) {
999  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
1000  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
1001  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
1002  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
1003  coincident([line1.end, line2.start])
1004  coincident([line2.end, line3.start])
1005  coincident([line3.end, line4.start])
1006  coincident([line4.end, line1.start])
1007}
1008
1009sketchB = sketch(on = -XZ) {
1010  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
1011  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
1012  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
1013  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
1014  coincident([line5.end, line6.start])
1015  coincident([line6.end, line7.start])
1016  coincident([line7.end, line8.start])
1017  coincident([line8.end, line5.start])
1018}
1019
1020surfaceA = extrude(region(segments = [sketchB.line5, sketchB.line6]), length = -2mm, bodyType = SURFACE)
1021surfaceB = extrude(region(segments = [sketchA.line1, sketchA.line2]), length = -2mm, bodyType = SURFACE)
1022bridge = blend([surfaceA.sketch.tags.line7, surfaceB.sketch.tags.line3])
1023bridgeClone = clone(bridge)
1024"#;
1025        let program = crate::Program::parse_no_errs(code).unwrap();
1026        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1027            .await
1028            .unwrap();
1029
1030        let result = ctx.run_with_caching(program).await.unwrap();
1031        let KclValueView::Solid { value: bridge } = result.variables.get("bridge").unwrap() else {
1032            panic!("Expected blend to create a procedural surface");
1033        };
1034        let KclValueView::Solid { value: cloned } = result.variables.get("bridgeClone").unwrap() else {
1035            panic!("Expected blend clone to create a procedural surface");
1036        };
1037
1038        assert_ne!(bridge.id, cloned.id);
1039        assert_eq!(
1040            runtime_solid(&result, "bridgeClone").best_guess_body_type,
1041            Some(BodyType::Surface)
1042        );
1043        assert!(matches!(cloned.creator, SolidCreatorView::Procedural));
1044        let Some(Artifact::Sweep(cloned_sweep)) = result.artifact_graph.get(&cloned.artifact_id) else {
1045            panic!("Expected the blend clone to have a sweep artifact");
1046        };
1047        assert_eq!(cloned_sweep.sub_type, SweepSubType::Blend);
1048        assert_eq!(cloned_sweep.source_sweep_id, Some(bridge.artifact_id));
1049
1050        ctx.close().await;
1051    }
1052
1053    #[tokio::test(flavor = "multi_thread")]
1054    async fn kcl_test_clone_multi_body_join_queries_body_type() {
1055        let code = r#"@settings(kclVersion = 2.0)
1056
1057targetSketch = sketch(on = XY) {
1058  bottom = line(start = [var -10, var -10], end = [var 10, var -10])
1059  right = line(start = [var 10, var -10], end = [var 10, var 10])
1060  top = line(start = [var 10, var 10], end = [var -10, var 10])
1061  left = line(start = [var -10, var 10], end = [var -10, var -10])
1062  coincident([bottom.end, right.start])
1063  coincident([right.end, top.start])
1064  coincident([top.end, left.start])
1065  coincident([left.end, bottom.start])
1066}
1067target = extrude(region(point = [0, 0], sketch = targetSketch), length = 10)
1068
1069cutterSketch = sketch(on = XY) {
1070  bottom = line(start = [var -1, var -12], end = [var 1, var -12])
1071  right = line(start = [var 1, var -12], end = [var 1, var 12])
1072  top = line(start = [var 1, var 12], end = [var -1, var 12])
1073  left = line(start = [var -1, var 12], end = [var -1, var -12])
1074  coincident([bottom.end, right.start])
1075  coincident([right.end, top.start])
1076  coincident([top.end, left.start])
1077  coincident([left.end, bottom.start])
1078}
1079cutter = extrude(region(point = [0, 0], sketch = cutterSketch), length = 10)
1080
1081pieces = split([target], tools = [cutter], keepTools = true)
1082joined = joinSurfaces(pieces)
1083joinedClone = clone(joined)
1084"#;
1085        let program = crate::Program::parse_no_errs(code).unwrap();
1086        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1087            .await
1088            .unwrap();
1089
1090        let result = ctx.run_with_caching(program).await.unwrap();
1091        let KclValueView::Solid { value: joined } = result.variables.get("joined").unwrap() else {
1092            panic!("Expected joinSurfaces to create a procedural body");
1093        };
1094        let KclValueView::Solid { value: cloned } = result.variables.get("joinedClone").unwrap() else {
1095            panic!("Expected joinSurfaces body to clone");
1096        };
1097
1098        assert_ne!(joined.id, cloned.id);
1099        assert!(runtime_solid(&result, "joined").best_guess_body_type.is_none());
1100        assert!(runtime_solid(&result, "joinedClone").best_guess_body_type.is_some());
1101        assert!(matches!(cloned.creator, SolidCreatorView::Procedural));
1102
1103        ctx.close().await;
1104    }
1105
1106    #[tokio::test(flavor = "multi_thread")]
1107    async fn kcl_test_clone_loft() {
1108        let code = r#"@settings(kclVersion = 2.0)
1109
1110firstSketch = sketch(on = XY) {
1111  circle1 = circle(start = [var 10, var 0], center = [var 0, var 0])
1112}
1113secondSketch = sketch(on = offsetPlane(XY, offset = 10)) {
1114  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
1115}
1116
1117lofted = loft([
1118  region(segments = [firstSketch.circle1]),
1119  region(segments = [secondSketch.circle1]),
1120])
1121clonedLoft = clone(lofted)
1122"#;
1123        let program = crate::Program::parse_no_errs(code).unwrap();
1124        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1125            .await
1126            .unwrap();
1127
1128        let result = ctx.run_with_caching(program).await.unwrap();
1129        let KclValueView::Solid { value: lofted } = result.variables.get("lofted").unwrap() else {
1130            panic!("Expected a solid loft");
1131        };
1132        let KclValueView::Solid { value: cloned_loft } = result.variables.get("clonedLoft").unwrap() else {
1133            panic!("Expected a cloned solid loft");
1134        };
1135
1136        assert_eq!(lofted.topology_id(), lofted.id);
1137        assert_eq!(lofted.original_id(), lofted.id);
1138        assert_eq!(lofted.artifact_id, lofted.id.into());
1139
1140        assert_ne!(lofted.id, cloned_loft.id);
1141        assert_ne!(lofted.artifact_id, cloned_loft.artifact_id);
1142        assert_eq!(cloned_loft.topology_id(), cloned_loft.id);
1143        assert_eq!(cloned_loft.original_id(), cloned_loft.id);
1144        assert_eq!(cloned_loft.artifact_id, cloned_loft.id.into());
1145
1146        let loft_sketch = lofted.sketch().expect("Expected loft to retain its base sketch");
1147        let cloned_sketch = cloned_loft
1148            .sketch()
1149            .expect("Expected cloned loft to retain its base sketch");
1150        for (path, cloned_path) in loft_sketch.paths.iter().zip(cloned_sketch.paths.iter()) {
1151            assert_ne!(path.get_id(), cloned_path.get_id());
1152            assert_eq!(path.get_tag(), cloned_path.get_tag());
1153        }
1154
1155        assert!(!cloned_loft.value.is_empty());
1156        for (surface, cloned_surface) in lofted.value.iter().zip(cloned_loft.value.iter()) {
1157            assert_ne!(surface.get_id(), cloned_surface.get_id());
1158            assert_eq!(surface.get_tag(), cloned_surface.get_tag());
1159        }
1160
1161        let Some(Artifact::Sweep(source_sweep)) = result.artifact_graph.get(&lofted.artifact_id) else {
1162            panic!("Expected the source loft to be represented by a sweep artifact");
1163        };
1164        assert_eq!(source_sweep.sub_type, SweepSubType::Loft);
1165
1166        let Some(Artifact::Sweep(cloned_sweep)) = result.artifact_graph.get(&cloned_loft.artifact_id) else {
1167            panic!("Expected the cloned loft to be represented by a sweep artifact");
1168        };
1169        assert_eq!(cloned_sweep.sub_type, SweepSubType::Loft);
1170        assert_eq!(cloned_sweep.source_sweep_id, Some(lofted.artifact_id));
1171        assert_eq!(cloned_sweep.path_id, source_sweep.path_id);
1172        assert!(!cloned_sweep.consumed);
1173
1174        ctx.close().await;
1175    }
1176
1177    #[tokio::test(flavor = "multi_thread")]
1178    async fn kcl_test_clone_composite_solid_keeps_engine_artifact_id() {
1179        let code = r#"left = startSketchOn(XY)
1180    |> startProfile(at = [0, 0])
1181    |> line(end = [10, 0])
1182    |> line(end = [0, 10])
1183    |> line(end = [-10, 0])
1184    |> close()
1185    |> extrude(length = 5)
1186
1187right = startSketchOn(XY)
1188    |> startProfile(at = [5, 0])
1189    |> line(end = [10, 0])
1190    |> line(end = [0, 10])
1191    |> line(end = [-10, 0])
1192    |> close()
1193    |> extrude(length = 5)
1194
1195composite = union([left, right])
1196clonedComposite = clone(composite)
1197"#;
1198        let program = crate::Program::parse_no_errs(code).unwrap();
1199        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1200            .await
1201            .unwrap();
1202
1203        let result = ctx.run_with_caching(program).await.unwrap();
1204        let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
1205            panic!("Expected composite to be a solid");
1206        };
1207        let KclValueView::Solid {
1208            value: cloned_composite,
1209        } = result.variables.get("clonedComposite").unwrap()
1210        else {
1211            panic!("Expected clonedComposite to be a solid");
1212        };
1213
1214        assert_eq!(composite.artifact_id, composite.id.into());
1215        assert_eq!(cloned_composite.artifact_id, cloned_composite.id.into());
1216        assert_ne!(composite.id, cloned_composite.id);
1217        assert_ne!(composite.original_id(), composite.id);
1218        assert_eq!(composite.topology_id(), composite.id);
1219        assert_eq!(cloned_composite.original_id(), cloned_composite.id);
1220        assert_eq!(cloned_composite.topology_id(), cloned_composite.id);
1221
1222        assert_cloned_composite_topology(&result.artifact_graph, cloned_composite);
1223
1224        ctx.close().await;
1225    }
1226
1227    #[tokio::test(flavor = "multi_thread")]
1228    async fn kcl_test_clone_imported_patterned_composite_uses_composite_topology() {
1229        let module_code = r#"left = startSketchOn(XY)
1230    |> startProfile(at = [0, 0])
1231    |> line(end = [10, 0])
1232    |> line(end = [0, 10])
1233    |> line(end = [-10, 0])
1234    |> close()
1235    |> extrude(length = 5)
1236
1237right = startSketchOn(XY)
1238    |> startProfile(at = [5, 0])
1239    |> line(end = [10, 0])
1240    |> line(end = [0, 10])
1241    |> line(end = [-10, 0])
1242    |> close()
1243    |> extrude(length = 5)
1244
1245export composite = union([left, right])
1246"#;
1247        let code = r#"import composite from 'composite.kcl'
1248
1249patterned = patternLinear3d(
1250    composite,
1251    instances = 2,
1252    distance = 20,
1253    axis = [1, 0, 0],
1254)
1255patternCopy = patterned[1]
1256clonedCopy = clone(patternCopy)
1257"#;
1258        let tmpdir = tempfile::TempDir::with_prefix("clone_imported_patterned_composite").unwrap();
1259        let main_path = tmpdir.path().join("main.kcl");
1260        std::fs::write(tmpdir.path().join("composite.kcl"), module_code).unwrap();
1261        std::fs::write(&main_path, code).unwrap();
1262
1263        let program = crate::Program::parse_no_errs(code).unwrap();
1264        let ctx = crate::test_server::new_context(true, Some(main_path), true, program.language_version().unwrap())
1265            .await
1266            .unwrap();
1267
1268        let result = ctx.run_with_caching(program).await.unwrap();
1269        let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
1270            panic!("Expected composite to be a solid");
1271        };
1272        let KclValueView::Solid { value: pattern_copy } = result.variables.get("patternCopy").unwrap() else {
1273            panic!("Expected patternCopy to be a solid");
1274        };
1275        let KclValueView::Solid { value: cloned_copy } = result.variables.get("clonedCopy").unwrap() else {
1276            panic!("Expected clonedCopy to be a solid");
1277        };
1278
1279        assert_eq!(composite.topology_id(), composite.id);
1280        assert_eq!(pattern_copy.topology_id(), composite.id);
1281        assert_eq!(cloned_copy.original_id(), cloned_copy.id);
1282        assert_eq!(cloned_copy.topology_id(), cloned_copy.id);
1283        assert_eq!(cloned_copy.artifact_id, cloned_copy.id.into());
1284        assert_ne!(pattern_copy.id, cloned_copy.id);
1285        assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
1286        assert_cloned_composite_topology(&result.artifact_graph, cloned_copy);
1287
1288        ctx.close().await;
1289    }
1290
1291    // Ensure the clone function returns a sketch with different ids for all the internal paths and
1292    // the resulting sketch.
1293    // AND TAGS.
1294    #[tokio::test(flavor = "multi_thread")]
1295    async fn kcl_test_clone_sketch_with_tags() {
1296        let code = r#"cube = startSketchOn(XY)
1297    |> startProfile(at = [0,0]) // tag this one
1298    |> line(end = [0, 10], tag = $tag02)
1299    |> line(end = [10, 0], tag = $tag03)
1300    |> line(end = [0, -10], tag = $tag04)
1301    |> close(tag = $tag05)
1302
1303clonedCube = clone(cube)
1304"#;
1305        let program = crate::Program::parse_no_errs(code).unwrap();
1306        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1307            .await
1308            .unwrap();
1309
1310        // Execute the program.
1311        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1312        let cube = result.variables.get("cube").unwrap();
1313        let cloned_cube = result.variables.get("clonedCube").unwrap();
1314
1315        assert_ne!(cube, cloned_cube);
1316
1317        let KclValueView::Sketch { value: cube } = cube else {
1318            panic!("Expected a sketch, got: {cube:?}");
1319        };
1320        let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
1321            panic!("Expected a sketch, got: {cloned_cube:?}");
1322        };
1323
1324        assert_ne!(cube.id, cloned_cube.id);
1325        assert_ne!(cube.original_id, cloned_cube.original_id);
1326
1327        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
1328            assert_ne!(path.get_id(), cloned_path.get_id());
1329            assert_eq!(path.get_tag(), cloned_path.get_tag());
1330        }
1331
1332        for (tag_name, tag) in &cube.tags {
1333            assert_eq!(Some(tag), cloned_cube.tags.get(tag_name));
1334        }
1335
1336        ctx.close().await;
1337    }
1338
1339    // Ensure the clone function returns a solid with different ids for all the internal paths and
1340    // references.
1341    // WITH TAGS.
1342    #[tokio::test(flavor = "multi_thread")]
1343    async fn kcl_test_clone_solid_with_tags() {
1344        let code = r#"cube = startSketchOn(XY)
1345    |> startProfile(at = [0,0]) // tag this one
1346    |> line(end = [0, 10], tag = $tag02)
1347    |> line(end = [10, 0], tag = $tag03)
1348    |> line(end = [0, -10], tag = $tag04)
1349    |> close(tag = $tag05)
1350    |> extrude(length = 5, tagEnd = $endCap)
1351
1352clonedCube = clone(cube)
1353"#;
1354        let program = crate::Program::parse_no_errs(code).unwrap();
1355        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1356            .await
1357            .unwrap();
1358
1359        // Execute the program.
1360        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1361        let cube = result.variables.get("cube").unwrap();
1362        let cloned_cube = result.variables.get("clonedCube").unwrap();
1363
1364        assert_ne!(cube, cloned_cube);
1365
1366        let KclValueView::Solid { value: cube } = cube else {
1367            panic!("Expected a solid, got: {cube:?}");
1368        };
1369        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1370            panic!("Expected a solid, got: {cloned_cube:?}");
1371        };
1372        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1373        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1374
1375        assert_ne!(cube.id, cloned_cube.id);
1376        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1377        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1378        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1379        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1380
1381        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1382
1383        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
1384            assert_ne!(path.get_id(), cloned_path.get_id());
1385            assert_eq!(path.get_tag(), cloned_path.get_tag());
1386        }
1387
1388        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1389            assert_ne!(value.get_id(), cloned_value.get_id());
1390            assert_eq!(value.get_tag(), cloned_value.get_tag());
1391        }
1392
1393        for (tag_name, tag) in &cube_sketch.tags {
1394            assert_eq!(Some(tag), cloned_cube_sketch.tags.get(tag_name));
1395        }
1396
1397        for (tag_name, tag) in &cube.faces {
1398            assert_eq!(Some(tag), cloned_cube.faces.get(tag_name));
1399        }
1400        assert!(cube.faces.contains_key("endCap"));
1401        assert!(cloned_cube.faces.contains_key("endCap"));
1402
1403        assert_eq!(cube.edge_cuts.len(), 0);
1404        assert_eq!(cloned_cube.edge_cuts.len(), 0);
1405
1406        ctx.close().await;
1407    }
1408
1409    // Pattern copies retain the source topology in program memory. Cloning a
1410    // copy must map that source topology directly onto the clone so both wall
1411    // and cap tags refer to the clone's faces.
1412    #[tokio::test(flavor = "multi_thread")]
1413    async fn kcl_test_clone_pattern_copy_with_face_tags() {
1414        let code = r#"source = startSketchOn(XY)
1415    |> startProfile(at = [0, 0])
1416    |> line(end = [0, 10], tag = $wall)
1417    |> line(end = [10, 0])
1418    |> line(end = [0, -10])
1419    |> close()
1420    |> extrude(length = 5, tagEnd = $endCap)
1421
1422patterned = patternLinear3d(
1423    source,
1424    instances = 2,
1425    distance = 15,
1426    axis = [1, 0, 0],
1427)
1428patternCopy = patterned[1]
1429clonedCopy = clone(patternCopy)
1430"#;
1431        let program = crate::Program::parse_no_errs(code).unwrap();
1432        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1433            .await
1434            .unwrap();
1435
1436        let result = ctx.run_with_caching(program).await.unwrap();
1437        let source = result.variables.get("source").unwrap();
1438        let pattern_copy = result.variables.get("patternCopy").unwrap();
1439        let cloned_copy = result.variables.get("clonedCopy").unwrap();
1440
1441        let KclValueView::Solid { value: source } = source else {
1442            panic!("Expected a solid, got: {source:?}");
1443        };
1444        let KclValueView::Solid { value: pattern_copy } = pattern_copy else {
1445            panic!("Expected a solid, got: {pattern_copy:?}");
1446        };
1447        let KclValueView::Solid { value: cloned_copy } = cloned_copy else {
1448            panic!("Expected a solid, got: {cloned_copy:?}");
1449        };
1450        let pattern_sketch = pattern_copy.sketch().expect("Expected pattern copy to have a sketch");
1451        let cloned_sketch = cloned_copy.sketch().expect("Expected cloned copy to have a sketch");
1452        assert_eq!(pattern_copy.original_id(), source.id);
1453        assert_eq!(cloned_copy.original_id(), cloned_copy.id);
1454        assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
1455        assert_ne!(cloned_copy.artifact_id, cloned_copy.id.into());
1456
1457        assert_eq!(pattern_sketch.tags.get("wall"), cloned_sketch.tags.get("wall"));
1458        assert_eq!(pattern_copy.faces.get("endCap"), cloned_copy.faces.get("endCap"));
1459
1460        assert!(matches!(
1461            result.artifact_graph.get(&cloned_copy.artifact_id),
1462            Some(Artifact::Sweep(sweep))
1463                if sweep.path_id == Some(cloned_copy.id.into())
1464        ));
1465        assert!(matches!(
1466            result.artifact_graph.get(&cloned_copy.id.into()),
1467            Some(Artifact::Path(path))
1468                if path.sweep_id == Some(cloned_copy.artifact_id)
1469        ));
1470        assert!(
1471            result
1472                .artifact_graph
1473                .values()
1474                .any(|artifact| matches!(artifact, Artifact::Wall(wall) if wall.sweep_id == cloned_copy.artifact_id))
1475        );
1476        assert!(
1477            result
1478                .artifact_graph
1479                .values()
1480                .any(|artifact| matches!(artifact, Artifact::Cap(cap) if cap.sweep_id == cloned_copy.artifact_id))
1481        );
1482
1483        ctx.close().await;
1484    }
1485
1486    // Ensure we can get all paths even on a sketch where we closed it and it was already closed.
1487    #[tokio::test(flavor = "multi_thread")]
1488    #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
1489    async fn kcl_test_clone_cube_already_closed_sketch() {
1490        let code = r#"// Clone a basic solid and move it.
1491
1492exampleSketch = startSketchOn(XY)
1493  |> startProfile(at = [0, 0])
1494  |> line(end = [10, 0])
1495  |> line(end = [0, 10])
1496  |> line(end = [-10, 0])
1497  |> line(end = [0, -10])
1498  |> close()
1499
1500cube = extrude(exampleSketch, length = 5)
1501clonedCube = clone(cube)
1502    |> translate(
1503        x = 25.0,
1504    )"#;
1505        let program = crate::Program::parse_no_errs(code).unwrap();
1506        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1507            .await
1508            .unwrap();
1509
1510        // Execute the program.
1511        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1512        let cube = result.variables.get("cube").unwrap();
1513        let cloned_cube = result.variables.get("clonedCube").unwrap();
1514
1515        assert_ne!(cube, cloned_cube);
1516
1517        let KclValueView::Solid { value: cube } = cube else {
1518            panic!("Expected a solid, got: {cube:?}");
1519        };
1520        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1521            panic!("Expected a solid, got: {cloned_cube:?}");
1522        };
1523        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1524        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1525
1526        assert_ne!(cube.id, cloned_cube.id);
1527        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1528        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1529        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1530        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1531
1532        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1533
1534        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
1535            assert_ne!(path.get_id(), cloned_path.get_id());
1536            assert_eq!(path.get_tag(), cloned_path.get_tag());
1537        }
1538
1539        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1540            assert_ne!(value.get_id(), cloned_value.get_id());
1541            assert_eq!(value.get_tag(), cloned_value.get_tag());
1542        }
1543
1544        for (tag_name, tag) in &cube_sketch.tags {
1545            assert_eq!(Some(tag), cloned_cube_sketch.tags.get(tag_name));
1546        }
1547
1548        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1549            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1550            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1551            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1552        }
1553
1554        ctx.close().await;
1555    }
1556
1557    // Ensure the clone function returns a solid with different ids for all the internal paths and
1558    // references.
1559    // WITH TAGS AND EDGE CUTS.
1560    #[tokio::test(flavor = "multi_thread")]
1561    async fn kcl_test_clone_solid_with_edge_cuts() {
1562        let code = r#"cube = startSketchOn(XY)
1563    |> startProfile(at = [0,0]) // tag this one
1564    |> line(end = [0, 10], tag = $tag02)
1565    |> line(end = [10, 0], tag = $tag03)
1566    |> line(end = [0, -10], tag = $tag04)
1567    |> close(tag = $tag05)
1568    |> extrude(length = 5) // TODO: Tag these
1569  |> fillet(
1570    radius = 2,
1571    tags = [
1572      getNextAdjacentEdge(tag02),
1573    ],
1574    tag = $fillet01,
1575  )
1576  |> fillet(
1577    radius = 2,
1578    tags = [
1579      getNextAdjacentEdge(tag04),
1580    ],
1581    tag = $fillet02,
1582  )
1583  |> chamfer(
1584    length = 2,
1585    tags = [
1586      getNextAdjacentEdge(tag03),
1587    ],
1588    tag = $chamfer01,
1589  )
1590  |> chamfer(
1591    length = 2,
1592    tags = [
1593      getNextAdjacentEdge(tag05),
1594    ],
1595    tag = $chamfer02,
1596  )
1597
1598clonedCube = clone(cube)
1599"#;
1600        let program = crate::Program::parse_no_errs(code).unwrap();
1601        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1602            .await
1603            .unwrap();
1604
1605        // Execute the program.
1606        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1607        let cube = result.variables.get("cube").unwrap();
1608        let cloned_cube = result.variables.get("clonedCube").unwrap();
1609
1610        assert_ne!(cube, cloned_cube);
1611
1612        let KclValueView::Solid { value: cube } = cube else {
1613            panic!("Expected a solid, got: {cube:?}");
1614        };
1615        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1616            panic!("Expected a solid, got: {cloned_cube:?}");
1617        };
1618        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1619        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1620
1621        assert_ne!(cube.id, cloned_cube.id);
1622        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1623        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1624        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1625        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1626
1627        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1628
1629        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1630            assert_ne!(value.get_id(), cloned_value.get_id());
1631            assert_eq!(value.get_tag(), cloned_value.get_tag());
1632        }
1633
1634        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1635            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1636            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1637            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1638        }
1639
1640        ctx.close().await;
1641    }
1642
1643    // KCL 3.0 copy of kcl_test_clone_solid_with_edge_cuts. Edge cuts are sent
1644    // to the engine immediately, so every adjacent edge is looked up before
1645    // the first fillet consumes any of them.
1646    #[tokio::test(flavor = "multi_thread")]
1647    async fn kcl_test_clone_solid_with_edge_cuts_v3() {
1648        let code = r#"@settings(kclVersion = "3.0-preview")
1649
1650baseCube = startSketchOn(XY)
1651    |> startProfile(at = [0,0]) // tag this one
1652    |> line(end = [0, 10], tag = $tag02)
1653    |> line(end = [10, 0], tag = $tag03)
1654    |> line(end = [0, -10], tag = $tag04)
1655    |> close(tag = $tag05)
1656    |> extrude(length = 5) // TODO: Tag these
1657
1658tag02NextAdjacentEdge = getNextAdjacentEdge(tag02)
1659tag03NextAdjacentEdge = getNextAdjacentEdge(tag03)
1660tag04NextAdjacentEdge = getNextAdjacentEdge(tag04)
1661tag05NextAdjacentEdge = getNextAdjacentEdge(tag05)
1662
1663cube = baseCube
1664  |> fillet(
1665    radius = 2,
1666    tags = [
1667      tag02NextAdjacentEdge,
1668    ],
1669    tag = $fillet01,
1670  )
1671  |> fillet(
1672    radius = 2,
1673    tags = [
1674      tag04NextAdjacentEdge,
1675    ],
1676    tag = $fillet02,
1677  )
1678  |> chamfer(
1679    length = 2,
1680    tags = [
1681      tag03NextAdjacentEdge,
1682    ],
1683    tag = $chamfer01,
1684  )
1685  |> chamfer(
1686    length = 2,
1687    tags = [
1688      tag05NextAdjacentEdge,
1689    ],
1690    tag = $chamfer02,
1691  )
1692
1693clonedCube = clone(cube)
1694"#;
1695        let program = crate::Program::parse_no_errs(code).unwrap();
1696        let ctx = crate::test_server::new_context(true, None, true, program.language_version().unwrap())
1697            .await
1698            .unwrap();
1699
1700        // Execute the program.
1701        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1702        let cube = result.variables.get("cube").unwrap();
1703        let cloned_cube = result.variables.get("clonedCube").unwrap();
1704
1705        assert_ne!(cube, cloned_cube);
1706
1707        let KclValueView::Solid { value: cube } = cube else {
1708            panic!("Expected a solid, got: {cube:?}");
1709        };
1710        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1711            panic!("Expected a solid, got: {cloned_cube:?}");
1712        };
1713        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1714        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1715
1716        assert_ne!(cube.id, cloned_cube.id);
1717        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1718        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1719        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1720        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1721
1722        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1723
1724        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1725            assert_ne!(value.get_id(), cloned_value.get_id());
1726            assert_eq!(value.get_tag(), cloned_value.get_tag());
1727        }
1728
1729        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1730            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1731            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1732            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1733        }
1734
1735        ctx.close().await;
1736    }
1737}