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-<pid>, vectorcraft-mcp-fileio-<pid>,
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.
This commit is contained in:
dotCooCoo
2026-11-13 07:44:41 -08:00
parent 77970c6924
commit 98ea3231fa
5 changed files with 127 additions and 66 deletions
+73 -15
View File
@@ -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-<tag>-<pid>`, 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("<svg") && (svg.contains("<rect") || svg.contains("<path")), "{svg}");
// Save native, open it back.
let dc = tmp("out.vectorcraft");
let dc = dir.file("out.vectorcraft");
assert_eq!(call(&mut s, 7, "save_file", json!({"path": dc.to_str().unwrap()}))["isError"], false);
let r = call(&mut s, 8, "open_file", json!({"path": dc.to_str().unwrap()}));
assert_eq!(r["isError"], false, "{r}");
@@ -359,9 +412,9 @@ fn headless_end_to_end() {
assert_eq!(r["isError"], false, "{r}");
// PDF and PDF-compatible .ai open headless too.
let pdf_path = tmp("out.pdf");
let pdf_path = dir.file("out.pdf");
assert_eq!(call(&mut s, 20, "export", json!({"path": pdf_path.to_str().unwrap()}))["isError"], false);
let ai_path = tmp("out.ai");
let ai_path = dir.file("out.ai");
std::fs::copy(&pdf_path, &ai_path).unwrap();
for (id, p) in [(21, &pdf_path), (22, &ai_path)] {
let r = call(&mut s, id, "open_file", json!({"path": p.to_str().unwrap()}));
@@ -386,9 +439,10 @@ fn headless_end_to_end() {
/// succeeds, the expanded trace holds no image, and its paths keep the image's colors.
#[test]
fn trace_and_style_prompt_steps_run_in_order() {
let dir = TempFolder::new("trace-and-style");
let mut s = server();
// 60 × 40 pixels: a red and a blue block on white.
let png = tmp("trace-and-style.png");
let png = dir.file("trace-and-style.png");
image::RgbImage::from_fn(60, 40, |x, y| match (x, y) {
(5..25, 5..35) => 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<std::path::PathBuf> {
#[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());
+3 -2
View File
@@ -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();
+4 -8
View File
@@ -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);
}
+44 -37
View File
@@ -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<u8> {
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<u8> {
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<u8> {
#[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<String> { 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<String> {
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##"<svg xmlns="http://www.w3.org/2000/svg" width="200pt" height="50pt" viewBox="0 0 200 50">
@@ -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#"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect width="5" height="5"/></svg>"#).unwrap();
let mut h = Headless::new();
assert_eq!(text(&call_tool(&mut h, "open_file", &json!({"path": svg})))["format"], "svg");
+3 -4
View File
@@ -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);
}