Skip to main content

kcl_lib/execution/
remote.rs

1//! Native clients can execute a project on their existing engine connection.
2use std::borrow::Cow;
3use std::collections::VecDeque;
4use std::path::Path;
5use std::path::PathBuf;
6
7use indexmap::IndexMap;
8use kittycad_modeling_cmds::exec_kcl::ExecKclProjectErr;
9use kittycad_modeling_cmds::exec_kcl::ExecKclProjectOk;
10use kittycad_modeling_cmds::exec_kcl::KclFile;
11use kittycad_modeling_cmds::exec_kcl::KclProject;
12use kittycad_modeling_cmds::shared::safe_filepath::SafeFilepath;
13use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
14use kittycad_modeling_cmds::websocket::WebSocketRequest;
15
16use super::ExecOutcome;
17use super::ExecState;
18use super::ExecutorContext;
19use super::IdGenerator;
20use super::OperationsByModule;
21use crate::Program;
22use crate::SourceRange;
23use crate::TypedPath;
24use crate::engine::GridScaleBehavior;
25use crate::errors::KclError;
26use crate::errors::KclErrorDetails;
27use crate::errors::KclErrorWithOutputs;
28use crate::modules::ModuleId;
29use crate::modules::ModulePath;
30use crate::modules::ModuleSource;
31use crate::parsing::ast::types::ImportPath;
32
33impl ExecutorContext {
34    /// Execute KCL on the engine, retaining this connection for follow-up commands.
35    /// `state` holds only client-side follow-up settings, not interpreter memory.
36    pub async fn execute_remote_project(
37        &self,
38        program: &Program,
39        state: &mut ExecState,
40    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
41        let mut project = UploadedProject::collect(self, program)
42            .await
43            .map_err(KclErrorWithOutputs::no_outputs)?;
44        let id = uuid::Uuid::new_v4();
45        let range = SourceRange::synthetic();
46        let request = KclProject::new(
47            std::mem::take(&mut project.request.files),
48            project.request.entrypoint.clone(),
49        );
50        self.cache.bump_engine_scene_generation().await;
51        let engine_scene_generation = self.cache.engine_scene_generation().await;
52        let response = self
53            .engine
54            .transport
55            .inner_send_modeling_cmd(
56                id,
57                range,
58                WebSocketRequest::ExecKclProject {
59                    request_id: id,
60                    project: request,
61                },
62                Default::default(),
63            )
64            .await
65            .map_err(KclErrorWithOutputs::no_outputs)?;
66        let response = self
67            .engine
68            .parse_websocket_response(response, range)
69            .map_err(KclErrorWithOutputs::no_outputs)?;
70        let OkWebSocketResponseData::ExecKclProject { result } = response else {
71            return Err(KclErrorWithOutputs::no_outputs(remote_error(
72                "Engine did not return an ExecKclProject response; update the engine to support remote KCL execution.",
73            )));
74        };
75        let outcome = project.outcome(*result, engine_scene_generation)?;
76
77        state.mod_local.settings = program
78            .meta_settings()
79            .map_err(KclErrorWithOutputs::no_outputs)?
80            .unwrap_or_default();
81        state.mod_local.id_generator = IdGenerator::remote_session();
82        *self.engine.get_default_planes().write().await = outcome.default_planes.clone();
83        // The engine executes without client graphics settings. Reapply them before
84        // snapshots, including overrides such as highlight_edges=False.
85        let grid_scale = if self.settings.fixed_size_grid {
86            GridScaleBehavior::Fixed(Some(state.length_unit()))
87        } else {
88            GridScaleBehavior::ScaleWithZoom
89        };
90        self.engine
91            .reapply_settings(
92                &self.engine_batch,
93                &self.settings,
94                range,
95                state.id_generator(),
96                grid_scale,
97            )
98            .await
99            .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
100        if !self.settings.geometry_only && self.settings.enable_ssao {
101            self.engine
102                .send_modeling_cmd(
103                    &self.engine_batch,
104                    state.next_uuid(),
105                    range,
106                    &kittycad_modeling_cmds::ModelingCmd::from(
107                        kittycad_modeling_cmds::SetOrderIndependentTransparency::builder()
108                            .enabled(false)
109                            .build(),
110                    ),
111                )
112                .await
113                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
114        }
115        Ok(outcome)
116    }
117}
118
119fn remote_error(message: impl Into<String>) -> KclError {
120    KclError::new_semantic(KclErrorDetails::new(message.into(), vec![SourceRange::synthetic()]))
121}
122
123/// Normalize uploaded virtual paths without accessing the host filesystem.
124fn normalize(path: &Path) -> PathBuf {
125    PathBuf::from(
126        typed_path::TypedPath::derive(path.as_os_str().as_encoded_bytes())
127            .normalize()
128            .to_string_lossy()
129            .as_ref(),
130    )
131}
132
133struct UploadedProject {
134    request: KclProject,
135    /// Upload name -> original client filename and exact bytes sent to the engine.
136    files: IndexMap<String, (PathBuf, Vec<u8>)>,
137    inline: bool,
138}
139
140impl UploadedProject {
141    async fn collect(ctx: &ExecutorContext, program: &Program) -> Result<Self, KclError> {
142        let cwd = std::env::current_dir().map_err(|err| remote_error(err.to_string()))?;
143        let root = normalize(
144            &ctx.settings
145                .current_file
146                .as_ref()
147                .map(|path| cwd.join(&path.0))
148                .unwrap_or_else(|| {
149                    ctx.settings
150                        .project_directory
151                        .as_ref()
152                        .map(|dir| cwd.join(&dir.0))
153                        .unwrap_or(cwd)
154                        .join(format!("__zoo_kcl_{}.kcl", uuid::Uuid::new_v4()))
155                }),
156        );
157        let mut files = IndexMap::new();
158        files.insert(root.clone(), program.original_file_contents.as_bytes().to_vec());
159        let mut pending = VecDeque::from([(root.clone(), Cow::Borrowed(program), Vec::<PathBuf>::new())]);
160        while let Some((path, program, mut ancestors)) = pending.pop_front() {
161            if let Ok(real_path) = tokio::fs::canonicalize(&path).await {
162                if ancestors.contains(&real_path) {
163                    continue;
164                }
165                ancestors.push(real_path);
166            }
167            let imports = std::cell::RefCell::new(Vec::new());
168            crate::walk::walk(&program.ast, |node| {
169                if let crate::walk::Node::ImportStatement(import) = node {
170                    match &import.path {
171                        ImportPath::Kcl { filename } => imports.borrow_mut().push((filename.clone(), true)),
172                        ImportPath::Foreign { path } => imports.borrow_mut().push((path.clone(), false)),
173                        ImportPath::Std { .. } => {}
174                    }
175                }
176                Ok::<_, KclError>(true)
177            })
178            .map_err(|err| remote_error(err.to_string()))?;
179            for (import, is_kcl) in imports.into_inner() {
180                reject_absolute_import(&import.to_string())?;
181                let imported_path = normalize(&path.parent().unwrap_or(Path::new("")).join(&import.0));
182                if files.contains_key(&imported_path) {
183                    continue;
184                }
185                // Leave missing files and invalid modules to the engine so its
186                // diagnostics retain the correct source ranges and partial output.
187                let Ok(bytes) = ctx
188                    .fs
189                    .read(&TypedPath(imported_path.clone()), SourceRange::synthetic())
190                    .await
191                else {
192                    continue;
193                };
194                if is_kcl {
195                    if let Ok(source) = std::str::from_utf8(&bytes)
196                        && let Ok(imported) = Program::parse_no_errs(source)
197                    {
198                        pending.push_back((imported_path.clone(), Cow::Owned(imported), ancestors.clone()));
199                    }
200                } else if imported_path
201                    .extension()
202                    .is_some_and(|ext| ext.eq_ignore_ascii_case("gltf") || ext.eq_ignore_ascii_case("glb"))
203                    && !bytes.starts_with(b"glTF")
204                    && let Ok(gltf) = gltf_json::Root::from_slice(&bytes)
205                {
206                    for buffer in gltf.buffers {
207                        if let Some(uri) = buffer.uri
208                            && !uri.starts_with("data:")
209                        {
210                            reject_absolute_import(&uri)?;
211                            let buffer_path = normalize(&imported_path.parent().unwrap_or(Path::new("")).join(uri));
212                            if let Ok(data) = ctx
213                                .fs
214                                .read(&TypedPath(buffer_path.clone()), SourceRange::synthetic())
215                                .await
216                            {
217                                files.entry(buffer_path).or_insert(data);
218                            }
219                        }
220                    }
221                }
222                files.insert(imported_path, bytes);
223            }
224        }
225
226        // A common root keeps relative parent imports inside the uploaded project.
227        let mut directory = root.parent().unwrap_or(Path::new("")).to_path_buf();
228        for path in files.keys() {
229            while !path.starts_with(&directory) {
230                if !directory.pop() {
231                    return Err(remote_error("Imported files do not share a project root"));
232                }
233            }
234        }
235        let relative = |path: &Path| -> Result<String, KclError> {
236            path.strip_prefix(&directory)
237                .map(|path| path.to_string_lossy().replace('\\', "/"))
238                .map_err(|err| remote_error(err.to_string()))
239        };
240        let entrypoint = safe_path(&relative(&root)?)?;
241        let files: IndexMap<_, _> = files
242            .into_iter()
243            .map(|(path, bytes)| Ok((relative(&path)?, (path, bytes))))
244            .collect::<Result<_, KclError>>()?;
245        let request = KclProject::new(
246            files
247                .iter()
248                .map(|(name, (_, bytes))| Ok(KclFile::new(safe_path(name)?, bytes.clone())))
249                .collect::<Result<_, KclError>>()?,
250            entrypoint,
251        );
252        Ok(Self {
253            request,
254            files,
255            inline: ctx.settings.current_file.is_none(),
256        })
257    }
258
259    fn sources(&self, filenames: &mut IndexMap<ModuleId, ModulePath>) -> IndexMap<ModuleId, ModuleSource> {
260        let mut sources = IndexMap::new();
261        for (id, path) in filenames {
262            let source = if id.is_top_level() {
263                let (original, bytes) = &self.files[&self.request.entrypoint.to_string()];
264                *path = if self.inline {
265                    ModulePath::Main
266                } else {
267                    ModulePath::Local {
268                        value: TypedPath(original.clone()),
269                        original_import_path: None,
270                    }
271                };
272                Some(bytes.as_slice())
273            } else {
274                match path {
275                    ModulePath::Main => Some(self.files[&self.request.entrypoint.to_string()].1.as_slice()),
276                    ModulePath::Local { value, .. } => {
277                        let name = normalize(&value.0).to_string_lossy().replace('\\', "/");
278                        self.files.get(&name).map(|(original, bytes)| {
279                            *value = TypedPath(original.clone());
280                            bytes.as_slice()
281                        })
282                    }
283                    ModulePath::Std { value } => crate::modules::read_std(value).map(str::as_bytes),
284                }
285            };
286            if let Some(source) = source.and_then(|source| std::str::from_utf8(source).ok()) {
287                sources.insert(
288                    *id,
289                    ModuleSource {
290                        path: path.clone(),
291                        source: source.to_owned(),
292                    },
293                );
294            }
295        }
296        sources
297    }
298
299    fn outcome(
300        &self,
301        result: Result<ExecKclProjectOk, ExecKclProjectErr>,
302        engine_scene_generation: usize,
303    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
304        let mut response = match result {
305            Ok(response) => response,
306            Err(response) => {
307                if let Some(mut error) = response.error {
308                    self.restore_entrypoint_name(&mut error.details_mut().message);
309                    let mut error = KclErrorWithOutputs::no_outputs(error);
310                    error.non_fatal = response.non_fatal;
311                    for issue in &mut error.non_fatal {
312                        self.restore_entrypoint_name(&mut issue.message);
313                    }
314                    error.variables = response.variables;
315                    error.operations = OperationsByModule {
316                        map: response.operations,
317                    };
318                    error.artifact_graph = response.artifact_graph;
319                    error.scene_objects = response.scene_objects;
320                    error.filenames = response.filenames;
321                    error.source_files = self.sources(&mut error.filenames);
322                    error.default_planes = response.default_planes.map(|planes| *planes);
323                    error.engine_scene_generation = Some(engine_scene_generation);
324                    return Err(error);
325                }
326                // Completed execution with non-fatal errors still returns an
327                // outcome, as the local interpreter does.
328                ExecKclProjectOk::builder()
329                    .variables(response.variables)
330                    .operations(response.operations)
331                    .artifact_graph(response.artifact_graph)
332                    .scene_objects(response.scene_objects)
333                    .filenames(response.filenames)
334                    .issues(response.non_fatal)
335                    .maybe_default_planes(response.default_planes)
336                    .build()
337            }
338        };
339        for issue in &mut response.issues {
340            self.restore_entrypoint_name(&mut issue.message);
341        }
342        let mut filenames = response.filenames;
343        let source_files = self.sources(&mut filenames);
344        Ok(ExecOutcome {
345            variables: response.variables,
346            operations: OperationsByModule {
347                map: response.operations,
348            },
349            artifact_graph: response.artifact_graph,
350            scene_objects: response.scene_objects,
351            source_range_to_object: Default::default(),
352            var_solutions: Default::default(),
353            refactor_metadata: Default::default(),
354            issues: response.issues,
355            filenames,
356            engine_scene_generation,
357            source_files,
358            default_planes: response.default_planes.map(|planes| *planes),
359            #[cfg(test)]
360            test_program_memory: Default::default(),
361        })
362    }
363
364    fn restore_entrypoint_name(&self, message: &mut String) {
365        let upload_name = self.request.entrypoint.to_string();
366        let client_name = if self.inline {
367            Cow::Borrowed("main")
368        } else {
369            self.files[&upload_name].0.to_string_lossy()
370        };
371        *message = message.replace(&format!("`{upload_name}`"), &format!("`{client_name}`"));
372    }
373}
374
375fn reject_absolute_import(path: &str) -> Result<(), KclError> {
376    if typed_path::UnixPath::new(path).is_absolute() || typed_path::WindowsPath::new(path).has_root() {
377        return Err(remote_error(format!(
378            "Absolute import paths are not supported by engine execution: {path}"
379        )));
380    }
381    Ok(())
382}
383
384fn safe_path(path: &str) -> Result<SafeFilepath, KclError> {
385    path.parse()
386        .map_err(|err| remote_error(format!("Cannot upload project path {path}: {err}")))
387}
388
389#[cfg(test)]
390mod tests {
391    use std::sync::Arc;
392
393    use kittycad_modeling_cmds::websocket::WebSocketResponse;
394
395    use super::*;
396    use crate::ExecutorSettings;
397    use crate::engine_connection::EngineManager;
398    use crate::engine_connection::EngineTransport;
399
400    const ROOT: &str = "@settings(kclVersion = 3.0)\nimport \"sub/main.kcl\"\nvalue = 1\n";
401
402    async fn project() -> (tempfile::TempDir, UploadedProject) {
403        let dir = tempfile::tempdir().unwrap();
404        std::fs::create_dir(dir.path().join("sub")).unwrap();
405        std::fs::write(
406            dir.path().join("sub/main.kcl"),
407            "@settings(kclVersion = 3.0)\nimport \"main.kcl\"\nexport value = 2\n",
408        )
409        .unwrap();
410        std::fs::write(dir.path().join("main.kcl"), ROOT).unwrap();
411        std::fs::write(dir.path().join("unrelated.txt"), "must not upload").unwrap();
412        let ctx = ExecutorContext::new_mock(Some(ExecutorSettings {
413            current_file: Some(TypedPath(dir.path().join("main.kcl"))),
414            ..Default::default()
415        }))
416        .await;
417        let uploaded = UploadedProject::collect(&ctx, &Program::parse_no_errs(ROOT).unwrap())
418            .await
419            .unwrap();
420        (dir, uploaded)
421    }
422
423    fn response() -> ExecKclProjectOk {
424        ExecKclProjectOk::builder()
425            .artifact_graph(Default::default())
426            .variables(Default::default())
427            .operations(Default::default())
428            .issues(Vec::new())
429            .scene_objects(Vec::new())
430            .filenames(IndexMap::from([
431                (
432                    ModuleId::default(),
433                    ModulePath::Local {
434                        value: "main.kcl".into(),
435                        original_import_path: None,
436                    },
437                ),
438                (
439                    ModuleId::from_usize(1),
440                    ModulePath::Local {
441                        value: "sub/main.kcl".into(),
442                        original_import_path: Some("sub/main.kcl".into()),
443                    },
444                ),
445            ]))
446            .build()
447    }
448
449    #[tokio::test]
450    async fn uploads_only_dependencies_and_stops_at_cycles() {
451        let (_, project) = project().await;
452        assert_eq!(project.request.entrypoint.to_string(), "main.kcl");
453        assert_eq!(
454            project.files.keys().map(String::as_str).collect::<Vec<_>>(),
455            ["main.kcl", "sub/main.kcl"]
456        );
457    }
458
459    #[tokio::test]
460    async fn reconstructs_sources_from_uploaded_bytes_after_files_change() {
461        let (dir, project) = project().await;
462        std::fs::write(dir.path().join("main.kcl"), "changed").unwrap();
463        let outcome = project.outcome(Ok(response()), 1).unwrap();
464        assert_eq!(outcome.source_files[&ModuleId::default()].source, ROOT);
465        assert_eq!(
466            outcome.filenames[&ModuleId::default()].to_string(),
467            dir.path().join("main.kcl").display().to_string()
468        );
469        assert_eq!(outcome.source_files.len(), 2);
470    }
471
472    #[tokio::test]
473    async fn diagnostics_keep_the_client_entrypoint_name() {
474        let ctx = ExecutorContext::new_mock(None).await;
475        let program = Program::parse_no_errs("@settings(kclVersion = 3.0)\nvalue = 1").unwrap();
476        let project = UploadedProject::collect(&ctx, &program).await.unwrap();
477        let error = project
478            .outcome(
479                Err(ExecKclProjectErr::error_before_execution(remote_error(format!(
480                    "The entry point `{}` failed",
481                    project.request.entrypoint
482                )))),
483                1,
484            )
485            .unwrap_err();
486        assert_eq!(error.error.message(), "The entry point `main` failed");
487    }
488
489    #[tokio::test]
490    async fn retains_partial_output_on_fatal_errors_and_all_nonfatal_issues() {
491        let (_, project) = project().await;
492        let success = response();
493        let mut warning = crate::CompilationIssue::err(SourceRange::synthetic(), "keep warning");
494        warning.severity = crate::errors::Severity::Warning;
495        let error = project
496            .outcome(
497                Err(ExecKclProjectErr::builder()
498                    .error(remote_error("failed"))
499                    .non_fatal(vec![warning.clone()])
500                    .variables(success.variables)
501                    .operations(success.operations)
502                    .artifact_graph(success.artifact_graph)
503                    .scene_objects(success.scene_objects)
504                    .filenames(success.filenames)
505                    .build()),
506                1,
507            )
508            .unwrap_err();
509        assert_eq!(error.non_fatal, [warning.clone()]);
510        assert_eq!(error.engine_scene_generation, Some(1));
511        assert_eq!(error.source_files.len(), 2);
512        let response = response();
513        let outcome = project
514            .outcome(
515                Err(ExecKclProjectErr::builder()
516                    .non_fatal(vec![warning.clone()])
517                    .variables(response.variables)
518                    .operations(response.operations)
519                    .artifact_graph(response.artifact_graph)
520                    .scene_objects(response.scene_objects)
521                    .filenames(response.filenames)
522                    .build()),
523                1,
524            )
525            .unwrap();
526        assert_eq!(outcome.issues, [warning]);
527        assert_eq!(outcome.engine_scene_generation, 1);
528    }
529
530    #[test]
531    fn absolute_imports_are_rejected_on_all_platforms() {
532        for path in ["/tmp/part.kcl", "C:\\project\\part.kcl", "\\\\server\\part.kcl"] {
533            assert!(reject_absolute_import(path).is_err(), "{path}");
534        }
535        reject_absolute_import("../part.kcl").unwrap();
536    }
537
538    #[tokio::test]
539    async fn includes_gltf_buffers_without_interpreting_them_as_modules() {
540        let dir = tempfile::tempdir().unwrap();
541        let source = "@settings(kclVersion = 3.0)\nimport \"shape.gltf\" as shape\n";
542        std::fs::write(
543            dir.path().join("shape.gltf"),
544            r#"{"asset":{"version":"2.0"},"buffers":[{"uri":"buffer.bin","byteLength":4}]}"#,
545        )
546        .unwrap();
547        std::fs::write(dir.path().join("buffer.bin"), [0, 255, 1, 2]).unwrap();
548        let ctx = ExecutorContext::new_mock(Some(ExecutorSettings {
549            current_file: Some(TypedPath(dir.path().join("main.kcl"))),
550            ..Default::default()
551        }))
552        .await;
553        let project = UploadedProject::collect(&ctx, &Program::parse_no_errs(source).unwrap())
554            .await
555            .unwrap();
556        assert_eq!(project.files["buffer.bin"].1, [0, 255, 1, 2]);
557    }
558
559    #[tokio::test]
560    async fn uploaded_nested_entrypoints_resolve_parent_relative_assets() {
561        let dir = tempfile::tempdir().unwrap();
562        std::fs::create_dir(dir.path().join("sub")).unwrap();
563        let code = "@settings(kclVersion = 3.0)\nimport \"../part.step\" as part\n";
564        std::fs::write(dir.path().join("part.step"), b"step file sent to the engine").unwrap();
565        let client = ExecutorContext::new_mock(Some(ExecutorSettings {
566            current_file: Some(TypedPath(dir.path().join("sub/main.kcl"))),
567            ..Default::default()
568        }))
569        .await;
570        let program = Program::parse_no_errs(code).unwrap();
571        let project = UploadedProject::collect(&client, &program).await.unwrap().request;
572        let entrypoint = TypedPath::new(&project.entrypoint.to_string());
573        let files = crate::InMemoryFiles::new(
574            project
575                .files
576                .into_iter()
577                .map(|file| (file.path.to_string(), file.contents))
578                .collect(),
579        );
580        // This is the same filesystem and directory setup used by the engine,
581        // with a mock modeling transport so no external process is required.
582        let ctx = ExecutorContext::new_with_engine_and_fs(
583            Arc::new(EngineManager::new_mock()),
584            crate::new_file_system_handle(files),
585            ExecutorSettings {
586                project_directory: entrypoint.parent(),
587                current_file: Some(entrypoint),
588                geometry_only: true,
589                ..Default::default()
590            },
591        );
592        let mut state = ExecState::new(&ctx);
593        ctx.run(&program, &mut state).await.unwrap();
594    }
595
596    struct RecordingTransport {
597        requests: Arc<tokio::sync::Mutex<Vec<WebSocketRequest>>>,
598    }
599
600    #[async_trait::async_trait]
601    impl EngineTransport for RecordingTransport {
602        async fn inner_fire_modeling_cmd(
603            &self,
604            _: uuid::Uuid,
605            _: SourceRange,
606            _: WebSocketRequest,
607            _: std::collections::HashMap<uuid::Uuid, SourceRange>,
608        ) -> Result<(), KclError> {
609            panic!("unexpected unawaited command")
610        }
611        async fn inner_send_modeling_cmd(
612            &self,
613            id: uuid::Uuid,
614            _: SourceRange,
615            request: WebSocketRequest,
616            _: std::collections::HashMap<uuid::Uuid, SourceRange>,
617        ) -> Result<WebSocketResponse, KclError> {
618            let response = if matches!(request, WebSocketRequest::ExecKclProject { .. }) {
619                OkWebSocketResponseData::ExecKclProject {
620                    result: Box::new(Ok(response())),
621                }
622            } else {
623                OkWebSocketResponseData::Modeling {
624                    modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::Empty {},
625                }
626            };
627            self.requests.lock().await.push(request);
628            Ok(WebSocketResponse::success(Some(id), response))
629        }
630        async fn close(&self) -> Result<(), crate::engine_connection::TransportCloseError> {
631            Ok(())
632        }
633    }
634
635    #[tokio::test]
636    async fn executes_once_and_keeps_the_transport_for_followups() {
637        let requests = Arc::new(tokio::sync::Mutex::new(Vec::new()));
638        let mut engine = EngineManager::new_mock();
639        engine.transport = Arc::new(Box::new(RecordingTransport {
640            requests: requests.clone(),
641        }));
642        let ctx = ExecutorContext::new_with_engine(
643            Arc::new(engine),
644            ExecutorSettings {
645                geometry_only: true,
646                ..Default::default()
647            },
648        );
649        let mut state = ExecState::new(&ctx);
650        let outcome = ctx
651            .execute_remote_project(
652                &Program::parse_no_errs("@settings(kclVersion = 3.0)\nvalue = 1").unwrap(),
653                &mut state,
654            )
655            .await
656            .unwrap();
657        ctx.engine
658            .send_modeling_cmd(
659                &ctx.engine_batch,
660                state.next_uuid(),
661                SourceRange::synthetic(),
662                &kittycad_modeling_cmds::ModelingCmd::ViewIsometric(
663                    kittycad_modeling_cmds::ViewIsometric::builder().padding(0.1).build(),
664                ),
665            )
666            .await
667            .unwrap();
668        assert_eq!(outcome.engine_scene_generation, 1);
669        assert_eq!(ctx.cache.engine_scene_generation().await, 1);
670        let requests = requests.lock().await;
671        assert_eq!(requests.len(), 2);
672        assert!(matches!(requests[0], WebSocketRequest::ExecKclProject { .. }));
673        assert!(matches!(requests[1], WebSocketRequest::ModelingCmdReq(_)));
674        assert_ne!(state.next_uuid(), IdGenerator::new(None).next_uuid());
675    }
676}