From 1f613f8c66eb8caaf42688f6d3724c9ba1bb1ae3 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 08:36:04 -0700 Subject: [PATCH 1/2] MCP: the several-roots tests expect Windows paths without the \\?\ prefix Four tests from #1678 fail on Windows. Each one builds its paths, or the text it expects, from a folder resolved with `std::fs::canonicalize`, which adds the `\\?\` prefix on Windows. `AutomationRoots::describe` and the rejection messages show the roots without that prefix, so the expected text does not match. Windows does not resolve `..` in a path with the prefix, so the checks refuse every such path as one they cannot resolve, including the ones the tests expect to pass from one root into another. The code the tests check is unchanged. The engine tests take the resolved folder through `file_access::plain`, which the roots use for the text they show. The CLI test strips the prefix the same way. --- apps/designcraft-cli/tests/it/cli.rs | 8 +++++++- crates/engine/src/file_access.rs | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/designcraft-cli/tests/it/cli.rs b/apps/designcraft-cli/tests/it/cli.rs index 467b231f6..8e73fc314 100644 --- a/apps/designcraft-cli/tests/it/cli.rs +++ b/apps/designcraft-cli/tests/it/cli.rs @@ -411,8 +411,14 @@ fn mcp_automation_roots_take_several_folders() { for d in ["in", "shared", "out", "scratch", "outside", "in2", "scratch2"] { std::fs::create_dir_all(base.join(d)).unwrap(); } - // Resolved, as the server shows the roots (`/var` is `/private/var` on macOS). + // Resolved, as the server shows the roots (`/var` is `/private/var` on macOS), and without the + // `\\?\` prefix that `canonicalize` adds on Windows: the server shows the roots without it, and + // Windows does not resolve `..` in a path that has it. let base = std::fs::canonicalize(base).unwrap(); + let base = match base.to_str().and_then(|t| t.strip_prefix(r"\\?\")) { + Some(rest) => std::path::PathBuf::from(rest), + None => base, + }; for f in ["in/a.designcraft", "shared/b.designcraft", "outside/secret.designcraft", "parent.designcraft", "in2/c.designcraft"] { make_document(&base.join(f)); } diff --git a/crates/engine/src/file_access.rs b/crates/engine/src/file_access.rs index 31d61e382..17c6526d1 100644 --- a/crates/engine/src/file_access.rs +++ b/crates/engine/src/file_access.rs @@ -412,9 +412,11 @@ mod tests { /// siblings `in2` and `scratch2` whose names start like a root's, and a file in their parent. /// The folder is returned resolved (`/var` is `/private/var` on macOS), as the roots show it, so /// a path that passes only because it was resolved is the case being tested and nothing else. + /// It has no `\\?\` prefix on Windows, where the roots are shown without it and `..` is not + /// resolved in a path that has it. fn several(name: &str) -> (PathBuf, Arc) { let (base, _) = folders(name); - let base = std::fs::canonicalize(base).unwrap(); + let base = plain(std::fs::canonicalize(base).unwrap()); for d in ["shared", "scratch", "scratch2"] { std::fs::create_dir_all(base.join(d)).unwrap(); } @@ -492,7 +494,7 @@ mod tests { assert!(e.starts_with("cannot open the automation write root"), "{e}"); // A folder given twice (here once through `..`) counts once. let twice = AutomationRoots::new(&[&base.join("in"), &base.join("in/sub/.."), &base.join("in")], &[]).unwrap().unwrap(); - let real = std::fs::canonicalize(&base).unwrap(); + let real = plain(std::fs::canonicalize(&base).unwrap()); assert_eq!(twice.describe(), format!("read {}; write none", s(real.join("in")))); let e = twice.check(s(base.join("outside/x")), Access::Read).unwrap_err(); assert!(e.contains("outside the read root ") && !e.contains("roots"), "{e}"); From bab76dcd111d23b6387ef2919105ef55c91d9768 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 10:43:16 -0700 Subject: [PATCH 2/2] MCP: the several-roots CLI test keeps a share path a share path The test removed only the \\?\ prefix from the folder it resolved, so a temporary folder on a network share became the relative path UNC\server\share\... . It now removes the \\?\UNC\ prefix first and keeps the two leading backslashes, as file_access::plain does, with a test of both forms. --- apps/designcraft-cli/tests/it/cli.rs | 29 +++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/designcraft-cli/tests/it/cli.rs b/apps/designcraft-cli/tests/it/cli.rs index 8e73fc314..77ee74cff 100644 --- a/apps/designcraft-cli/tests/it/cli.rs +++ b/apps/designcraft-cli/tests/it/cli.rs @@ -347,6 +347,29 @@ fn make_document(path: &std::path::Path) { assert!(path.exists()); } +/// `path` without the `\\?\` or `\\?\UNC\` prefix that `canonicalize` adds on Windows, as the +/// server shows a root (`file_access::plain`). +fn without_verbatim_prefix(path: std::path::PathBuf) -> std::path::PathBuf { + let Some(text) = path.to_str() else { + return path; + }; + if let Some(rest) = text.strip_prefix(r"\\?\UNC\") { + return std::path::PathBuf::from(format!(r"\\{rest}")); + } + match text.strip_prefix(r"\\?\") { + Some(rest) => std::path::PathBuf::from(rest), + None => path, + } +} + +#[test] +fn without_verbatim_prefix_keeps_a_share_a_share() { + let p = |t: &str| without_verbatim_prefix(std::path::PathBuf::from(t)); + assert_eq!(p(r"\\?\UNC\server\share\dir"), std::path::PathBuf::from(r"\\server\share\dir")); + assert_eq!(p(r"\\?\C:\Users\dir"), std::path::PathBuf::from(r"C:\Users\dir")); + assert_eq!(p("/private/var/dir"), std::path::PathBuf::from("/private/var/dir")); +} + #[test] fn mcp_automation_roots_confine_the_server() { let base = std::env::temp_dir().join(format!("dc-cli-roots-{}", std::process::id())); @@ -414,11 +437,7 @@ fn mcp_automation_roots_take_several_folders() { // Resolved, as the server shows the roots (`/var` is `/private/var` on macOS), and without the // `\\?\` prefix that `canonicalize` adds on Windows: the server shows the roots without it, and // Windows does not resolve `..` in a path that has it. - let base = std::fs::canonicalize(base).unwrap(); - let base = match base.to_str().and_then(|t| t.strip_prefix(r"\\?\")) { - Some(rest) => std::path::PathBuf::from(rest), - None => base, - }; + let base = without_verbatim_prefix(std::fs::canonicalize(base).unwrap()); for f in ["in/a.designcraft", "shared/b.designcraft", "outside/secret.designcraft", "parent.designcraft", "in2/c.designcraft"] { make_document(&base.join(f)); }