Files
solidcraft-upstream/crates/engine/src/tests/path_scope.rs
T
NOWN-DEV 08b6a8fe0f fix: the server's own folders under a short-named temp path are expanded, not refused (#1055)
On Windows the temporary folder often has an 8.3 short name, which the path scope refuses. vfs::long_names spells folders SolidCraft derives from the environment with long names, and the MCP scratch folder and the recovery folder use it, so screenshot without a path and the recovery commands work there again.
2026-10-14 09:05:43 -04:00

67 lines
4.0 KiB
Rust

//! Commands run for an agent (#205): with a `vfs::PathScope` active, every command that reads,
//! writes or deletes a file it was given a path for stays inside the scope's folders.
use serde_json::json;
use solidcraft_io::vfs::{PathScope, scoped};
use crate::*;
#[test]
fn file_commands_stay_inside_the_agents_folders() {
let base = solidcraft_io::vfs::temp_dir().join(format!("solidcraft-path-scope-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let (root, out) = (base.join("root"), base.join("out"));
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&out).unwrap();
let at = |d: &std::path::Path, f: &str| d.join(f).to_string_lossy().to_string();
let mut s = Session::default();
s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap();
std::fs::write(out.join("part.solidcraft"), solidcraft_io::write_design(&s.doc)).unwrap();
std::fs::write(out.join("p.csv"), "name,expression\nw,5 mm\n").unwrap();
scoped(Some(PathScope::new([root.clone()])), || {
// Inside: the usual workflow.
s.execute("file.save_as", &json!({"path": at(&root, "part.solidcraft")})).unwrap();
s.execute("file.export", &json!({"path": at(&root, "part.step")})).unwrap();
s.execute("doc.open", &json!({"path": at(&root, "part.solidcraft")})).unwrap();
// Outside, directly or through `..`: refused, with the reason.
for (cmd, p) in [
("file.save_as", json!({"path": at(&out, "x.solidcraft")})),
("file.save", json!({"path": at(&root, "../out/x.solidcraft")})),
("file.export", json!({"path": at(&out, "x.stl")})),
("file.export", json!({"path": at(&out, "x.json"), "format": "json"})),
("file.save_as", json!({"path": at(&out, "x.json"), "format": "json", "preview": true})),
("parameters.export", json!({"path": at(&out, "x.csv")})),
("doc.open", json!({"path": at(&out, "part.solidcraft")})),
("parameters.import", json!({"path": at(&out, "p.csv")})),
("sketch.insert_dxf", json!({"path": at(&out, "a.dxf")})),
("doc.recovery_list", json!({"dir": out.to_string_lossy()})),
("doc.recovery_discard", json!({"dir": out.to_string_lossy(), "id": "part"})),
("doc.recover", json!({"dir": out.to_string_lossy(), "id": "part", "discard": true})),
] {
let e = s.execute(cmd, &p).unwrap_err().to_string();
assert!(e.contains("outside the folders this agent may use"), "{cmd}: {e}");
}
// A font file named by path is a file read too; the built-in font still works.
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
let e = s.execute("sketch.text", &json!({"text": "A", "at": [0, 0], "font": at(&out, "f.ttf")})).unwrap_err().to_string();
assert!(e.contains("outside the folders this agent may use"), "sketch.text: {e}");
s.execute("sketch.text", &json!({"text": "A", "at": [0, 0]})).unwrap();
s.execute("sketch.finish", &json!({})).unwrap();
// Saving follows a link to its file (#845), so a link that leads out is refused.
#[cfg(unix)]
{
std::os::unix::fs::symlink(out.join("part.solidcraft"), root.join("linked.solidcraft")).unwrap();
let e = s.execute("file.save_as", &json!({"path": at(&root, "linked.solidcraft")})).unwrap_err().to_string();
assert!(e.contains("outside the folders this agent may use"), "save through a link: {e}");
}
// The default recovery folder isn't one of the agent's folders either.
assert!(s.execute("doc.recovery_list", &json!({})).is_err());
});
let left: Vec<_> = std::fs::read_dir(&out).unwrap().flatten().map(|e| e.file_name()).collect();
assert_eq!(left.len(), 2, "nothing written or deleted outside: {left:?}");
// Without a scope (the user's own dialogs) nothing changes.
s.execute("file.export", &json!({"path": at(&out, "x.stl")})).unwrap();
let _ = std::fs::remove_dir_all(&base);
}