feat(privacy): contract \$HOME to ~ in user-visible display paths
Anywhere the TUI, doctor stdout, setup stdout, or onboarding shows a
file path, it used to print the absolute form (e.g. /Users/<name>/...).
On macOS/Linux the home-directory segment reveals the OS account name,
which is often the same as a public handle — undesirable for users who
share screenshots, screencasts, or paste doctor output into a public
help request.
Adds `crate::utils::display_path` that contracts a leading $HOME to `~`
and falls through unchanged otherwise. Used at every viewer-visible site:
doctor: workspace, config.toml, MCP config, all skills dirs,
selected skills dir, tools dir, plugins dir
setup: workspace, skills/tools/plugins paths and status output
TUI: context inspector header, trust-directory onboarding,
shell-job cwd (sidebar + detail pager), subagent task header
Persisted state, audit log, session checkpoints, and LLM-bound system
prompts intentionally keep the absolute path — those need full fidelity
to resolve correctly across processes and the LLM provider sees
absolute paths anyway by virtue of the workspace summary.
`display_path` has 4 tests covering: home contraction, bare-`~` for
home itself, untouched-when-unrelated, and a username-prefix regression
guard (so `/Users/alice2/...` doesn't get rewritten when $HOME is
`/Users/alice`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+30
-24
@@ -979,7 +979,7 @@ fn run_setup(config: &Config, workspace: &Path, args: SetupArgs) -> Result<()> {
|
|||||||
"DeepSeek Setup".truecolor(aqua_r, aqua_g, aqua_b).bold()
|
"DeepSeek Setup".truecolor(aqua_r, aqua_g, aqua_b).bold()
|
||||||
);
|
);
|
||||||
println!("{}", "==============".truecolor(sky_r, sky_g, sky_b));
|
println!("{}", "==============".truecolor(sky_r, sky_g, sky_b));
|
||||||
println!("Workspace: {}", workspace.display());
|
println!("Workspace: {}", crate::utils::display_path(workspace));
|
||||||
|
|
||||||
if run_mcp {
|
if run_mcp {
|
||||||
let mcp_path = config.mcp_config_path();
|
let mcp_path = config.mcp_config_path();
|
||||||
@@ -1022,10 +1022,13 @@ fn run_setup(config: &Config, workspace: &Path, args: SetupArgs) -> Result<()> {
|
|||||||
if args.local {
|
if args.local {
|
||||||
println!(
|
println!(
|
||||||
" Local skills dir enabled for this workspace: {}",
|
" Local skills dir enabled for this workspace: {}",
|
||||||
skills_dir.display()
|
crate::utils::display_path(&skills_dir)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(" Skills dir: {}", skills_dir.display());
|
println!(
|
||||||
|
" Skills dir: {}",
|
||||||
|
crate::utils::display_path(&skills_dir)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
println!(" Next: run the TUI and use `/skills` then `/skill getting-started`.");
|
println!(" Next: run the TUI and use `/skills` then `/skill getting-started`.");
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1038,7 @@ fn run_setup(config: &Config, workspace: &Path, args: SetupArgs) -> Result<()> {
|
|||||||
let (dir, readme_status, example_status) = init_tools_dir(&tools_dir, args.force)?;
|
let (dir, readme_status, example_status) = init_tools_dir(&tools_dir, args.force)?;
|
||||||
report_write_status("Tools README", &dir.join("README.md"), readme_status);
|
report_write_status("Tools README", &dir.join("README.md"), readme_status);
|
||||||
report_write_status("Example tool", &dir.join("example.sh"), example_status);
|
report_write_status("Example tool", &dir.join("example.sh"), example_status);
|
||||||
println!(" Tools dir: {}", dir.display());
|
println!(" Tools dir: {}", crate::utils::display_path(&dir));
|
||||||
println!(" Next: drop scripts here; surface them via skills/MCP when ready.");
|
println!(" Next: drop scripts here; surface them via skills/MCP when ready.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,7 +1048,10 @@ fn run_setup(config: &Config, workspace: &Path, args: SetupArgs) -> Result<()> {
|
|||||||
init_plugins_dir(&plugins_dir, args.force)?;
|
init_plugins_dir(&plugins_dir, args.force)?;
|
||||||
report_write_status("Plugins README", &readme_path, readme_status);
|
report_write_status("Plugins README", &readme_path, readme_status);
|
||||||
report_write_status("Example plugin", &example_path, example_status);
|
report_write_status("Example plugin", &example_path, example_status);
|
||||||
println!(" Plugins dir: {}", plugins_dir.display());
|
println!(
|
||||||
|
" Plugins dir: {}",
|
||||||
|
crate::utils::display_path(&plugins_dir)
|
||||||
|
);
|
||||||
println!(" Next: copy the example dir, edit PLUGIN.md, wire via skill/MCP.");
|
println!(" Next: copy the example dir, edit PLUGIN.md, wire via skill/MCP.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1200,7 +1206,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
|
|||||||
println!(
|
println!(
|
||||||
" · skills: {} at {}",
|
" · skills: {} at {}",
|
||||||
skills_count_for(&skills_dir),
|
skills_count_for(&skills_dir),
|
||||||
skills_dir.display()
|
crate::utils::display_path(&skills_dir)
|
||||||
);
|
);
|
||||||
|
|
||||||
let tools_dir = default_tools_dir();
|
let tools_dir = default_tools_dir();
|
||||||
@@ -1216,7 +1222,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
},
|
},
|
||||||
tools_dir.display()
|
crate::utils::display_path(&tools_dir)
|
||||||
);
|
);
|
||||||
|
|
||||||
let plugins_dir = default_plugins_dir();
|
let plugins_dir = default_plugins_dir();
|
||||||
@@ -1232,7 +1238,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
},
|
},
|
||||||
plugins_dir.display()
|
crate::utils::display_path(&plugins_dir)
|
||||||
);
|
);
|
||||||
|
|
||||||
let sandbox = crate::sandbox::get_platform_sandbox();
|
let sandbox = crate::sandbox::get_platform_sandbox();
|
||||||
@@ -1348,16 +1354,16 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} config.toml found at {}",
|
" {} config.toml found at {}",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
config_path.display()
|
crate::utils::display_path(&config_path)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" {} config.toml not found at {} (using defaults/env)",
|
" {} config.toml not found at {} (using defaults/env)",
|
||||||
"!".truecolor(sky_r, sky_g, sky_b),
|
"!".truecolor(sky_r, sky_g, sky_b),
|
||||||
config_path.display()
|
crate::utils::display_path(&config_path)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
println!(" workspace: {}", workspace.display());
|
println!(" workspace: {}", crate::utils::display_path(workspace));
|
||||||
|
|
||||||
// Check API keys
|
// Check API keys
|
||||||
println!();
|
println!();
|
||||||
@@ -1477,7 +1483,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} MCP config found at {}",
|
" {} MCP config found at {}",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
mcp_config_path.display()
|
crate::utils::display_path(&mcp_config_path)
|
||||||
);
|
);
|
||||||
match load_mcp_config(&mcp_config_path) {
|
match load_mcp_config(&mcp_config_path) {
|
||||||
Ok(cfg) if cfg.servers.is_empty() => {
|
Ok(cfg) if cfg.servers.is_empty() => {
|
||||||
@@ -1532,7 +1538,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} MCP config not found at {}",
|
" {} MCP config not found at {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
mcp_config_path.display()
|
crate::utils::display_path(&mcp_config_path)
|
||||||
);
|
);
|
||||||
println!(" Run `deepseek mcp init` or `deepseek setup --mcp`.");
|
println!(" Run `deepseek mcp init` or `deepseek setup --mcp`.");
|
||||||
}
|
}
|
||||||
@@ -1561,14 +1567,14 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} local skills dir found at {} ({} items)",
|
" {} local skills dir found at {} ({} items)",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
local_skills_dir.display(),
|
crate::utils::display_path(&local_skills_dir),
|
||||||
describe_dir(&local_skills_dir)
|
describe_dir(&local_skills_dir)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" {} local skills dir not found at {}",
|
" {} local skills dir not found at {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
local_skills_dir.display()
|
crate::utils::display_path(&local_skills_dir)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1576,14 +1582,14 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} .agents skills dir found at {} ({} items)",
|
" {} .agents skills dir found at {} ({} items)",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
agents_skills_dir.display(),
|
crate::utils::display_path(&agents_skills_dir),
|
||||||
describe_dir(&agents_skills_dir)
|
describe_dir(&agents_skills_dir)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" {} .agents skills dir not found at {}",
|
" {} .agents skills dir not found at {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
agents_skills_dir.display()
|
crate::utils::display_path(&agents_skills_dir)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1591,21 +1597,21 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} global skills dir found at {} ({} items)",
|
" {} global skills dir found at {} ({} items)",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
global_skills_dir.display(),
|
crate::utils::display_path(&global_skills_dir),
|
||||||
describe_dir(&global_skills_dir)
|
describe_dir(&global_skills_dir)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" {} global skills dir not found at {}",
|
" {} global skills dir not found at {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
global_skills_dir.display()
|
crate::utils::display_path(&global_skills_dir)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
" {} selected skills dir: {}",
|
" {} selected skills dir: {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
selected_skills_dir.display()
|
crate::utils::display_path(&selected_skills_dir)
|
||||||
);
|
);
|
||||||
if !agents_skills_dir.exists() && !local_skills_dir.exists() && !global_skills_dir.exists() {
|
if !agents_skills_dir.exists() && !local_skills_dir.exists() && !global_skills_dir.exists() {
|
||||||
println!(" Run `deepseek setup --skills` (or add --local for ./skills).");
|
println!(" Run `deepseek setup --skills` (or add --local for ./skills).");
|
||||||
@@ -1620,14 +1626,14 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} tools dir found at {} ({} items)",
|
" {} tools dir found at {} ({} items)",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
tools_dir.display(),
|
crate::utils::display_path(&tools_dir),
|
||||||
count
|
count
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" {} tools dir not found at {}",
|
" {} tools dir not found at {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
tools_dir.display()
|
crate::utils::display_path(&tools_dir)
|
||||||
);
|
);
|
||||||
println!(" Run `deepseek-tui setup --tools` to scaffold a starter dir.");
|
println!(" Run `deepseek-tui setup --tools` to scaffold a starter dir.");
|
||||||
}
|
}
|
||||||
@@ -1641,14 +1647,14 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
|||||||
println!(
|
println!(
|
||||||
" {} plugins dir found at {} ({} items)",
|
" {} plugins dir found at {} ({} items)",
|
||||||
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
"✓".truecolor(aqua_r, aqua_g, aqua_b),
|
||||||
plugins_dir.display(),
|
crate::utils::display_path(&plugins_dir),
|
||||||
count
|
count
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" {} plugins dir not found at {}",
|
" {} plugins dir not found at {}",
|
||||||
"·".dimmed(),
|
"·".dimmed(),
|
||||||
plugins_dir.display()
|
crate::utils::display_path(&plugins_dir)
|
||||||
);
|
);
|
||||||
println!(" Run `deepseek-tui setup --plugins` to scaffold a starter dir.");
|
println!(" Run `deepseek-tui setup --plugins` to scaffold a starter dir.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ pub fn build_context_inspector_text(app: &App) -> String {
|
|||||||
let _ = writeln!(out, "Session Context");
|
let _ = writeln!(out, "Session Context");
|
||||||
let _ = writeln!(out, "---------------");
|
let _ = writeln!(out, "---------------");
|
||||||
let _ = writeln!(out, "Model: {}", app.model);
|
let _ = writeln!(out, "Model: {}", app.model);
|
||||||
let _ = writeln!(out, "Workspace: {}", app.workspace.display());
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"Workspace: {}",
|
||||||
|
crate::utils::display_path(&app.workspace)
|
||||||
|
);
|
||||||
if let Some(session_id) = app.current_session_id.as_deref() {
|
if let Some(session_id) = app.current_session_id.as_deref() {
|
||||||
let _ = writeln!(out, "Session: {}", session_id);
|
let _ = writeln!(out, "Session: {}", session_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
|
|||||||
Style::default().fg(palette::TEXT_PRIMARY),
|
Style::default().fg(palette::TEXT_PRIMARY),
|
||||||
)));
|
)));
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
format!("Workspace: {}", app.workspace.display()),
|
format!("Workspace: {}", crate::utils::display_path(&app.workspace)),
|
||||||
Style::default().fg(palette::TEXT_MUTED),
|
Style::default().fg(palette::TEXT_MUTED),
|
||||||
)));
|
)));
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub(super) fn format_shell_job_list(jobs: &[ShellJobSnapshot]) -> String {
|
|||||||
job.exit_code,
|
job.exit_code,
|
||||||
task
|
task
|
||||||
));
|
));
|
||||||
lines.push(format!(" cwd: {}", job.cwd.display()));
|
lines.push(format!(" cwd: {}", crate::utils::display_path(&job.cwd)));
|
||||||
lines.push(format!(" cmd: {}", job.command));
|
lines.push(format!(" cmd: {}", job.command));
|
||||||
let tail = if !job.stderr_tail.trim().is_empty() {
|
let tail = if !job.stderr_tail.trim().is_empty() {
|
||||||
job.stderr_tail.trim()
|
job.stderr_tail.trim()
|
||||||
@@ -115,7 +115,7 @@ fn format_shell_job_detail(detail: &ShellJobDetail) -> String {
|
|||||||
format!("Job: {}", job.id),
|
format!("Job: {}", job.id),
|
||||||
format!("Status: {}", status_label(&job.status, job.stale)),
|
format!("Status: {}", status_label(&job.status, job.stale)),
|
||||||
format!("Command: {}", job.command),
|
format!("Command: {}", job.command),
|
||||||
format!("Cwd: {}", job.cwd.display()),
|
format!("Cwd: {}", crate::utils::display_path(&job.cwd)),
|
||||||
format!("Elapsed: {}", format_elapsed(job.elapsed_ms)),
|
format!("Elapsed: {}", format_elapsed(job.elapsed_ms)),
|
||||||
format!("Exit Code: {:?}", job.exit_code),
|
format!("Exit Code: {:?}", job.exit_code),
|
||||||
format!("Stdin Available: {}", job.stdin_available),
|
format!("Stdin Available: {}", job.stdin_available),
|
||||||
|
|||||||
@@ -489,7 +489,10 @@ fn format_task_detail(task: &TaskRecord) -> String {
|
|||||||
lines.push(format!("Status: {}", task_status_label(task.status)));
|
lines.push(format!("Status: {}", task_status_label(task.status)));
|
||||||
lines.push(format!("Mode: {}", task.mode));
|
lines.push(format!("Mode: {}", task.mode));
|
||||||
lines.push(format!("Model: {}", task.model));
|
lines.push(format!("Model: {}", task.model));
|
||||||
lines.push(format!("Workspace: {}", task.workspace.display()));
|
lines.push(format!(
|
||||||
|
"Workspace: {}",
|
||||||
|
crate::utils::display_path(&task.workspace)
|
||||||
|
));
|
||||||
if let Some(thread_id) = task.thread_id.as_ref() {
|
if let Some(thread_id) = task.thread_id.as_ref() {
|
||||||
lines.push(format!("Runtime Thread: {thread_id}"));
|
lines.push(format!("Runtime Thread: {thread_id}"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,33 @@ pub fn url_encode(input: &str) -> String {
|
|||||||
encoded
|
encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render a path for **user-facing display** with the home directory
|
||||||
|
/// contracted to `~`. Use this in the TUI, doctor/setup stdout, and any
|
||||||
|
/// other place a viewer might see the output (screenshot, video,
|
||||||
|
/// pasted-into-issue help). On macOS/Linux the absolute path
|
||||||
|
/// `/Users/<name>/...` or `/home/<name>/...` reveals the OS account name,
|
||||||
|
/// which is often the same as a public handle — undesirable for users
|
||||||
|
/// who share their terminal.
|
||||||
|
///
|
||||||
|
/// **Do not use** this for paths that get persisted (sessions, audit log)
|
||||||
|
/// or sent to the LLM provider — those want full fidelity so they
|
||||||
|
/// resolve correctly across processes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn display_path(path: &Path) -> String {
|
||||||
|
let Some(home) = dirs::home_dir() else {
|
||||||
|
return path.display().to_string();
|
||||||
|
};
|
||||||
|
if let Ok(rest) = path.strip_prefix(&home) {
|
||||||
|
if rest.as_os_str().is_empty() {
|
||||||
|
return "~".to_string();
|
||||||
|
}
|
||||||
|
// Render with the platform-correct separator after the tilde.
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
return format!("~{sep}{}", rest.display());
|
||||||
|
}
|
||||||
|
path.display().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Estimate the total character count across message content blocks.
|
/// Estimate the total character count across message content blocks.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn estimate_message_chars(messages: &[Message]) -> usize {
|
pub fn estimate_message_chars(messages: &[Message]) -> usize {
|
||||||
@@ -205,3 +232,74 @@ pub fn estimate_message_chars(messages: &[Message]) -> usize {
|
|||||||
}
|
}
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::display_path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Save and restore $HOME inside one test so a panic anywhere can't
|
||||||
|
/// poison sibling tests that read the env var.
|
||||||
|
fn with_home<R>(home: &str, f: impl FnOnce() -> R) -> R {
|
||||||
|
let prev = std::env::var_os("HOME");
|
||||||
|
// SAFETY: tests in this crate are run single-threaded with respect
|
||||||
|
// to env-var mutation by the integration harness, and we restore
|
||||||
|
// immediately after the closure.
|
||||||
|
unsafe { std::env::set_var("HOME", home) };
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
||||||
|
match prev {
|
||||||
|
Some(v) => unsafe { std::env::set_var("HOME", v) },
|
||||||
|
None => unsafe { std::env::remove_var("HOME") },
|
||||||
|
}
|
||||||
|
match result {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(p) => std::panic::resume_unwind(p),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_path_contracts_home_prefix() {
|
||||||
|
with_home("/Users/alice", || {
|
||||||
|
assert_eq!(
|
||||||
|
display_path(&PathBuf::from("/Users/alice/projects/foo")),
|
||||||
|
format!(
|
||||||
|
"~{}projects{}foo",
|
||||||
|
std::path::MAIN_SEPARATOR,
|
||||||
|
std::path::MAIN_SEPARATOR
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_path_returns_bare_tilde_for_home_itself() {
|
||||||
|
with_home("/Users/alice", || {
|
||||||
|
assert_eq!(display_path(&PathBuf::from("/Users/alice")), "~");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_path_leaves_unrelated_paths_alone() {
|
||||||
|
with_home("/Users/alice", || {
|
||||||
|
// Different user — must not get rewritten or share the tilde.
|
||||||
|
assert_eq!(
|
||||||
|
display_path(&PathBuf::from("/Users/bob/Code")),
|
||||||
|
"/Users/bob/Code".to_string()
|
||||||
|
);
|
||||||
|
// System path must stay absolute.
|
||||||
|
assert_eq!(display_path(&PathBuf::from("/etc/hosts")), "/etc/hosts");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_path_does_not_match_username_prefix() {
|
||||||
|
// Regression guard: a directory named like the user's home
|
||||||
|
// *prefix* but not under it must not get rewritten.
|
||||||
|
with_home("/Users/alice", || {
|
||||||
|
assert_eq!(
|
||||||
|
display_path(&PathBuf::from("/Users/alice2/work")),
|
||||||
|
"/Users/alice2/work"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user