use super::*;
use serde_json::json;
#[test]
fn patch_one_creates_missing_file() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let outcome = patch_one(&path, &entry).expect("patch ok");
assert!(outcome.mcp_wrote, "first patch writes the MCP entry");
assert!(outcome.hook_wrote, "first patch installs the hook");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let server = &value["mcpServers"][MCP_SERVER_KEY];
assert_eq!(server["command"], "trusty-memory");
assert_eq!(server["args"][0], "serve");
assert_eq!(server["args"][1], "--stdio");
let hook_entries = value["hooks"][HOOK_EVENT].as_array().unwrap();
assert_eq!(hook_entries.len(), 1, "exactly one matcher block");
let inner = hook_entries[0]["hooks"].as_array().unwrap();
assert_eq!(inner[0]["command"], HOOK_COMMAND);
assert_eq!(inner[0]["type"], "command");
assert_eq!(inner[0]["timeout"], HOOK_TIMEOUT_MS);
let ss_entries = value["hooks"][SESSION_START_HOOK_EVENT]
.as_array()
.expect("SessionStart hooks installed");
assert_eq!(
ss_entries.len(),
1,
"exactly one SessionStart matcher block"
);
let ss_inner = ss_entries[0]["hooks"].as_array().unwrap();
assert_eq!(ss_inner[0]["command"], INBOX_CHECK_HOOK_COMMAND);
assert_eq!(ss_inner[0]["type"], "command");
assert_eq!(ss_inner[0]["timeout"], HOOK_TIMEOUT_MS);
}
#[test]
fn patch_one_installs_session_start_hook_when_upgrading() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let seed = json!({
"mcpServers": {
MCP_SERVER_KEY: { "command": "trusty-memory", "args": ["serve", "--stdio"] }
},
"hooks": {
HOOK_EVENT: [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": HOOK_COMMAND,
"timeout": HOOK_TIMEOUT_MS,
}]
}]
}
});
std::fs::write(&path, serde_json::to_string_pretty(&seed).unwrap()).unwrap();
let outcome = patch_one(&path, &entry).expect("patch ok");
assert!(!outcome.mcp_wrote);
assert!(outcome.hook_wrote, "SessionStart hook must be added");
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let ups = value["hooks"][HOOK_EVENT].as_array().unwrap();
assert_eq!(ups.len(), 1);
assert_eq!(ups[0]["hooks"][0]["command"], HOOK_COMMAND);
let ss = value["hooks"][SESSION_START_HOOK_EVENT].as_array().unwrap();
assert_eq!(ss.len(), 1);
assert_eq!(ss[0]["hooks"][0]["command"], INBOX_CHECK_HOOK_COMMAND);
}
#[test]
fn patch_one_adds_session_start_when_legacy_user_prompt_submit_has_different_shape() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let seed = json!({
"mcpServers": {
MCP_SERVER_KEY: { "command": "trusty-memory", "args": ["serve", "--stdio"] }
},
"hooks": {
HOOK_EVENT: [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": HOOK_COMMAND,
}]
}]
}
});
std::fs::write(&path, serde_json::to_string_pretty(&seed).unwrap()).unwrap();
let outcome = patch_one(&path, &entry).expect("patch ok");
assert!(!outcome.mcp_wrote, "MCP entry already canonical");
assert!(
outcome.hook_wrote,
"SessionStart (and a fresh UserPromptSubmit shape) must be added"
);
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let ss = value["hooks"][SESSION_START_HOOK_EVENT]
.as_array()
.expect("SessionStart array exists");
assert_eq!(ss.len(), 1, "exactly one SessionStart matcher block");
assert_eq!(ss[0]["hooks"][0]["command"], INBOX_CHECK_HOOK_COMMAND);
assert_eq!(ss[0]["hooks"][0]["timeout"], HOOK_TIMEOUT_MS);
let ups = value["hooks"][HOOK_EVENT].as_array().unwrap();
assert!(
ups.iter().any(|e| e["hooks"][0].get("timeout").is_none()),
"legacy timeout-less entry must be preserved"
);
assert!(
ups.iter()
.any(|e| e["hooks"][0]["timeout"] == HOOK_TIMEOUT_MS),
"canonical timeout=3000 entry must be appended"
);
}
#[test]
fn patch_one_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let first = patch_one(&path, &entry).unwrap();
assert!(first.mcp_wrote && first.hook_wrote, "first patch writes");
let after_first = std::fs::read_to_string(&path).unwrap();
let second = patch_one(&path, &entry).unwrap();
assert!(
!second.mcp_wrote && !second.hook_wrote,
"second patch is no-op"
);
let after_second = std::fs::read_to_string(&path).unwrap();
assert_eq!(after_first, after_second, "file must not change on no-op");
}
#[test]
fn patch_one_preserves_unrelated_keys() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let seed = json!({
"theme": "dark",
"mcpServers": {
"some-other-server": { "command": "x", "args": [] }
},
"hooks": {
"Stop": [{ "matcher": "*", "hooks": [
{ "type": "command", "command": "echo bye" }
] }]
}
});
std::fs::write(&path, serde_json::to_string_pretty(&seed).unwrap()).unwrap();
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let outcome = patch_one(&path, &entry).expect("patch ok");
assert!(outcome.mcp_wrote);
assert!(outcome.hook_wrote);
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(value["theme"], "dark", "unrelated top-level key dropped");
let servers = value["mcpServers"].as_object().unwrap();
assert!(servers.contains_key("some-other-server"));
assert!(servers.contains_key(MCP_SERVER_KEY));
let stop = value["hooks"]["Stop"].as_array().unwrap();
assert_eq!(stop.len(), 1);
assert_eq!(stop[0]["hooks"][0]["command"], "echo bye");
let ups = value["hooks"][HOOK_EVENT].as_array().unwrap();
assert_eq!(ups[0]["hooks"][0]["command"], HOOK_COMMAND);
}
#[test]
fn patch_one_installs_hook_when_mcp_already_present() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let seed = json!({
"mcpServers": {
MCP_SERVER_KEY: { "command": "trusty-memory", "args": ["serve", "--stdio"] }
}
});
std::fs::write(&path, serde_json::to_string_pretty(&seed).unwrap()).unwrap();
let outcome = patch_one(&path, &entry).expect("patch ok");
assert!(!outcome.mcp_wrote, "MCP entry already present");
assert!(outcome.hook_wrote, "hook freshly installed");
}
#[test]
fn patch_one_upgrades_serve_entry_to_serve_stdio() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("settings.json");
let seed = json!({
"mcpServers": {
MCP_SERVER_KEY: { "command": "trusty-memory", "args": ["serve"] }
}
});
std::fs::write(&path, serde_json::to_string_pretty(&seed).unwrap()).unwrap();
let entry = mcp_server_entry(MCP_SERVER_KEY, &["serve", "--stdio"]);
let outcome = patch_one(&path, &entry).expect("patch ok");
assert!(
outcome.mcp_wrote,
"old args:[\"serve\"] entry must be rewritten to args:[\"serve\",\"--stdio\"]"
);
let value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let args = value["mcpServers"][MCP_SERVER_KEY]["args"]
.as_array()
.expect("args array present after upgrade");
assert_eq!(args[0], "serve", "first arg is 'serve'");
assert_eq!(args[1], "--stdio", "second arg is '--stdio' after upgrade");
}