From 98ea3231faecf17ee16b332f17601c820ddaa4d5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 07:44:41 -0800 Subject: [PATCH] Tests: the vectorcraft-mcp tests remove the temp folders they create The MCP unit tests wrote their files into one folder per process under the temp dir (vectorcraft-mcp-test-, vectorcraft-mcp-fileio-, testkit's mcp-cancel folder) and nothing removed them. The export-as and links tests removed their folder at the end, so a failing test left it. Each test that writes files now holds a TempFolder, a folder of its own that starts empty and is removed with everything in it when the test ends, also when it panics, as the findfiles tests' Tree guard does (crates/engine/src/cmd/findfiles/tests.rs:49). A new test checks that a leftover folder is emptied and that the folder is removed on drop and after a panic. --- crates/mcp/src/tests.rs | 88 ++++++++++++++++++++++++----- crates/mcp/src/tests/concurrency.rs | 5 +- crates/mcp/src/tests/exportas.rs | 12 ++-- crates/mcp/src/tests/fileio.rs | 81 ++++++++++++++------------ crates/mcp/src/tests/links.rs | 7 +-- 5 files changed, 127 insertions(+), 66 deletions(-) diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index ffb6fcb93e..6529cf175d 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -45,10 +45,62 @@ fn text_of(result: &Value) -> String { result["content"].as_array().unwrap().iter().filter(|c| c["type"] == "text").map(|c| c["text"].as_str().unwrap().to_string()).collect() } -fn tmp(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("vectorcraft-mcp-test-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - dir.join(name) +/// A test's own folder under the temp dir, `vectorcraft-mcp--`, made empty when created +/// and removed with everything in it when dropped, also when the test panics. A removal that fails +/// (a file still open) is ignored. +struct TempFolder(std::path::PathBuf); + +impl TempFolder { + fn new(tag: &str) -> Self { + let dir = std::env::temp_dir().join(format!("vectorcraft-mcp-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + Self(dir) + } + + /// The path of `name` in the folder. + fn file(&self, name: &str) -> std::path::PathBuf { + self.0.join(name) + } +} + +impl std::ops::Deref for TempFolder { + type Target = std::path::Path; + fn deref(&self) -> &std::path::Path { + &self.0 + } +} + +impl Drop for TempFolder { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// A `TempFolder` starts empty, and dropping it removes it with the files and folders in it, also +/// when the test that holds it panics. +#[test] +fn temp_folders_start_empty_and_are_removed_when_dropped() { + let leftover = std::env::temp_dir().join(format!("vectorcraft-mcp-drop-check-{}", std::process::id())); + std::fs::create_dir_all(&leftover).unwrap(); + std::fs::write(leftover.join("old.txt"), b"old").unwrap(); + let dir = TempFolder::new("drop-check"); + assert_eq!(std::fs::read_dir(&*dir).unwrap().count(), 0, "a leftover folder is emptied"); + std::fs::write(dir.file("a.txt"), b"a").unwrap(); + std::fs::create_dir(dir.file("sub")).unwrap(); + std::fs::write(dir.file("sub").join("b.txt"), b"b").unwrap(); + let path = dir.to_path_buf(); + drop(dir); + assert!(!path.exists(), "{} is removed", path.display()); + + let panicked = std::panic::catch_unwind(|| { + let dir = TempFolder::new("panic-check"); + std::fs::write(dir.file("a.txt"), b"a").unwrap(); + panic!("the test fails with {} in use", dir.display()); + }); + assert!(panicked.is_err()); + let path = std::env::temp_dir().join(format!("vectorcraft-mcp-panic-check-{}", std::process::id())); + assert!(!path.exists(), "{} is removed after a panic", path.display()); } // ---------- framing ---------- @@ -286,6 +338,7 @@ fn headless_command_list_includes_ui_commands() { #[test] fn headless_end_to_end() { + let dir = TempFolder::new("headless-end-to-end"); let mut s = server(); rpc(&mut s, 1, "initialize", json!({"protocolVersion": PROTOCOL_VERSION})); s.handle_line(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#); @@ -334,7 +387,7 @@ fn headless_end_to_end() { assert_eq!(call(&mut s, 44, "doc_inspect", json!({"ids": [999_999]}))["isError"], true); // Screenshot returns image content (base64 PNG) and text. - let shot_path = tmp("shot.png"); + let shot_path = dir.file("shot.png"); let r = call(&mut s, 5, "screenshot", json!({"path": shot_path.to_str().unwrap()})); assert_eq!(r["isError"], false, "{r}"); let img = r["content"].as_array().unwrap().iter().find(|c| c["type"] == "image").expect("image content"); @@ -344,14 +397,14 @@ fn headless_end_to_end() { assert_eq!(std::fs::read(&shot_path).unwrap(), png); // Export SVG. - let svg_path = tmp("out.svg"); + let svg_path = dir.file("out.svg"); let r = call(&mut s, 6, "export", json!({"path": svg_path.to_str().unwrap()})); assert_eq!(r["isError"], false, "{r}"); let svg = std::fs::read_to_string(&svg_path).unwrap(); assert!(svg.contains(" image::Rgb([220, 30, 30]), (35..55, 5..35) => image::Rgb([30, 60, 200]), @@ -441,8 +495,9 @@ fn trace_and_style_prompt_steps_run_in_order() { /// result, as exports do. The image is a dark disc (r = 30 px) on blue, traced with Ignore White. #[test] fn screenshot_and_render_preview_draw_the_trace_view_with_trace_views() { + let dir = TempFolder::new("trace-views"); let mut s = server(); - let png = tmp("trace-views.png"); + let png = dir.file("trace-views.png"); image::RgbImage::from_fn(100, 100, |x, y| { let (dx, dy) = (f64::from(x) + 0.5 - 50.0, f64::from(y) + 0.5 - 50.0); if dx * dx + dy * dy <= 900.0 { image::Rgb([0, 0, 0]) } else { image::Rgb([170, 210, 255]) } @@ -490,6 +545,7 @@ fn screenshot_and_render_preview_draw_the_trace_view_with_trace_views() { #[test] fn headless_path_gesture_undo() { + let dir = TempFolder::new("path-gesture-undo"); let mut s = server(); let count = |s: &mut Server| -> u64 { let r = call(s, 90, "inspect_document", json!({})); @@ -543,9 +599,9 @@ fn headless_path_gesture_undo() { assert_eq!(r["isError"], false, "{r}"); let r = call(&mut s, 11, "select_tool", json!({"tool": "pen"})); assert_eq!(r["isError"], false, "{r}"); - let r = call(&mut s, 12, "export", json!({"path": tmp("x.png").to_str().unwrap(), "scale": 0.5})); + let r = call(&mut s, 12, "export", json!({"path": dir.file("x.png").to_str().unwrap(), "scale": 0.5})); assert_eq!(r["isError"], false, "{r}"); - assert_eq!(&std::fs::read(tmp("x.png")).unwrap()[..4], b"\x89PNG"); + assert_eq!(&std::fs::read(dir.file("x.png")).unwrap()[..4], b"\x89PNG"); } /// A hover's reply comes once the tool's highlight is ready, also when the tool finds it on another @@ -1074,8 +1130,9 @@ fn relative_to_cwd(p: &std::path::Path) -> Option { #[test] fn headless_file_tools_take_relative_paths() { + let dir = TempFolder::new("relative-paths"); let mut s = server(); - let file = tmp("relative.vectorcraft"); + let file = dir.file("relative.vectorcraft"); let Some(rel) = relative_to_cwd(&file) else { return }; let rel = rel.to_str().unwrap(); assert!(std::path::Path::new(rel).is_relative()); @@ -1547,13 +1604,14 @@ fn text_effect_pathfinder_transform_graph_and_wrap_tools() { #[test] fn oversized_screenshot_and_png_export_are_errors() { // 16383 pt at the maximum scale (16) is 262 128 px a side: an error result, not a crashed server. + let dir = TempFolder::new("oversized-export"); let mut s = server(); let r = call(&mut s, 1, "run_command", json!({"command": "file.new", "params": {"width": 16383, "height": 16383}})); assert_eq!(r["isError"], false, "{r}"); let r = call(&mut s, 2, "screenshot", json!({"scale": 16})); assert_eq!(r["isError"], true, "{r}"); assert!(text_of(&r).contains("pixels"), "{r}"); - let path = tmp("huge.png"); + let path = dir.file("huge.png"); let r = call(&mut s, 3, "export", json!({"path": path.to_str().unwrap(), "scale": 16})); assert_eq!(r["isError"], true, "{r}"); assert!(!path.exists()); diff --git a/crates/mcp/src/tests/concurrency.rs b/crates/mcp/src/tests/concurrency.rs index d291d1e9fe..8e244b4eb0 100644 --- a/crates/mcp/src/tests/concurrency.rs +++ b/crates/mcp/src/tests/concurrency.rs @@ -10,6 +10,7 @@ use std::time::{Duration, Instant}; use serde_json::{Value, json}; use vectorcraft_engine::cancel; +use super::TempFolder; use crate::{Backend, Server}; /// A backend whose `engine.execute` runs until cancelled (or for `limit`), like a long export. @@ -318,8 +319,8 @@ impl Backend for Gated { #[test] fn a_cancelled_export_through_the_real_engine_writes_nothing_and_leaves_the_session_usable() { - let path = vectorcraft_testkit::temp_dir("mcp-cancel").join("out.pdf"); - std::fs::remove_file(&path).ok(); + let dir = TempFolder::new("cancel"); + let path = dir.file("out.pdf"); let (read, mut to_server) = std::io::pipe().unwrap(); let (tx, replies) = mpsc::channel(); let (started_tx, started) = mpsc::channel(); diff --git a/crates/mcp/src/tests/exportas.rs b/crates/mcp/src/tests/exportas.rs index bc6e422d70..f46956bab8 100644 --- a/crates/mcp/src/tests/exportas.rs +++ b/crates/mcp/src/tests/exportas.rs @@ -3,6 +3,7 @@ use serde_json::{Value, json}; +use super::TempFolder; use crate::{Backend, Headless, call_tool}; fn text(r: &crate::ToolResult) -> Value { @@ -48,9 +49,7 @@ fn an_option_no_format_takes_is_an_error() { /// write anything. #[test] fn a_save_or_screens_option_no_format_takes_is_an_error() { - let dir = std::env::temp_dir().join(format!("vc-mcp-save-options-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); + let dir = TempFolder::new("save-options"); let mut h = Headless::new(); h.call("engine.execute", json!({"command": "file.new", "params": {"width": 50, "height": 30}})).unwrap(); let pdf = dir.join("a.pdf").to_string_lossy().into_owned(); @@ -71,9 +70,8 @@ fn a_save_or_screens_option_no_format_takes_is_an_error() { let e = h.call("engine.execute", json!({"command": "file.exportForScreens", "params": params})).unwrap_err(); assert!(e.contains(named), "file.exportForScreens: {e}"); } - assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "nothing was written"); + assert_eq!(std::fs::read_dir(&*dir).unwrap().count(), 0, "nothing was written"); h.call("app.save", json!({"path": pdf})).unwrap(); - let _ = std::fs::remove_dir_all(&dir); } #[test] @@ -97,8 +95,7 @@ fn raster_export_uses_document_resolution_unless_overridden() { #[test] fn export_with_a_range_writes_one_file_per_artboard() { let mut h = three_boards(); - let dir = std::env::temp_dir().join(format!("vectorcraft-mcp-exportas-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + let dir = TempFolder::new("exportas"); let path = dir.join("icon.png").to_string_lossy().to_string(); let args = json!({"path": path, "range": "1, 3", "options": {"useArtboards": true, "ppi": 144, "background": "white"}}); let r = text(&call_tool(&mut h, "export", &args)); @@ -120,5 +117,4 @@ fn export_with_a_range_writes_one_file_per_artboard() { let r = text(&call_tool(&mut h, "export", &json!({"format": format, "range": "1, 3"}))); assert_eq!(r["files"].as_array().map(Vec::len), Some(2), "{format}: {r}"); } - let _ = std::fs::remove_dir_all(dir); } diff --git a/crates/mcp/src/tests/fileio.rs b/crates/mcp/src/tests/fileio.rs index 39bcde8d2e..3a89588ecd 100644 --- a/crates/mcp/src/tests/fileio.rs +++ b/crates/mcp/src/tests/fileio.rs @@ -5,12 +5,12 @@ use std::io::{BufRead, BufReader, Write}; use serde_json::{Value, json}; +use super::TempFolder; use crate::{Backend, Headless, Remote, Server, call_tool, tool_definitions}; -fn tmp(name: &str) -> String { - let dir = std::env::temp_dir().join(format!("vectorcraft-mcp-fileio-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - dir.join(name).to_string_lossy().to_string() +/// The path of `name` in `dir`, as a string. +fn tmp(dir: &TempFolder, name: &str) -> String { + dir.file(name).to_string_lossy().to_string() } fn text(r: &crate::ToolResult) -> Value { @@ -29,22 +29,23 @@ fn two_boards() -> Headless { #[test] fn headless_opens_pdf_ai_and_templates() { + let dir = TempFolder::new("fileio-open"); let mut h = two_boards(); let pdf = h.call("engine.execute", json!({"command": "document.serialize", "params": {"format": "pdf"}})).unwrap(); let pdf = vectorcraft_format::base64_decode(pdf["dataBase64"].as_str().unwrap()).unwrap(); for name in ["x.pdf", "x.ai"] { - let path = tmp(name); + let path = tmp(&dir, name); std::fs::write(&path, &pdf).unwrap(); let r = text(&call_tool(&mut h, "open_file", &json!({"path": path}))); assert_eq!(r["title"], name); assert_eq!(h.session.doc().unwrap().doc.artboards.len(), 2, "one artboard per page"); } - let ait = tmp("x.ait"); + let ait = tmp(&dir, "x.ait"); std::fs::write(&ait, &pdf).unwrap(); let r = text(&call_tool(&mut h, "open_file", &json!({"path": ait}))); assert!(r["title"].as_str().unwrap().starts_with("Untitled-"), "{r}"); - let saved = h.call("engine.execute", json!({"command": "file.saveAsTemplate", "params": {"path": tmp("t.vectorcraft")}})).unwrap(); - let tpl = tmp("t.vctemplate"); + let saved = h.call("engine.execute", json!({"command": "file.saveAsTemplate", "params": {"path": tmp(&dir, "t.vectorcraft")}})).unwrap(); + let tpl = tmp(&dir, "t.vctemplate"); assert_eq!(saved["path"], tpl.as_str(), "saved with the template's extension: {saved}"); let r = text(&call_tool(&mut h, "open_file", &json!({"path": tpl}))); assert!(r["title"].as_str().unwrap().starts_with("Untitled-"), "{r}"); @@ -147,10 +148,11 @@ fn the_raster_scale_leaves_other_formats_alone() { #[test] fn exporting_native_keeps_the_document_path() { + let dir = TempFolder::new("fileio-keep-path"); let mut h = two_boards(); - let doc = tmp("keep.vectorcraft"); + let doc = tmp(&dir, "keep.vectorcraft"); h.call("app.save", json!({"path": doc})).unwrap(); - let copy = tmp("copy.vectorcraft"); + let copy = tmp(&dir, "copy.vectorcraft"); let r = text(&call_tool(&mut h, "export", &json!({"path": copy}))); assert_eq!(r["format"], "vectorcraft"); assert_eq!(h.session.doc().unwrap().path.as_deref(), Some(doc.as_str())); @@ -167,9 +169,9 @@ fn exporting_native_keeps_the_document_path() { assert_eq!(h.session.doc().unwrap().path.as_deref(), Some(doc.as_str())); } -/// The bytes `save_file` writes to `name` with `args`. -fn saved_with(h: &mut Headless, name: &str, mut args: Value) -> Vec { - let path = tmp(name); +/// The bytes `save_file` writes to `name` in `dir` with `args`. +fn saved_with(h: &mut Headless, dir: &TempFolder, name: &str, mut args: Value) -> Vec { + let path = tmp(dir, name); args["path"] = json!(path); let r = text(&call_tool(h, "save_file", &args)); assert_eq!(r["format"], "vectorcraft", "{r}"); @@ -178,22 +180,23 @@ fn saved_with(h: &mut Headless, name: &str, mut args: Value) -> Vec { #[test] fn save_file_takes_the_native_save_options() { + let dir = TempFolder::new("fileio-save-options"); let mut h = two_boards(); - let gz = saved_with(&mut h, "gz.vectorcraft", json!({"compress": true})); + let gz = saved_with(&mut h, &dir, "gz.vectorcraft", json!({"compress": true})); assert_eq!(gz[..2], [0x1f, 0x8b], "gzipped"); - let plain = saved_with(&mut h, "plain.vectorcraft", json!({"compress": false})); + let plain = saved_with(&mut h, &dir, "plain.vectorcraft", json!({"compress": false})); assert_eq!(plain[0], b'{', "plain JSON"); let nodes = h.session.doc().unwrap().doc.node_count(); for name in ["gz.vectorcraft", "plain.vectorcraft"] { let mut back = Headless::new(); - text(&call_tool(&mut back, "open_file", &json!({"path": tmp(name)}))); + text(&call_tool(&mut back, "open_file", &json!({"path": tmp(&dir, name)}))); assert_eq!(back.session.doc().unwrap().doc.node_count(), nodes, "{name} reopens"); } - let opt = saved_with(&mut h, "opt.vectorcraft", json!({"options": {"compress": true}})); + let opt = saved_with(&mut h, &dir, "opt.vectorcraft", json!({"options": {"compress": true}})); assert_eq!(opt[..2], [0x1f, 0x8b], "`options` carries them too"); - let v3: Value = serde_json::from_slice(&saved_with(&mut h, "v3.vectorcraft", json!({"version": 3, "preview": null}))).unwrap(); + let v3: Value = serde_json::from_slice(&saved_with(&mut h, &dir, "v3.vectorcraft", json!({"version": 3, "preview": null}))).unwrap(); assert_eq!(v3["version"], 3); - let shown = saved_with(&mut h, "preview.vectorcraft", json!({"preview": true})); + let shown = saved_with(&mut h, &dir, "preview.vectorcraft", json!({"preview": true})); assert!(vectorcraft_format::preview(&shown).is_some(), "a preview"); assert!(vectorcraft_format::preview(&plain).is_none(), "off by default"); // `precision` rounds the path coordinates of that save only. @@ -201,12 +204,12 @@ fn save_file_takes_the_native_save_options() { p.call("engine.execute", json!({"command": "file.new", "params": {"width": 100, "height": 100}})).unwrap(); p.call("engine.execute", json!({"command": "path.create", "params": {"anchors": [{"x": 10.123456, "y": 20.654321}, {"x": 30.5, "y": 40.25}]}})) .unwrap(); - let exact = String::from_utf8(saved_with(&mut p, "exact.vectorcraft", json!({"compress": false}))).unwrap(); + let exact = String::from_utf8(saved_with(&mut p, &dir, "exact.vectorcraft", json!({"compress": false}))).unwrap(); assert!(exact.contains("10.123456") && exact.contains("20.654321"), "{exact}"); - let rounded = String::from_utf8(saved_with(&mut p, "rounded.vectorcraft", json!({"compress": false, "precision": 1}))).unwrap(); + let rounded = String::from_utf8(saved_with(&mut p, &dir, "rounded.vectorcraft", json!({"compress": false, "precision": 1}))).unwrap(); assert!(!rounded.contains("10.123456") && !rounded.contains("20.654321") && rounded.contains("20.7"), "{rounded}"); // Version 2 files can't be compressed: the engine's error comes back. - let r = call_tool(&mut h, "save_file", &json!({"path": tmp("v2.vectorcraft"), "version": 2, "compress": true})); + let r = call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, "v2.vectorcraft"), "version": 2, "compress": true})); assert!(r.is_error && r.content[0]["text"].as_str().unwrap().contains("can't be compressed"), "{r:?}"); // The options are in the schema; a misspelled one is still refused. @@ -214,9 +217,9 @@ fn save_file_takes_the_native_save_options() { let call = |s: &mut Server, args: Value| { s.handle(json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "save_file", "arguments": args}})).unwrap() }; - let r = call(&mut s, json!({"path": tmp("ok.vectorcraft"), "compress": true})); + let r = call(&mut s, json!({"path": tmp(&dir, "ok.vectorcraft"), "compress": true})); assert_eq!(r["result"]["isError"], false, "{r}"); - let r = call(&mut s, json!({"path": tmp("typo.vectorcraft"), "compresss": true})); + let r = call(&mut s, json!({"path": tmp(&dir, "typo.vectorcraft"), "compresss": true})); assert_eq!(r["error"]["code"], -32602, "{r}"); assert!(r["error"]["message"].as_str().unwrap().contains("unknown argument `compresss`"), "{r}"); } @@ -225,12 +228,13 @@ fn save_file_takes_the_native_save_options() { /// that disagrees with the same key in `options` is an error. #[test] fn save_file_warns_about_options_it_does_not_use() { + let dir = TempFolder::new("fileio-unused-options"); let mut h = two_boards(); let warnings = |r: &Value| -> Vec { r["warnings"].as_array().unwrap().iter().map(|w| w.as_str().unwrap().to_string()).collect() }; let ignored = |w: &[String], key: &str| w.iter().any(|w| w.starts_with(&format!("`{key}` was not used"))); // SVG reads none of them, at the top level or in `options`. - let path = tmp("ignored.svg"); + let path = tmp(&dir, "ignored.svg"); let r = text(&call_tool(&mut h, "save_file", &json!({"path": path, "compress": true, "version": 3, "options": {"preview": true}}))); assert_eq!(r["format"], "svg", "{r}"); let w = warnings(&r); @@ -239,16 +243,15 @@ fn save_file_warns_about_options_it_does_not_use() { assert!(w.iter().any(|w| w.contains("SVG")), "names the format: {w:?}"); assert!(std::fs::read(&path).unwrap().starts_with(b"<"), "plain SVG"); // A .ai file reads `compress` but not `version`. - let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp("ignored.ai"), "compress": false, "version": 3}))); + let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, "ignored.ai"), "compress": false, "version": 3}))); let w = warnings(&r); assert!(ignored(&w, "version") && !ignored(&w, "compress"), "{w:?}"); // `precision` is read by native files only. - let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp("ignored-precision.svg"), "precision": 2}))); + let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, "ignored-precision.svg"), "precision": 2}))); assert!(ignored(&warnings(&r), "precision"), "{r}"); // A top-level value that disagrees with `options` is refused, and nothing is written. - let path = tmp("both.vectorcraft"); - let _ = std::fs::remove_file(&path); + let path = tmp(&dir, "both.vectorcraft"); let r = call_tool(&mut h, "save_file", &json!({"path": path, "compress": true, "options": {"compress": false}})); let e = r.content[0]["text"].as_str().unwrap(); assert!(r.is_error && e.contains("`compress: true` and `options.compress: false` disagree"), "{r:?}"); @@ -257,7 +260,7 @@ fn save_file_warns_about_options_it_does_not_use() { for mut args in [json!({"compress": true, "options": {"compress": true}}), json!({"compress": false, "version": 3, "preview": true, "precision": 4})] { - args["path"] = json!(tmp("quiet.vectorcraft")); + args["path"] = json!(tmp(&dir, "quiet.vectorcraft")); let r = text(&call_tool(&mut h, "save_file", &args)); assert_eq!(r["warnings"], json!([]), "{args}: {r}"); } @@ -286,6 +289,7 @@ fn save_file_warns_about_options_it_does_not_use() { /// when the save reuses the options the document was last saved with. #[test] fn save_file_notes_name_the_ai_file_it_writes() { + let dir = TempFolder::new("fileio-ai-notes"); let mut h = Headless::with_document(); let notes = |r: &Value| -> Vec { r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).filter(|w| w.contains("was not used")).map(str::to_string).collect() @@ -296,21 +300,22 @@ fn save_file_notes_name_the_ai_file_it_writes() { ("no-pdf.ai", json!({"pdfCompatible": false, "preview": true}), "non-PDF-compatible .ai"), ("pdf.ai", json!({"pdfCompatible": true, "preview": true}), "PDF-compatible .ai"), ] { - let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp(name), "version": 3, "options": options}))); + let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, name), "version": 3, "options": options}))); let expected = [format!("`version` was not used: {label} files don't read it"), format!("`options.preview` was not used: {label} files don't read it")]; assert_eq!(notes(&r), expected, "{name}: {r}"); } // A save given no option the file reads writes it as last saved: a legacy `.ai` again. - text(&call_tool(&mut h, "save_file", &json!({"path": tmp("again.ai"), "options": {"aiVersion": "7"}}))); - let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp("again.ai"), "options": {"preview": true}}))); + text(&call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, "again.ai"), "options": {"aiVersion": "7"}}))); + let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, "again.ai"), "options": {"preview": true}}))); assert_eq!(notes(&r), ["`options.preview` was not used: legacy .ai (version 7) files don't read it"], "{r}"); - assert!(std::fs::read(tmp("again.ai")).unwrap().starts_with(b"%!PS"), "a legacy .ai is written"); + assert!(std::fs::read(tmp(&dir, "again.ai")).unwrap().starts_with(b"%!PS"), "a legacy .ai is written"); } #[test] fn save_file_is_the_same_call_in_the_app_and_headless() { - let path = tmp("app.vectorcraft"); + let dir = TempFolder::new("fileio-app-save"); + let path = tmp(&dir, "app.vectorcraft"); let (addr, app) = recording_app(); let mut remote = Remote::connect(&addr).unwrap(); let args = json!({"path": path, "compress": true, "version": 3, "preview": null, "precision": 2, "options": {"preview": true}}); @@ -392,7 +397,8 @@ fn pdf_pages(pdf: &[u8]) -> usize { /// only, in the document and in the exported PDF. #[test] fn document_color_mode_puts_rgb_greys_on_the_black_plate() { - let svg = tmp("greys.svg"); + let dir = TempFolder::new("fileio-greys"); + let svg = tmp(&dir, "greys.svg"); std::fs::write( &svg, r##" @@ -447,7 +453,8 @@ fn open_file_passes_the_importer_on() { let tools = tool_definitions(); let open = tools.iter().find(|t| t["name"] == "open_file").unwrap(); assert!(open["inputSchema"]["properties"].get("importer").is_some(), "{open}"); - let svg = tmp("importer.svg"); + let dir = TempFolder::new("fileio-importer"); + let svg = tmp(&dir, "importer.svg"); std::fs::write(&svg, r#""#).unwrap(); let mut h = Headless::new(); assert_eq!(text(&call_tool(&mut h, "open_file", &json!({"path": svg})))["format"], "svg"); diff --git a/crates/mcp/src/tests/links.rs b/crates/mcp/src/tests/links.rs index 20298fc612..61d660a8d8 100644 --- a/crates/mcp/src/tests/links.rs +++ b/crates/mcp/src/tests/links.rs @@ -3,6 +3,7 @@ use serde_json::{Value, json}; +use super::TempFolder; use crate::{Headless, call_tool}; fn run(h: &mut Headless, command: &str, params: Value) -> Value { @@ -13,9 +14,8 @@ fn run(h: &mut Headless, command: &str, params: Value) -> Value { #[test] fn headless_links_report_missing_files_and_relink() { - let dir = std::env::temp_dir().join(format!("vectorcraft-mcp-links-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let file = |n: &str| dir.join(n).to_string_lossy().into_owned(); + let dir = TempFolder::new("links"); + let file = |n: &str| dir.file(n).to_string_lossy().into_owned(); let mut h = Headless::new(); run(&mut h, "file.new", json!({"width": 300, "height": 200})); let png = run(&mut h, "document.serialize", json!({"format": "png"})); @@ -30,5 +30,4 @@ fn headless_links_report_missing_files_and_relink() { let r = run(&mut h, "links.relink", json!({"ids": [id], "path": file("b.png")})); assert_eq!(r["relinked"], json!([id])); assert_eq!(run(&mut h, "links.check", json!({}))["links"][0]["status"], "ok"); - let _ = std::fs::remove_dir_all(dir); }