fix: resolve the weird path issues from windows <-> WSL (#106)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m18s
CI / Lint & Test (push) Has been cancelled
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #106
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #106.
This commit is contained in:
2026-02-05 19:21:36 -08:00
committed by Naomi Carrigan
parent a72f2afaff
commit 479652b69e
4 changed files with 648 additions and 71 deletions
+536 -62
View File
@@ -1,5 +1,5 @@
use std::path::PathBuf;
use tauri::{AppHandle, State};
use tauri::{AppHandle, Manager, State};
use tauri_plugin_http::reqwest;
use tauri_plugin_store::StoreExt;
@@ -11,6 +11,43 @@ use crate::temp_manager::SharedTempFileManager;
const CONFIG_STORE_KEY: &str = "config";
/// Convert a Windows path to a WSL path
/// Example: C:\Users\accou\Documents\item.txt -> /mnt/c/Users/accou/Documents/item.txt
fn windows_path_to_wsl(windows_path: &str) -> Option<String> {
// Check if it's a Windows path (has drive letter like C:\)
if windows_path.len() >= 3 && windows_path.chars().nth(1) == Some(':') {
let drive_letter = windows_path.chars().next()?.to_lowercase().to_string();
let path_without_drive = &windows_path[2..]; // Remove "C:"
// Replace backslashes with forward slashes and convert to WSL mount point
let wsl_path = path_without_drive.replace('\\', "/");
Some(format!("/mnt/{}{}", drive_letter, wsl_path))
} else {
None
}
}
/// Convert a WSL path to a Windows path
/// Example: /mnt/c/Users/accou/Documents/item.txt -> C:\Users\accou\Documents\item.txt
#[allow(dead_code)]
fn wsl_path_to_windows(wsl_path: &str) -> Option<String> {
// Check if it's a WSL mount point path
if wsl_path.starts_with("/mnt/") && wsl_path.len() > 6 {
let rest = &wsl_path[5..]; // Remove "/mnt/"
if let Some(drive_letter) = rest.chars().next() {
let path_after_drive = &rest[1..]; // Remove drive letter
// Convert to Windows path with backslashes
let windows_path = path_after_drive.replace('/', "\\");
Some(format!("{}:{}", drive_letter.to_uppercase(), windows_path))
} else {
None
}
} else {
None
}
}
#[tauri::command]
pub async fn start_claude(
bridge_manager: State<'_, SharedBridgeManager>,
@@ -69,7 +106,10 @@ pub async fn get_working_directory(
#[tauri::command]
pub async fn select_wsl_directory() -> Result<String, String> {
Ok("/home".to_string())
// Return the user's home directory cross-platform
dirs::home_dir()
.ok_or_else(|| "Could not determine home directory".to_string())
.map(|p| p.to_string_lossy().to_string())
}
#[tauri::command]
@@ -120,53 +160,93 @@ pub async fn validate_directory(
path: String,
current_dir: Option<String>,
) -> Result<String, String> {
use std::path::Path;
use std::path::{Path, PathBuf};
let path = Path::new(&path);
// Detect if we're dealing with a WSL path (starts with / on Windows, or current_dir is a WSL path)
let is_wsl_path = cfg!(windows) && (path.starts_with('/') || current_dir.as_ref().is_some_and(|p| p.starts_with('/')));
// Expand ~ to home directory
let expanded_path = if path.starts_with("~") {
if let Some(home) = std::env::var_os("HOME") {
let home_path = Path::new(&home);
if path == Path::new("~") {
home_path.to_path_buf()
if is_wsl_path {
// WSL path - handle as Unix-style path without filesystem validation
// since the Windows binary can't validate WSL filesystem paths
let resolved = if path.starts_with('/') {
// Absolute WSL path - use as-is
path
} else if let Some(ref cwd) = current_dir {
// Relative path - resolve manually using Unix path logic
if path == "." {
cwd.clone()
} else if path == ".." {
// Go up one directory
cwd.rsplit_once('/').map(|x| x.0).unwrap_or("/").to_string()
} else if path.starts_with("../") {
// Handle ../ prefix
let parent = cwd.rsplit_once('/').map(|x| x.0).unwrap_or("/");
let remainder = path.strip_prefix("../").unwrap();
if remainder.is_empty() {
parent.to_string()
} else {
format!("{}/{}", parent, remainder)
}
} else if path.starts_with("./") {
// Handle ./ prefix
format!("{}/{}", cwd, path.strip_prefix("./").unwrap())
} else {
home_path.join(path.strip_prefix("~").unwrap())
// Regular relative path
format!("{}/{}", cwd, path)
}
} else {
return Err("Could not determine home directory".to_string());
}
} else if path.is_relative() {
// Handle relative paths (., .., or any relative path) by resolving against current_dir
if let Some(ref cwd) = current_dir {
Path::new(cwd).join(path)
return Err("Cannot resolve relative WSL path without current directory".to_string());
};
// Normalize the path (remove duplicate slashes, etc.)
let normalized = resolved.split('/').filter(|s| !s.is_empty()).collect::<Vec<_>>().join("/");
Ok(if normalized.is_empty() { "/".to_string() } else { format!("/{}", normalized) })
} else {
// Native path (Windows on Windows, Unix on Unix) - validate normally
let path = Path::new(&path);
let expanded_path = if path.starts_with("~") {
if let Some(home) = dirs::home_dir() {
if path == Path::new("~") {
home
} else {
home.join(path.strip_prefix("~").unwrap())
}
} else {
return Err("Could not determine home directory".to_string());
}
} else if path.is_relative() {
if let Some(ref cwd) = current_dir {
let cwd_path = PathBuf::from(cwd);
cwd_path.join(path)
} else {
path.to_path_buf()
}
} else {
path.to_path_buf()
};
// Check if the path exists and is a directory
if !expanded_path.exists() {
return Err(format!(
"Directory does not exist: {}",
expanded_path.display()
));
}
} else {
path.to_path_buf()
};
// Check if the path exists and is a directory
if !expanded_path.exists() {
return Err(format!(
"Directory does not exist: {}",
expanded_path.display()
));
if !expanded_path.is_dir() {
return Err(format!(
"Path is not a directory: {}",
expanded_path.display()
));
}
// Return the canonicalized (absolute) path
expanded_path
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| format!("Failed to resolve path: {}", e))
}
if !expanded_path.is_dir() {
return Err(format!(
"Path is not a directory: {}",
expanded_path.display()
));
}
// Return the canonicalized (absolute) path
expanded_path
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| format!("Failed to resolve path: {}", e))
}
#[tauri::command]
@@ -202,21 +282,22 @@ pub async fn answer_question(
#[tauri::command]
pub async fn list_skills() -> Result<Vec<String>, String> {
// On Windows, we need to use WSL to access the skills directory
// since skills are stored in the WSL home directory
if cfg!(windows) {
return list_skills_via_wsl().await;
}
// On Unix systems, use the native filesystem
use std::fs;
use std::path::Path;
// Get the home directory
let home =
std::env::var_os("HOME").ok_or_else(|| "Could not determine home directory".to_string())?;
let home = dirs::home_dir().ok_or_else(|| "Could not determine home directory".to_string())?;
let skills_dir = home.join(".claude").join("skills");
let skills_dir = Path::new(&home).join(".claude").join("skills");
// If the skills directory doesn't exist, return empty list
if !skills_dir.exists() {
return Ok(Vec::new());
}
// Read the directory and collect skill names
let mut skills = Vec::new();
let entries =
fs::read_dir(&skills_dir).map_err(|e| format!("Failed to read skills directory: {}", e))?;
@@ -225,7 +306,6 @@ pub async fn list_skills() -> Result<Vec<String>, String> {
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
let path = entry.path();
// Only include directories that contain a SKILL.md file
if path.is_dir() {
let skill_file = path.join("SKILL.md");
if skill_file.exists() {
@@ -236,9 +316,42 @@ pub async fn list_skills() -> Result<Vec<String>, String> {
}
}
// Sort alphabetically
skills.sort();
Ok(skills)
}
/// List skills by executing commands through WSL (for Windows)
#[allow(dead_code)]
async fn list_skills_via_wsl() -> Result<Vec<String>, String> {
use std::process::Command;
// Use WSL to list directories in ~/.claude/skills that contain SKILL.md
let output = Command::new("wsl")
.args([
"-e",
"sh",
"-c",
"if [ -d ~/.claude/skills ]; then for d in ~/.claude/skills/*/; do [ -f \"${d}SKILL.md\" ] && basename \"$d\"; done; fi",
])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not found") || stderr.contains("No such file") {
return Ok(Vec::new());
}
return Err(format!("WSL command failed: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut skills: Vec<String> = stdout
.lines()
.filter(|line| !line.is_empty())
.map(|line| line.to_string())
.collect();
skills.sort();
Ok(skills)
}
@@ -335,8 +448,18 @@ pub async fn save_temp_file(
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let path_string = path.to_string_lossy().to_string();
// On Windows, convert the path to WSL format if needed
// so Claude Code (running in WSL) can access it via /mnt/c/...
let final_path = if cfg!(windows) {
windows_path_to_wsl(&path_string).unwrap_or(path_string)
} else {
path_string
};
Ok(SavedFileInfo {
path: path.to_string_lossy().to_string(),
path: final_path,
filename,
})
}
@@ -405,42 +528,142 @@ pub struct FileEntry {
}
#[tauri::command]
pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
pub async fn list_directory(app: AppHandle, path: String) -> Result<Vec<FileEntry>, String> {
// Set up logging
let log_path = if let Ok(app_data_dir) = app.path().app_data_dir() {
let _ = std::fs::create_dir_all(&app_data_dir);
app_data_dir.join("hikari_editor_debug.log")
} else {
PathBuf::from("hikari_editor_debug.log")
};
let mut log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.ok();
let mut log = |msg: String| {
if let Some(ref mut file) = log_file {
use std::io::Write;
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let _ = writeln!(file, "[{}] {}", timestamp, msg);
}
};
log(format!("list_directory called with path: {}", path));
log(format!("cfg!(windows) = {}", cfg!(windows)));
log(format!("path.starts_with('/') = {}", path.starts_with('/')));
// On Windows with a WSL path (starts with /), use WSL to list the directory
if cfg!(windows) && path.starts_with('/') {
log("Using WSL path".to_string());
return list_directory_via_wsl(&path).await;
}
log("Using native filesystem access".to_string());
// Native filesystem access
use std::fs;
use std::path::Path;
let dir_path = Path::new(&path);
if !dir_path.exists() {
return Err(format!("Directory does not exist: {}", path));
let err = format!("Directory does not exist: {}", path);
log(format!("ERROR: {}", err));
return Err(err);
}
if !dir_path.is_dir() {
return Err(format!("Path is not a directory: {}", path));
let err = format!("Path is not a directory: {}", path);
log(format!("ERROR: {}", err));
return Err(err);
}
let entries = fs::read_dir(dir_path)
.map_err(|e| format!("Failed to read directory: {}", e))?;
.map_err(|e| {
let err = format!("Failed to read directory: {}", e);
log(format!("ERROR: {}", err));
err
})?;
let mut file_entries = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let entry = entry.map_err(|e| {
let err = format!("Failed to read entry: {}", e);
log(format!("ERROR: {}", err));
err
})?;
let path = entry.path();
let name = entry
.file_name()
.to_string_lossy()
.to_string();
// Skip hidden files by default (can be made configurable later)
if name.starts_with('.') {
file_entries.push(FileEntry {
name: name.clone(),
path: path.to_string_lossy().to_string(),
is_directory: path.is_dir(),
});
}
log(format!("Successfully listed {} entries", file_entries.len()));
Ok(file_entries)
}
/// List directory contents via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn list_directory_via_wsl(path: &str) -> Result<Vec<FileEntry>, String> {
use std::process::Command;
// Use WSL to list directory contents
// Output format: type<tab>name (d for directory, f for file)
let script = format!(
r#"if [ -d '{}' ]; then for f in '{}'/* '{}'/.* ; do [ -e "$f" ] || continue; name=$(basename "$f"); if [ "$name" = "." ] || [ "$name" = ".." ]; then continue; fi; if [ -d "$f" ]; then echo "d $name"; else echo "f $name"; fi; done; else echo "ERROR: Directory does not exist"; exit 1; fi"#,
path, path, path
);
let output = Command::new("wsl")
.args(["-e", "sh", "-c", &script])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() || stdout.starts_with("ERROR:") {
let stderr = String::from_utf8_lossy(&output.stderr);
if stdout.starts_with("ERROR:") {
return Err(stdout.trim().to_string());
}
return Err(format!("WSL command failed: {}", stderr));
}
let mut file_entries = Vec::new();
for line in stdout.lines() {
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.splitn(2, '\t').collect();
if parts.len() != 2 {
continue;
}
let is_directory = parts[0] == "d";
let name = parts[1].to_string();
let entry_path = if path == "/" {
format!("/{}", name)
} else {
format!("{}/{}", path, name)
};
file_entries.push(FileEntry {
name,
path: path.to_string_lossy().to_string(),
is_directory: path.is_dir(),
path: entry_path,
is_directory,
});
}
@@ -449,22 +672,80 @@ pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
#[tauri::command]
pub async fn read_file_content(path: String) -> Result<String, String> {
use std::fs;
// On Windows with a WSL path, use WSL to read the file
if cfg!(windows) && path.starts_with('/') {
return read_file_via_wsl(&path).await;
}
use std::fs;
fs::read_to_string(&path)
.map_err(|e| format!("Failed to read file: {}", e))
}
/// Read file content via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn read_file_via_wsl(path: &str) -> Result<String, String> {
use std::process::Command;
let output = Command::new("wsl")
.args(["-e", "cat", path])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to read file: {}", stderr));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[tauri::command]
pub async fn write_file_content(path: String, content: String) -> Result<(), String> {
use std::fs;
// On Windows with a WSL path, use WSL to write the file
if cfg!(windows) && path.starts_with('/') {
return write_file_via_wsl(&path, &content).await;
}
use std::fs;
fs::write(&path, content)
.map_err(|e| format!("Failed to write file: {}", e))
}
/// Write file content via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn write_file_via_wsl(path: &str, content: &str) -> Result<(), String> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("wsl")
.args(["-e", "sh", "-c", &format!("cat > '{}'", path)])
.stdin(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(content.as_bytes())
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
}
let status = child.wait()
.map_err(|e| format!("Failed to wait for WSL command: {}", e))?;
if !status.success() {
return Err("Failed to write file via WSL".to_string());
}
Ok(())
}
#[tauri::command]
pub async fn create_file(path: String) -> Result<(), String> {
// On Windows with a WSL path, use WSL to create the file
if cfg!(windows) && path.starts_with('/') {
return create_file_via_wsl(&path).await;
}
use std::fs::File;
use std::path::Path;
@@ -479,8 +760,41 @@ pub async fn create_file(path: String) -> Result<(), String> {
Ok(())
}
/// Create file via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn create_file_via_wsl(path: &str) -> Result<(), String> {
use std::process::Command;
// Check if file exists first
let check = Command::new("wsl")
.args(["-e", "test", "-e", path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if check.success() {
return Err("File already exists".to_string());
}
let output = Command::new("wsl")
.args(["-e", "touch", path])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to create file: {}", stderr));
}
Ok(())
}
#[tauri::command]
pub async fn create_directory(path: String) -> Result<(), String> {
// On Windows with a WSL path, use WSL to create the directory
if cfg!(windows) && path.starts_with('/') {
return create_directory_via_wsl(&path).await;
}
use std::fs;
use std::path::Path;
@@ -495,8 +809,41 @@ pub async fn create_directory(path: String) -> Result<(), String> {
Ok(())
}
/// Create directory via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn create_directory_via_wsl(path: &str) -> Result<(), String> {
use std::process::Command;
// Check if directory exists first
let check = Command::new("wsl")
.args(["-e", "test", "-e", path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if check.success() {
return Err("Directory already exists".to_string());
}
let output = Command::new("wsl")
.args(["-e", "mkdir", "-p", path])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to create directory: {}", stderr));
}
Ok(())
}
#[tauri::command]
pub async fn delete_file(path: String) -> Result<(), String> {
// On Windows with a WSL path, use WSL to delete the file
if cfg!(windows) && path.starts_with('/') {
return delete_file_via_wsl(&path).await;
}
use std::fs;
use std::path::Path;
@@ -515,8 +862,51 @@ pub async fn delete_file(path: String) -> Result<(), String> {
Ok(())
}
/// Delete file via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn delete_file_via_wsl(path: &str) -> Result<(), String> {
use std::process::Command;
// Check if path exists
let check_exists = Command::new("wsl")
.args(["-e", "test", "-e", path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !check_exists.success() {
return Err("File does not exist".to_string());
}
// Check if path is a directory
let check_dir = Command::new("wsl")
.args(["-e", "test", "-d", path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if check_dir.success() {
return Err("Path is a directory, use delete_directory instead".to_string());
}
let output = Command::new("wsl")
.args(["-e", "rm", path])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to delete file: {}", stderr));
}
Ok(())
}
#[tauri::command]
pub async fn delete_directory(path: String) -> Result<(), String> {
// On Windows with a WSL path, use WSL to delete the directory
if cfg!(windows) && path.starts_with('/') {
return delete_directory_via_wsl(&path).await;
}
use std::fs;
use std::path::Path;
@@ -535,8 +925,51 @@ pub async fn delete_directory(path: String) -> Result<(), String> {
Ok(())
}
/// Delete directory via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn delete_directory_via_wsl(path: &str) -> Result<(), String> {
use std::process::Command;
// Check if path exists
let check_exists = Command::new("wsl")
.args(["-e", "test", "-e", path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !check_exists.success() {
return Err("Directory does not exist".to_string());
}
// Check if path is a directory
let check_dir = Command::new("wsl")
.args(["-e", "test", "-d", path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !check_dir.success() {
return Err("Path is not a directory".to_string());
}
let output = Command::new("wsl")
.args(["-e", "rm", "-rf", path])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to delete directory: {}", stderr));
}
Ok(())
}
#[tauri::command]
pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> {
// On Windows with WSL paths, use WSL to rename
if cfg!(windows) && old_path.starts_with('/') {
return rename_path_via_wsl(&old_path, &new_path).await;
}
use std::fs;
use std::path::Path;
@@ -556,6 +989,44 @@ pub async fn rename_path(old_path: String, new_path: String) -> Result<(), Strin
Ok(())
}
/// Rename path via WSL (for Windows with WSL paths)
#[allow(dead_code)]
async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), String> {
use std::process::Command;
// Check if old path exists
let check_old = Command::new("wsl")
.args(["-e", "test", "-e", old_path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !check_old.success() {
return Err("Path does not exist".to_string());
}
// Check if new path already exists
let check_new = Command::new("wsl")
.args(["-e", "test", "-e", new_path])
.status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if check_new.success() {
return Err("Destination already exists".to_string());
}
let output = Command::new("wsl")
.args(["-e", "mv", old_path, new_path])
.output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to rename: {}", stderr));
}
Ok(())
}
// ==================== Cost Tracking Commands ====================
const COST_HISTORY_STORE_KEY: &str = "cost_history";
@@ -892,7 +1363,10 @@ mod tests {
fn test_select_wsl_directory_returns_home() {
let result = run_async(select_wsl_directory());
assert!(result.is_ok());
assert_eq!(result.unwrap(), "/home");
// Should return the user's home directory
let home_dir = result.unwrap();
assert!(home_dir.starts_with("/home/") || home_dir == "/root");
}
// ==================== UpdateInfo struct tests ====================