use serde_json::json;
use tempfile::TempDir;
use trusty_memory::tools::dispatch_tool;
use trusty_memory::AppState;
fn fixture() -> (AppState, TempDir, TempDir) {
let root_tmp = tempfile::tempdir().expect("data root tempdir");
let cwd_tmp = tempfile::tempdir().expect("cwd tempdir");
let state = AppState::new(root_tmp.path().to_path_buf());
state.set_ready();
(state, root_tmp, cwd_tmp)
}
#[tokio::test]
async fn force_true_creates_custom_slug_palace_with_isolated_dirs() {
let (state, _root, cwd) = fixture();
let cwd_str = cwd.path().to_string_lossy().to_string();
for slug in ["acme-app", "tenant-42"] {
let created = dispatch_tool(
&state,
"palace_create",
json!({ "name": slug, "force": true, "cwd": cwd_str }),
)
.await
.unwrap_or_else(|e| panic!("force create '{slug}' should succeed: {e:#}"));
assert_eq!(created["palace_id"], slug);
assert_eq!(created["status"], "created");
}
let listed = dispatch_tool(&state, "palace_list", json!({}))
.await
.expect("palace_list");
let palaces = listed["palaces"].as_array().expect("palaces array");
let names: Vec<&str> = palaces.iter().filter_map(|v| v.as_str()).collect();
assert!(names.contains(&"acme-app"), "acme-app present: {names:?}");
assert!(names.contains(&"tenant-42"), "tenant-42 present: {names:?}");
let info_a = dispatch_tool(&state, "palace_info", json!({ "palace": "acme-app" }))
.await
.expect("palace_info acme-app");
let info_b = dispatch_tool(&state, "palace_info", json!({ "palace": "tenant-42" }))
.await
.expect("palace_info tenant-42");
let dir_a = info_a["data_dir"].as_str().expect("acme data_dir");
let dir_b = info_b["data_dir"].as_str().expect("tenant data_dir");
assert_ne!(dir_a, dir_b, "palaces must have distinct data dirs");
assert!(dir_a.ends_with("acme-app"), "dir_a = {dir_a}");
assert!(dir_b.ends_with("tenant-42"), "dir_b = {dir_b}");
assert!(
std::path::Path::new(dir_a).is_dir(),
"acme-app data dir exists on disk"
);
}
#[tokio::test]
async fn force_false_still_enforces_validation() {
let (state, _root, cwd) = fixture();
let cwd_str = cwd.path().to_string_lossy().to_string();
let err = dispatch_tool(
&state,
"palace_create",
json!({ "name": "acme-app", "cwd": cwd_str }),
)
.await
.expect_err("force=false must reject a non-project slug");
let msg = format!("{err:#}");
assert!(
msg.contains("no project root") || msg.contains("does not match"),
"expected a validation error, got: {msg}"
);
}
#[tokio::test]
async fn force_flag_rejects_unsafe_slugs() {
let (state, _root, cwd) = fixture();
let cwd_str = cwd.path().to_string_lossy().to_string();
let long_slug = "a".repeat(64);
let unsafe_slugs: &[&str] = &[
"../traversal", "has spaces", "UPPERCASE", "has_underscore", "-starts-with-dash", "", &long_slug, ];
for slug in unsafe_slugs {
let result = dispatch_tool(
&state,
"palace_create",
json!({ "name": slug, "force": true, "cwd": cwd_str }),
)
.await;
assert!(
result.is_err(),
"slug {slug:?} should be rejected by format validation even with force=true; got {result:?}"
);
let err = result.unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("slug") || msg.contains("character") || msg.contains("match"),
"error should mention slug format, got: {msg}"
);
}
}