#![allow(clippy::too_many_lines)]
use super::*;
fn test_state() -> (AppState, tempfile::TempDir) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state = AppState::new(root);
state.set_ready();
(state, tmp)
}
fn test_state_warming() -> (AppState, tempfile::TempDir) {
static SKIP_ENFORCEMENT_SET: std::sync::OnceLock<()> = std::sync::OnceLock::new();
SKIP_ENFORCEMENT_SET.get_or_init(|| unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
});
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
(AppState::new(root), tmp)
}
#[tokio::test]
async fn initialize_returns_protocol_version_and_capabilities() {
let (state, _tmp) = test_state();
let req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "0"}
}
});
let resp = handle_message(&state, req).await;
assert_eq!(resp["jsonrpc"], "2.0");
assert_eq!(resp["id"], 1);
assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
assert!(resp["result"]["capabilities"]["tools"].is_object());
assert_eq!(resp["result"]["serverInfo"]["name"], "trusty-memory");
}
#[tokio::test]
async fn initialized_notification_returns_null() {
let (state, _tmp) = test_state();
let req = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
});
let resp = handle_message(&state, req).await;
assert!(resp.is_null());
}
#[tokio::test]
async fn tools_list_returns_all_tools() {
let (state, _tmp) = test_state();
let req = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"});
let resp = handle_message(&state, req).await;
let tools = resp["result"]["tools"].as_array().expect("tools array");
assert_eq!(tools.len(), 25);
}
#[tokio::test]
async fn unknown_method_returns_error() {
let (state, _tmp) = test_state();
let req = json!({"jsonrpc": "2.0", "id": 4, "method": "wat"});
let resp = handle_message(&state, req).await;
assert_eq!(resp["error"]["code"], -32601);
}
#[tokio::test]
async fn ping_returns_empty_result() {
let (state, _tmp) = test_state();
let req = json!({"jsonrpc": "2.0", "id": 5, "method": "ping"});
let resp = handle_message(&state, req).await;
assert!(resp["result"].is_object());
}
#[tokio::test]
async fn app_state_default_constructs() {
let (s, _tmp) = test_state();
assert!(!s.version.is_empty());
assert!(s.registry.is_empty());
assert!(s.default_palace.is_none());
}
#[test]
#[cfg(unix)]
fn open_activity_log_with_fallback_returns_discard_when_unwritable() {
if unsafe { libc::geteuid() } == 0 {
eprintln!(
"skipping open_activity_log_with_fallback_returns_discard_when_unwritable: running as root"
);
return;
}
use std::os::unix::fs::PermissionsExt;
let outer = tempfile::tempdir().expect("outer tempdir");
let primary = outer.path().join("primary");
let tmpdir = outer.path().join("fake-tmp");
std::fs::create_dir(&primary).expect("create primary");
std::fs::create_dir(&tmpdir).expect("create tmpdir");
std::fs::set_permissions(&primary, std::fs::Permissions::from_mode(0o000))
.expect("chmod primary");
std::fs::set_permissions(&tmpdir, std::fs::Permissions::from_mode(0o000))
.expect("chmod tmpdir");
let prev_tmpdir = std::env::var_os("TMPDIR");
std::env::set_var("TMPDIR", &tmpdir);
let log = open_activity_log_with_fallback(&primary);
match prev_tmpdir {
Some(v) => std::env::set_var("TMPDIR", v),
None => std::env::remove_var("TMPDIR"),
}
let _ = std::fs::set_permissions(&primary, std::fs::Permissions::from_mode(0o700));
let _ = std::fs::set_permissions(&tmpdir, std::fs::Permissions::from_mode(0o700));
assert!(
log.is_discard(),
"expected ActivityLog::Discard when both data root and tempdir are unwritable"
);
let id = log
.append(
ActivitySource::Http,
None,
"drawer_added",
json!({"smoke": true}),
)
.expect("discard append must succeed");
assert_eq!(id, 0);
assert_eq!(log.count().expect("discard count"), 0);
assert!(log
.list(&ActivityFilter::default(), 10, 0)
.expect("discard list")
.is_empty());
}
#[tokio::test]
async fn default_palace_used_when_arg_omitted() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let registry = trusty_common::memory_core::PalaceRegistry::new();
let palace = trusty_common::memory_core::Palace {
id: trusty_common::memory_core::PalaceId::new("default-pal"),
name: "default-pal".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join("default-pal"),
};
registry
.create_palace(&root, palace)
.expect("create_palace");
let state = AppState::new(root).with_default_palace(Some("default-pal".to_string()));
state.set_ready();
let init = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
)
.await;
assert_eq!(
init["result"]["serverInfo"]["default_palace"], "default-pal",
"initialize must echo default_palace in serverInfo"
);
let list = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
)
.await;
let tools = list["result"]["tools"].as_array().expect("tools array");
let remember = tools
.iter()
.find(|t| t["name"] == "memory_remember")
.expect("memory_remember tool");
let required: Vec<&str> = remember["inputSchema"]["required"]
.as_array()
.expect("required array")
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
!required.contains(&"palace"),
"palace must not be required when default is configured; got {required:?}"
);
assert!(required.contains(&"text"));
let call = handle_message(
&state,
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "memory_remember",
"arguments": {"text": "default palace test memory content with several tokens"},
},
}),
)
.await;
let text = call["result"]["content"][0]["text"]
.as_str()
.unwrap_or_else(|| panic!("expected success result, got {call}"));
let parsed: Value = serde_json::from_str(text).expect("parse content json");
assert_eq!(parsed["palace"], "default-pal");
assert_eq!(parsed["status"], "stored");
assert!(parsed["drawer_id"].as_str().is_some());
}
#[tokio::test]
async fn missing_palace_without_default_errors() {
let (state, _tmp) = test_state();
let resp = handle_message(
&state,
json!({
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "memory_recall",
"arguments": {"query": "anything"},
},
}),
)
.await;
assert_eq!(resp["error"]["code"], -32603);
let msg = resp["error"]["message"].as_str().unwrap_or("");
assert!(
msg.contains("missing 'palace'"),
"expected helpful error, got: {msg}"
);
}
#[tokio::test]
async fn load_palaces_from_disk_rehydrates_registry() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
{
let writer = PalaceRegistry::new();
for id in ["alpha", "beta"] {
let palace = Palace {
id: PalaceId::new(id),
name: id.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join(id),
};
writer
.create_palace(&root, palace)
.expect("persist palace to disk");
}
}
std::fs::create_dir_all(root.join("not-a-palace")).expect("mkdir");
let state = AppState::new(root);
assert!(
state.registry.is_empty(),
"AppState::new must start with an empty registry"
);
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(count, 2, "both persisted palaces should be loaded");
assert_eq!(state.registry.len(), 2, "registry should hold both palaces");
let ids: Vec<String> = state.registry.list().into_iter().map(|p| p.0).collect();
assert!(ids.contains(&"alpha".to_string()));
assert!(ids.contains(&"beta".to_string()));
}
#[test]
fn resolve_palace_registry_dir_prefers_palaces_subdir() {
let tmp = tempfile::tempdir().expect("tempdir");
let data_dir = tmp.path().to_path_buf();
std::fs::create_dir_all(data_dir.join("palaces")).expect("mkdir palaces");
let resolved = resolve_palace_registry_dir(data_dir.clone());
assert_eq!(resolved, data_dir.join("palaces"));
}
#[test]
fn resolve_palace_registry_dir_falls_back_to_data_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let data_dir = tmp.path().to_path_buf();
let resolved = resolve_palace_registry_dir(data_dir.clone());
assert_eq!(resolved, data_dir);
}
#[test]
fn resolve_palace_registry_dir_relative_input_is_not_absolute() {
let relative = std::path::PathBuf::from("relative/path");
let result = resolve_palace_registry_dir(relative.clone());
assert!(
!result.is_absolute(),
"a relative input should produce a relative registry dir (caught by main.rs guard)"
);
}
#[test]
fn resolve_palace_registry_dir_root_input_stays_root() {
let root = std::path::PathBuf::from("/");
let result = resolve_palace_registry_dir(root);
assert_eq!(result, std::path::PathBuf::from("/"));
}
#[tokio::test]
async fn load_palaces_from_disk_handles_palaces_subdir() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let nested = root.join("palaces");
{
let writer = PalaceRegistry::new();
for id in ["cto", "engineering"] {
let palace = Palace {
id: PalaceId::new(id),
name: id.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: nested.join(id),
};
writer
.create_palace(&nested, palace)
.expect("persist palace under palaces/ subdir");
}
}
let registry_dir = resolve_palace_registry_dir(root);
assert_eq!(registry_dir, nested, "must resolve into palaces/ subdir");
let state = AppState::new(registry_dir);
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(count, 2, "both nested palaces should be loaded");
assert_eq!(state.registry.len(), 2);
let ids: Vec<String> = state.registry.list().into_iter().map(|p| p.0).collect();
assert!(ids.contains(&"cto".to_string()));
assert!(ids.contains(&"engineering".to_string()));
}
#[tokio::test]
async fn load_palaces_from_disk_empty_root_returns_zero() {
let (state, _tmp) = test_state();
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk on empty root");
assert_eq!(count, 0);
assert!(state.registry.is_empty());
}
#[tokio::test]
async fn palace_name_cache_populated_after_hydration() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
{
let writer = PalaceRegistry::new();
for (id, name) in [("alpha", "Alpha Project"), ("beta", "Beta Project")] {
let palace = Palace {
id: PalaceId::new(id),
name: name.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join(id),
};
writer.create_palace(&root, palace).expect("persist palace");
}
}
let state = AppState::new(root);
assert!(
state.palace_names.is_empty(),
"fresh AppState must start with an empty name cache"
);
state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(state.palace_names.len(), 2, "cache must hold both palaces");
assert_eq!(
state.palace_names.get("alpha").map(|e| e.value().clone()),
Some("Alpha Project".to_string()),
);
assert_eq!(
state.palace_names.get("beta").map(|e| e.value().clone()),
Some("Beta Project".to_string()),
);
}
#[tokio::test]
async fn palace_name_cache_updates_on_create() {
use serde_json::json;
let (state, _tmp) = test_state();
let _ = tools::dispatch_tool(&state, "palace_create", json!({"name": "gamma"}))
.await
.expect("palace_create");
assert_eq!(
state.palace_names.get("gamma").map(|e| e.value().clone()),
Some("gamma".to_string()),
"palace_create must populate the in-memory name cache so writes \
can resolve the friendly name without a disk walk"
);
}
#[tokio::test]
async fn initialize_without_default_palace_omits_field() {
let (state, _tmp) = test_state();
let init = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
)
.await;
assert!(init["result"]["serverInfo"]["default_palace"].is_null());
}
#[tokio::test]
async fn http_addr_path_uses_resolve_data_dir() {
let _guard = crate::commands::env_test_lock().lock().await;
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var(trusty_common::DATA_DIR_OVERRIDE_ENV, tmp.path());
}
let result = http_addr_path();
unsafe {
std::env::remove_var(trusty_common::DATA_DIR_OVERRIDE_ENV);
}
let p = result.expect("http_addr_path must return Some when data dir is resolvable");
assert!(
p.ends_with("trusty-memory/http_addr"),
"unexpected http_addr path: {}",
p.display()
);
}
#[cfg(feature = "axum-server")]
#[test]
fn http_addr_file_round_trip_via_helpers() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("http_addr");
let addr: SocketAddr = "127.0.0.1:7073".parse().unwrap();
write_http_addr_file(&path, &addr).unwrap();
let raw = std::fs::read_to_string(&path).unwrap();
assert_eq!(raw.trim(), "127.0.0.1:7073");
assert!(raw.ends_with('\n'));
}
#[tokio::test]
async fn bind_dynamic_port_returns_listener() {
let listener = bind_dynamic_port().await.expect("bind_dynamic_port");
let addr = listener.local_addr().expect("local_addr");
assert_eq!(addr.ip().to_string(), "127.0.0.1");
assert!(addr.port() > 0, "port must be non-zero after bind");
}
#[tokio::test]
async fn initialize_does_not_advertise_prompts_capability() {
let (state, _tmp) = test_state();
let init = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
)
.await;
assert!(
init["result"]["capabilities"]["prompts"].is_null(),
"initialize must NOT advertise the prompts capability; got {init}"
);
for method in ["prompts/list", "prompts/get"] {
let resp =
handle_message(&state, json!({"jsonrpc": "2.0", "id": 2, "method": method})).await;
assert_eq!(
resp["error"]["code"], -32601,
"{method} should return method-not-found; got {resp}"
);
}
}
#[tokio::test]
async fn app_state_starts_with_empty_bound_addr() {
let (state, _tmp) = test_state();
assert!(state.bound_addr.get().is_none());
}
#[test]
fn daemon_event_type_str_matches_sse_tag() {
let cases = [
DaemonEvent::PalaceCreated {
id: "p".into(),
name: "p".into(),
source: ActivitySource::Http,
},
DaemonEvent::DrawerAdded {
palace_id: "p".into(),
palace_name: "p".into(),
drawer_count: 1,
timestamp: chrono::Utc::now(),
content_preview: String::new(),
source: ActivitySource::Mcp,
},
DaemonEvent::DrawerDeleted {
palace_id: "p".into(),
drawer_count: 0,
source: ActivitySource::Http,
},
DaemonEvent::DreamCompleted {
palace_id: None,
merged: 0,
pruned: 0,
compacted: 0,
closets_updated: 0,
duration_ms: 0,
source: ActivitySource::Http,
},
DaemonEvent::StatusChanged {
total_drawers: 0,
total_vectors: 0,
total_kg_triples: 0,
},
DaemonEvent::HookFired {
palace_id: Some("p".into()),
palace_name: Some("p".into()),
hook_type: HookType::UserPromptSubmit,
injection_kind: InjectionKind::PromptContext,
injection_length: 12,
trigger_prompt_excerpt: "hello".into(),
timestamp: chrono::Utc::now(),
duration_ms: 5,
source: ActivitySource::Hook,
},
];
for ev in &cases {
let json = serde_json::to_value(ev).unwrap();
assert_eq!(json["type"].as_str(), Some(ev.type_str()));
}
}
#[test]
fn hook_type_serde_round_trips() {
let cases = [
(HookType::UserPromptSubmit, "\"UserPromptSubmit\""),
(HookType::SessionStart, "\"SessionStart\""),
];
for (ht, expected) in cases {
let s = serde_json::to_string(&ht).unwrap();
assert_eq!(s, expected, "{ht:?} should serialise to {expected}");
let back: HookType = serde_json::from_str(&s).unwrap();
assert_eq!(back, ht);
assert_eq!(ht.as_str(), expected.trim_matches('"'));
}
}
#[test]
fn injection_kind_serde_round_trips() {
let cases = [
(InjectionKind::PromptContext, "\"prompt-context\""),
(InjectionKind::InboxCheck, "\"inbox-check\""),
];
for (ik, expected) in cases {
let s = serde_json::to_string(&ik).unwrap();
assert_eq!(s, expected);
let back: InjectionKind = serde_json::from_str(&s).unwrap();
assert_eq!(back, ik);
assert_eq!(ik.as_str(), expected.trim_matches('"'));
}
}
#[test]
fn hook_excerpt_truncates_long_prompts() {
let long = "x".repeat(200);
let excerpt = hook_prompt_excerpt(&long);
assert!(excerpt.chars().count() <= HOOK_PROMPT_EXCERPT_CHARS);
assert!(excerpt.ends_with('…'));
assert_eq!(hook_prompt_excerpt(""), "");
}
#[test]
fn hook_excerpt_collapses_whitespace() {
let input = "hello\n\nworld\t\tfoo";
let excerpt = hook_prompt_excerpt(input);
assert_eq!(excerpt, "hello world foo");
}
#[test]
fn daemon_event_palace_id_and_source_extraction() {
let ev = DaemonEvent::DrawerAdded {
palace_id: "alpha".into(),
palace_name: "alpha".into(),
drawer_count: 1,
timestamp: chrono::Utc::now(),
content_preview: String::new(),
source: ActivitySource::Mcp,
};
assert_eq!(ev.palace_id(), Some("alpha"));
assert_eq!(ev.source(), Some(ActivitySource::Mcp));
let status = DaemonEvent::StatusChanged {
total_drawers: 1,
total_vectors: 2,
total_kg_triples: 3,
};
assert_eq!(status.palace_id(), None);
assert_eq!(status.source(), None);
let dream = DaemonEvent::DreamCompleted {
palace_id: Some("p1".into()),
merged: 0,
pruned: 0,
compacted: 0,
closets_updated: 0,
duration_ms: 10,
source: ActivitySource::Http,
};
assert_eq!(dream.palace_id(), Some("p1"));
assert_eq!(dream.source(), Some(ActivitySource::Http));
}
#[tokio::test]
async fn emit_persists_mutations_but_skips_status_changed() {
let (state, _tmp) = test_state();
state.emit(DaemonEvent::PalaceCreated {
id: "p".into(),
name: "p".into(),
source: ActivitySource::Http,
});
state.emit(DaemonEvent::StatusChanged {
total_drawers: 1,
total_vectors: 0,
total_kg_triples: 0,
});
state.emit(DaemonEvent::DrawerAdded {
palace_id: "p".into(),
palace_name: "p".into(),
drawer_count: 1,
timestamp: chrono::Utc::now(),
content_preview: "x".into(),
source: ActivitySource::Mcp,
});
state.flush_activity_writes().await;
let count = state.activity_log.count().unwrap();
assert_eq!(count, 2, "only PalaceCreated + DrawerAdded must persist");
}
#[tokio::test]
async fn bm25_client_disabled_by_default() {
let _guard = crate::commands::env_test_lock().lock().await;
let prev = std::env::var("TRUSTY_BM25_DAEMON").ok();
unsafe {
std::env::remove_var("TRUSTY_BM25_DAEMON");
}
let (state, _tmp) = test_state();
let state = state.with_bm25_client_from_env();
assert!(
state.bm25_client.is_none(),
"bm25_client must be None when TRUSTY_BM25_DAEMON is unset"
);
assert!(
state.bm25_supervisor.is_none(),
"bm25_supervisor must be None when TRUSTY_BM25_DAEMON is unset"
);
if let Some(v) = prev {
unsafe {
std::env::set_var("TRUSTY_BM25_DAEMON", v);
}
}
}
#[tokio::test]
async fn bm25_client_enabled_when_env_set() {
let _guard = crate::commands::env_test_lock().lock().await;
let prev = std::env::var("TRUSTY_BM25_DAEMON").ok();
unsafe {
std::env::set_var("TRUSTY_BM25_DAEMON", "1");
}
let (state, _tmp) = test_state();
let state = state.with_bm25_client_from_env();
assert!(
state.bm25_client.is_some(),
"bm25_client must be Some when TRUSTY_BM25_DAEMON=1"
);
assert!(
state.bm25_supervisor.is_some(),
"bm25_supervisor must be Some when TRUSTY_BM25_DAEMON=1"
);
match prev {
Some(v) => unsafe { std::env::set_var("TRUSTY_BM25_DAEMON", v) },
None => unsafe { std::env::remove_var("TRUSTY_BM25_DAEMON") },
}
}
#[tokio::test]
async fn daemon_readiness_transitions_warming_to_ready() {
let (state, _tmp) = test_state_warming();
assert_eq!(
state.readiness(),
DaemonReadiness::Warming,
"daemon must start in Warming state"
);
state.set_ready();
assert_eq!(
state.readiness(),
DaemonReadiness::Ready,
"daemon must be Ready after set_ready()"
);
}
#[tokio::test]
async fn readiness_check_ok_when_ready_err_when_warming() {
let (state, _tmp) = test_state_warming();
let err = state
.readiness_check()
.expect_err("readiness_check must fail when Warming");
let msg = err.to_string();
assert!(
msg.contains("warming up"),
"error must mention 'warming up'; got: {msg}"
);
state.set_ready();
state
.readiness_check()
.expect("readiness_check must succeed when Ready");
}
#[test]
fn daemon_readiness_from_u8() {
assert_eq!(DaemonReadiness::from_u8(0), DaemonReadiness::Warming);
assert_eq!(DaemonReadiness::from_u8(1), DaemonReadiness::Ready);
assert_eq!(DaemonReadiness::from_u8(255), DaemonReadiness::Ready);
}
#[tokio::test]
async fn open_palace_lazy_reopens_hydration_skipped_palace() {
let (state, _tmp) = test_state();
let pid = trusty_common::memory_core::palace::PalaceId::new("hydration-skip");
let palace = trusty_common::memory_core::Palace {
id: pid.clone(),
name: "hydration-skip".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("hydration-skip"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
state.registry.remove(&pid);
assert!(
state.registry.get(&pid).is_none(),
"palace must appear absent (simulating hydration skip) before the lazy-reopen"
);
let handle = state
.registry
.open_palace(&state.data_root, &pid)
.expect("open_palace must lazily reopen a hydration-skipped palace");
assert_eq!(handle.id.as_str(), "hydration-skip");
}
#[cfg(feature = "axum-server")]
#[test]
fn dotfile_http_addr_path_uses_home_dir() {
if let Some(p) = dotfile_http_addr_path() {
assert!(
p.ends_with(".trusty-memory/http_addr"),
"dotfile path must end in .trusty-memory/http_addr; got {}",
p.display()
);
}
}
#[cfg(feature = "axum-server")]
#[test]
fn dotfile_http_addr_write_read_round_trip() {
let home = tempfile::tempdir().unwrap();
let dotfile_dir = home.path().join(".trusty-memory");
let path = dotfile_dir.join("http_addr");
let addr: SocketAddr = "127.0.0.1:7099".parse().unwrap();
write_http_addr_file(&path, &addr).expect("write_http_addr_file to dotfile path");
let raw = std::fs::read_to_string(&path).unwrap();
assert_eq!(
raw.trim(),
"127.0.0.1:7099",
"dotfile round-trip content mismatch"
);
assert!(raw.ends_with('\n'), "dotfile must end with a newline");
}