//! Integration tests on the demo project: load, render every view, export, //! check, document operations through a Workspace, MCP round trips. use isokit_project::mcp; use isokit_project::ops::Op; use isokit_project::workspace::Workspace; use isokit_project::{check, page, Project}; use serde_json::{json, Value}; use std::path::{Path, PathBuf}; fn demo() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/demo") } /// A throwaway copy of the demo (tests edit it). fn copy_demo(name: &str) -> PathBuf { let dst = std::env::temp_dir().join(format!("isokit-test-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dst); fn cp(a: &Path, b: &Path) { std::fs::create_dir_all(b).unwrap(); for e in std::fs::read_dir(a).unwrap().flatten() { let p = e.path(); let n = e.file_name(); if p.is_dir() { if n != "dist" { cp(&p, &b.join(n)); } } else { std::fs::copy(&p, b.join(n)).unwrap(); } } } cp(&demo(), &dst); dst } #[test] fn demo_renders_every_view_cleanly() { let p = Project::load(demo()).unwrap(); assert!(p.runtime.is_ok(), "{:?}", p.runtime.as_ref().err()); for l in p.manifest.levels() { for rot in 0..4 { let v = p.render(&l.id, rot, None, false).unwrap(); assert!(v.warnings.is_empty(), "{}/{rot}: {:?}", l.id, v.warnings); assert!(!v.html.contains("NaN")); assert!( v.nodes < p.manifest.build.budget, "{}/{rot}: {} nodes", l.id, v.nodes ); for o in &p.doc.objects { assert!( v.boxes.contains_key(&o.id), "{} not drawn in {}/{rot}", o.id, l.id ); assert!(v.info.contains_key(&o.id), "{} has no tooltip", o.id); } // the script's sign and the project prefab are there assert!(v.html.contains("data-id=\"sign\"")); assert!(v.html.contains("data-obj=\"kiosk\"")); } } } #[test] fn demo_passes_the_check() { let p = Project::load(demo()).unwrap(); let f = check::check(&p); assert!(f.is_empty(), "{f:?}"); } #[test] fn static_page_and_svg_export() { let p = Project::load(demo()).unwrap(); let html = page::static_page(&p).unwrap(); assert!( html.contains("IsoKit.mount") && html.contains("\"site/1\"") && html.contains("\"gf/3\"") ); assert!( !html.contains("/*ISOKIT_"), "a placeholder was left in the page" ); assert!( html.contains(".iso-brick { --b: var(--iso-brick); }"), "project theme merged" ); let svg = page::svg(&p, "site", 1, true).unwrap(); assert!( svg.starts_with("") && svg.contains("iso-wall") || svg.contains("iso-render") ); } #[test] fn workspace_edits_save_and_undo() { let dir = copy_demo("ws"); let mut ws = Workspace::open(&dir).unwrap(); let n = ws.project.doc.objects.len(); let id = ws .apply( serde_json::from_value::(json!({"op": "add", "type": "tree", "x": 1, "y": 2})) .unwrap(), ) .unwrap() .id .unwrap(); // saved to disk let on_disk = Project::load(&dir).unwrap(); assert_eq!(on_disk.doc.objects.len(), n + 1); assert!(on_disk.doc.get(&id).is_some()); ws.undo().unwrap(); assert_eq!(Project::load(&dir).unwrap().doc.objects.len(), n); ws.redo().unwrap(); assert_eq!(ws.project.doc.objects.len(), n + 1); // an outside edit (another process) is picked up and can be undone let mut d = on_disk.doc.clone(); d.objects.retain(|o| o.kind != "lamp"); std::fs::write( dir.join("scene.json"), serde_json::to_string_pretty(&d).unwrap(), ) .unwrap(); assert!(ws.refresh()); assert!(ws.project.doc.objects.iter().all(|o| o.kind != "lamp")); ws.undo().unwrap(); assert!(ws.project.doc.objects.iter().any(|o| o.kind == "lamp")); let _ = std::fs::remove_dir_all(&dir); } fn call(ws: &mut Workspace, id: u64, name: &str, args: Value) -> (bool, Value) { let r = mcp::handle(ws, &json!({"jsonrpc": "2.0", "id": id, "method": "tools/call", "params": {"name": name, "arguments": args}})).unwrap(); let res = &r["result"]; let text = res["content"][0]["text"] .as_str() .unwrap_or_default() .to_string(); ( res["isError"].as_bool().unwrap_or(true), serde_json::from_str(&text).unwrap_or(Value::String(text)), ) } #[test] fn mcp_round_trip() { let dir = copy_demo("mcp"); let mut ws = Workspace::open(&dir).unwrap(); let init = mcp::handle(&mut ws, &json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}})).unwrap(); assert_eq!(init["result"]["protocolVersion"], "2025-06-18"); assert!(mcp::handle( &mut ws, &json!({"jsonrpc": "2.0", "method": "notifications/initialized"}) ) .is_none()); let list = mcp::handle( &mut ws, &json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}), ) .unwrap(); let names: Vec<&str> = list["result"]["tools"] .as_array() .unwrap() .iter() .map(|t| t["name"].as_str().unwrap()) .collect(); for t in [ "project_info", "list_prefabs", "add_object", "update_object", "render_view", "check_project", "build_html", ] { assert!(names.contains(&t), "{t}"); } let (err, info) = call(&mut ws, 3, "project_info", json!({})); assert!(!err && info["objects"].as_u64().unwrap() > 10); let (err, pf) = call(&mut ws, 4, "list_prefabs", json!({"type": "building"})); assert!( !err && pf["props"] .as_array() .unwrap() .iter() .any(|p| p["key"] == "floors") ); let (err, added) = call( &mut ws, 5, "add_object", json!({"type": "building", "x": 20, "y": 15, "props": {"w": 8, "d": 6, "floors": 1}, "name": "Store"}), ); assert!(!err, "{added}"); let id = added["id"].as_str().unwrap().to_string(); let b = added["object"]["box"].as_array().unwrap(); assert_eq!( (b[0].as_f64().unwrap(), b[3].as_f64().unwrap()), (16.0, 24.0) ); let (err, upd) = call( &mut ws, 6, "update_object", json!({"id": id, "rot": 90, "props": {"roof": "gable"}}), ); assert!(!err, "{upd}"); assert_eq!(upd["object"]["rot"], 90.0); let (err, bad) = call( &mut ws, 7, "update_object", json!({"id": id, "props": {"floors": 99}}), ); assert!(err && bad.as_str().unwrap().contains("outside"), "{bad}"); let (err, ops) = call( &mut ws, 8, "apply_ops", json!({"ops": [ {"op": "add", "type": "tree", "x": 0, "y": 18}, {"op": "add", "type": "tree", "x": 4, "y": 18}, {"op": "remove", "id": "tree-1"} ]}), ); assert!(!err, "{ops}"); let (err, view) = call( &mut ws, 9, "render_view", json!({"level": "gf", "rotation": 2}), ); assert!(!err && view["boxes"].get(&id).is_some(), "{view}"); let (err, svg) = call(&mut ws, 10, "render_view", json!({"format": "svg"})); assert!(!err && svg.as_str().unwrap().starts_with("