mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
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:
+73
-15
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user