692 lines
23 KiB
Rust
692 lines
23 KiB
Rust
#[cfg(unix)]
|
|
use std::fs;
|
|
#[cfg(unix)]
|
|
use std::process::Stdio;
|
|
|
|
#[cfg(unix)]
|
|
use command::blocking::Command;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_uname_linux_x86_64() {
|
|
let platform = parse_uname_output("Linux x86_64").unwrap();
|
|
assert_eq!(platform.os, RemoteOs::Linux);
|
|
assert_eq!(platform.arch, RemoteArch::X86_64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_linux_aarch64() {
|
|
let platform = parse_uname_output("Linux aarch64").unwrap();
|
|
assert_eq!(platform.os, RemoteOs::Linux);
|
|
assert_eq!(platform.arch, RemoteArch::Aarch64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_darwin_arm64() {
|
|
let platform = parse_uname_output("Darwin arm64").unwrap();
|
|
assert_eq!(platform.os, RemoteOs::MacOs);
|
|
assert_eq!(platform.arch, RemoteArch::Aarch64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_darwin_x86_64() {
|
|
let platform = parse_uname_output("Darwin x86_64").unwrap();
|
|
assert_eq!(platform.os, RemoteOs::MacOs);
|
|
assert_eq!(platform.arch, RemoteArch::X86_64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_unsupported_armv8l() {
|
|
let result = parse_uname_output("Linux armv8l");
|
|
match result {
|
|
Err(crate::transport::Error::UnsupportedArch { arch }) => {
|
|
assert_eq!(arch, "armv8l");
|
|
}
|
|
other => panic!("expected UnsupportedArch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_skips_shell_initialization_output() {
|
|
let output = "Last login: Mon Apr 7 10:00:00 2025\nWelcome to Ubuntu\nLinux x86_64";
|
|
let platform = parse_uname_output(output).unwrap();
|
|
assert_eq!(platform.os, RemoteOs::Linux);
|
|
assert_eq!(platform.arch, RemoteArch::X86_64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_trims_whitespace() {
|
|
let platform = parse_uname_output(" Linux x86_64 \n").unwrap();
|
|
assert_eq!(platform.os, RemoteOs::Linux);
|
|
assert_eq!(platform.arch, RemoteArch::X86_64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_unsupported_os() {
|
|
let result = parse_uname_output("Windows x86_64");
|
|
match result {
|
|
Err(crate::transport::Error::UnsupportedOs { os }) => {
|
|
assert_eq!(os, "Windows");
|
|
}
|
|
other => panic!("expected UnsupportedOs, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_unsupported_arch() {
|
|
let result = parse_uname_output("Linux mips");
|
|
match result {
|
|
Err(crate::transport::Error::UnsupportedArch { arch }) => {
|
|
assert_eq!(arch, "mips");
|
|
}
|
|
other => panic!("expected UnsupportedArch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_empty_output() {
|
|
let result = parse_uname_output("");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_missing_arch() {
|
|
let result = parse_uname_output("Linux");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn identity_dir_name_is_short_hash() {
|
|
let name = remote_server_identity_dir_name("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
|
|
assert_eq!(name.len(), 8, "identity dir should be 8 hex chars: {name}");
|
|
assert!(
|
|
name.chars().all(|c| c.is_ascii_hexdigit()),
|
|
"identity dir should be hex: {name}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn identity_dir_name_is_deterministic() {
|
|
let key = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
assert_eq!(
|
|
remote_server_identity_dir_name(key),
|
|
remote_server_identity_dir_name(key)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn identity_dir_name_differs_for_different_keys() {
|
|
assert_ne!(
|
|
remote_server_identity_dir_name("key-a"),
|
|
remote_server_identity_dir_name("key-b")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn data_dir_uses_percent_encoded_identity_key() {
|
|
let data_dir = remote_server_daemon_data_dir("user@example.com/ssh host");
|
|
assert_eq!(
|
|
data_dir,
|
|
format!(
|
|
"{}/user%40example%2Ecom%2Fssh%20host/data",
|
|
remote_server_dir()
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn data_dir_handles_empty_identity_key() {
|
|
let data_dir = remote_server_daemon_data_dir("");
|
|
assert_eq!(data_dir, format!("{}/empty/data", remote_server_dir()));
|
|
}
|
|
|
|
#[test]
|
|
fn daemon_dir_and_data_dir_use_different_identity_paths() {
|
|
let key = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
let daemon_dir = remote_server_daemon_dir(key);
|
|
let data_dir = remote_server_daemon_data_dir(key);
|
|
// Daemon dir uses the 8-char hash.
|
|
assert!(daemon_dir.contains(&remote_server_identity_dir_name(key)));
|
|
// Data dir uses the full key (no collision risk for persistent state).
|
|
assert!(data_dir.contains(key));
|
|
// They must be different paths.
|
|
assert!(!data_dir.starts_with(&daemon_dir));
|
|
}
|
|
|
|
#[test]
|
|
fn state_is_ready() {
|
|
assert!(RemoteServerSetupState::Ready.is_ready());
|
|
assert!(!RemoteServerSetupState::Checking.is_ready());
|
|
assert!(!RemoteServerSetupState::Initializing.is_ready());
|
|
}
|
|
|
|
#[test]
|
|
fn state_is_failed() {
|
|
assert!(RemoteServerSetupState::Failed {
|
|
error: "test".into()
|
|
}
|
|
.is_failed());
|
|
assert!(!RemoteServerSetupState::Ready.is_failed());
|
|
}
|
|
|
|
#[test]
|
|
fn state_is_terminal() {
|
|
assert!(RemoteServerSetupState::Ready.is_terminal());
|
|
assert!(RemoteServerSetupState::Failed {
|
|
error: "test".into()
|
|
}
|
|
.is_terminal());
|
|
assert!(RemoteServerSetupState::Unsupported {
|
|
reason: UnsupportedReason::NonGlibc {
|
|
name: "musl".into()
|
|
}
|
|
}
|
|
.is_terminal());
|
|
assert!(!RemoteServerSetupState::Checking.is_terminal());
|
|
assert!(!RemoteServerSetupState::Installing {
|
|
progress_percent: None,
|
|
}
|
|
.is_terminal());
|
|
assert!(!RemoteServerSetupState::Updating.is_terminal());
|
|
assert!(!RemoteServerSetupState::Initializing.is_terminal());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_preinstall_supported_glibc() {
|
|
let stdout = "required_glibc=2.31\n\
|
|
libc_family=glibc\n\
|
|
libc_version=2.35\n\
|
|
status=supported\n";
|
|
let result = PreinstallCheckResult::parse(stdout);
|
|
assert_eq!(result.status, PreinstallStatus::Supported);
|
|
assert_eq!(result.libc, RemoteLibc::Glibc(GlibcVersion::new(2, 35)));
|
|
assert!(result.is_supported());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_preinstall_unsupported_glibc_too_old() {
|
|
let stdout = "required_glibc=2.31\n\
|
|
libc_family=glibc\n\
|
|
libc_version=2.17\n\
|
|
status=unsupported\n\
|
|
reason=glibc_too_old\n";
|
|
let result = PreinstallCheckResult::parse(stdout);
|
|
assert_eq!(
|
|
result.status,
|
|
PreinstallStatus::Unsupported {
|
|
reason: UnsupportedReason::GlibcTooOld {
|
|
detected: GlibcVersion::new(2, 17),
|
|
required: GlibcVersion::new(2, 31),
|
|
}
|
|
}
|
|
);
|
|
assert!(!result.is_supported());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_preinstall_unsupported_non_glibc() {
|
|
let stdout = "required_glibc=2.31\n\
|
|
libc_family=musl\n\
|
|
status=unsupported\n\
|
|
reason=non_glibc\n";
|
|
let result = PreinstallCheckResult::parse(stdout);
|
|
assert_eq!(
|
|
result.status,
|
|
PreinstallStatus::Unsupported {
|
|
reason: UnsupportedReason::NonGlibc {
|
|
name: "musl".to_string()
|
|
}
|
|
}
|
|
);
|
|
assert_eq!(
|
|
result.libc,
|
|
RemoteLibc::NonGlibc {
|
|
name: "musl".to_string()
|
|
}
|
|
);
|
|
assert!(!result.is_supported());
|
|
}
|
|
|
|
#[test]
|
|
fn bundled_resources_dir_is_global_and_version_independent() {
|
|
let dir = remote_server_bundled_resources_dir();
|
|
assert_eq!(
|
|
dir,
|
|
format!("{}/{}", remote_server_dir(), BUNDLED_RESOURCES_DIR_NAME)
|
|
);
|
|
// The whole point of the global location: no version in the path.
|
|
assert!(!dir.contains(remote_server_artifact_version()));
|
|
}
|
|
|
|
#[test]
|
|
fn binary_check_runs_version() {
|
|
assert_eq!(
|
|
binary_check_command(),
|
|
format!("{} --version", remote_server_binary())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn removal_command_removes_binary_but_leaves_global_resources() {
|
|
let command = remote_server_removal_command();
|
|
assert_eq!(command, format!("rm -f {}", remote_server_binary()));
|
|
assert!(!command.contains(BUNDLED_RESOURCES_DIR_NAME));
|
|
}
|
|
|
|
#[test]
|
|
fn install_script_substitutes_bundled_resources_dir_name() {
|
|
let script = install_script(None);
|
|
assert!(!script.contains("{bundled_resources_dir_name}"));
|
|
assert!(script.contains(&format!("$install_dir/{BUNDLED_RESOURCES_DIR_NAME}")));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn make_test_tarball(
|
|
test_root: &std::path::Path,
|
|
tarball_name: &str,
|
|
skill_content: &str,
|
|
include_decoy: bool,
|
|
) -> std::path::PathBuf {
|
|
let tar_source = test_root.join(format!("tar-source-{tarball_name}"));
|
|
let resources = tar_source.join("resources/bundled/skills/test-skill");
|
|
let tarball = test_root.join(format!("{tarball_name}.tar.gz"));
|
|
fs::create_dir_all(&resources).unwrap();
|
|
fs::write(
|
|
tar_source.join("oz-test"),
|
|
"#!/usr/bin/env bash\n[ \"$1\" = \"--version\" ]\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(resources.join("SKILL.md"), skill_content).unwrap();
|
|
if include_decoy {
|
|
// Decoy: skills may ship companion files whose names also start
|
|
// with `oz`. The installer must not mistake them for the executable.
|
|
fs::write(
|
|
resources.join("oz-decoy.sh"),
|
|
"#!/usr/bin/env bash\nexit 1\n",
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let tar_output = Command::new("tar")
|
|
.arg("-czf")
|
|
.arg(&tarball)
|
|
.arg("-C")
|
|
.arg(&tar_source)
|
|
.arg("oz-test")
|
|
.arg("resources")
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.expect("failed to create test tarball");
|
|
assert!(
|
|
tar_output.status.success(),
|
|
"tar failed: {}",
|
|
String::from_utf8_lossy(&tar_output.stderr)
|
|
);
|
|
tarball
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn run_install_script(tarball: &std::path::Path, fake_home: &std::path::Path) {
|
|
let script = install_script(Some(tarball.to_str().unwrap()));
|
|
let install_output = Command::new("bash")
|
|
.arg("-c")
|
|
.arg(&script)
|
|
.env("HOME", fake_home)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.expect("failed to run install script");
|
|
assert!(
|
|
install_output.status.success(),
|
|
"install failed: {}",
|
|
String::from_utf8_lossy(&install_output.stderr)
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn install_script_installs_binary_and_global_resources() {
|
|
let test_root = std::env::temp_dir().join(format!(
|
|
"remote-server-global-install-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let fake_home = test_root.join("home");
|
|
fs::create_dir_all(&fake_home).unwrap();
|
|
|
|
let tarball = make_test_tarball(&test_root, "first", "test skill", true);
|
|
run_install_script(&tarball, &fake_home);
|
|
|
|
let resolve_remote_path = |path: String| path.replacen('~', fake_home.to_str().unwrap(), 1);
|
|
let binary = resolve_remote_path(remote_server_binary());
|
|
let resources = resolve_remote_path(remote_server_bundled_resources_dir());
|
|
let skill_md = std::path::Path::new(&resources).join("bundled/skills/test-skill/SKILL.md");
|
|
|
|
assert!(fs::metadata(&binary).unwrap().is_file());
|
|
assert!(fs::metadata(&resources).unwrap().is_dir());
|
|
assert_eq!(fs::read_to_string(&skill_md).unwrap(), "test skill");
|
|
assert!(std::path::Path::new(&resources)
|
|
.join("bundled/skills/test-skill/oz-decoy.sh")
|
|
.is_file());
|
|
|
|
let check_output = Command::new("bash")
|
|
.arg("-c")
|
|
.arg(binary_check_command())
|
|
.env("HOME", &fake_home)
|
|
.output()
|
|
.expect("failed to run binary check command");
|
|
assert!(check_output.status.success());
|
|
|
|
// A later install fully replaces the global resources (last install
|
|
// wins): updated content lands and files absent from the new artifact
|
|
// disappear, proving a swap rather than a merge.
|
|
let second_tarball = make_test_tarball(&test_root, "second", "updated skill", false);
|
|
run_install_script(&second_tarball, &fake_home);
|
|
assert_eq!(fs::read_to_string(&skill_md).unwrap(), "updated skill");
|
|
assert!(!std::path::Path::new(&resources)
|
|
.join("bundled/skills/test-skill/oz-decoy.sh")
|
|
.exists());
|
|
|
|
// Removal deletes the binary but leaves the global resources for the
|
|
// next install to overwrite.
|
|
let removal_output = Command::new("bash")
|
|
.arg("-c")
|
|
.arg(remote_server_removal_command())
|
|
.env("HOME", &fake_home)
|
|
.output()
|
|
.expect("failed to run removal command");
|
|
assert!(removal_output.status.success());
|
|
assert!(!std::path::Path::new(&binary).exists());
|
|
assert!(fs::metadata(&resources).unwrap().is_dir());
|
|
|
|
fs::remove_dir_all(test_root).unwrap();
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn install_script_tolerates_tarball_without_resources() {
|
|
let test_root = std::env::temp_dir().join(format!(
|
|
"remote-server-no-resources-install-{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
let fake_home = test_root.join("home");
|
|
let tar_source = test_root.join("tar-source");
|
|
let tarball = test_root.join("oz.tar.gz");
|
|
fs::create_dir_all(&fake_home).unwrap();
|
|
fs::create_dir_all(&tar_source).unwrap();
|
|
fs::write(
|
|
tar_source.join("oz-test"),
|
|
"#!/usr/bin/env bash\n[ \"$1\" = \"--version\" ]\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let tar_output = Command::new("tar")
|
|
.arg("-czf")
|
|
.arg(&tarball)
|
|
.arg("-C")
|
|
.arg(&tar_source)
|
|
.arg("oz-test")
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.expect("failed to create test tarball");
|
|
assert!(tar_output.status.success());
|
|
|
|
run_install_script(&tarball, &fake_home);
|
|
|
|
let resolve_remote_path = |path: String| path.replacen('~', fake_home.to_str().unwrap(), 1);
|
|
let binary = resolve_remote_path(remote_server_binary());
|
|
let resources = resolve_remote_path(remote_server_bundled_resources_dir());
|
|
assert!(fs::metadata(&binary).unwrap().is_file());
|
|
assert!(!std::path::Path::new(&resources).exists());
|
|
|
|
fs::remove_dir_all(test_root).unwrap();
|
|
}
|
|
|
|
/// Regression: the install script's tilde-expansion logic must work
|
|
/// across the bash versions we actually invoke at install time
|
|
/// (`run_ssh_script` pipes the script into `bash -s` on the remote).
|
|
/// Two interpreter quirks have to be avoided simultaneously:
|
|
///
|
|
/// 1. bash 3.2 (macOS `/bin/bash`) keeps inner double-quotes around
|
|
/// the replacement of `${var/pattern/replacement}` literal, so
|
|
/// `"$HOME"` ends up as 6 literal characters and the install
|
|
/// lands under a directory tree literally named `"`.
|
|
/// 2. bash 5.2+ with `patsub_replacement` (default-on) treats `&`
|
|
/// in the replacement as the matched pattern, so a `$HOME`
|
|
/// containing `&` resolves to a `~`-substituted path.
|
|
///
|
|
/// Both bugs surface as the install binary landing somewhere Warp's
|
|
/// launch step doesn't look, producing a misleading "Response channel
|
|
/// closed before receiving a reply".
|
|
///
|
|
/// This test drives the *actual* production script (via
|
|
/// [`install_script`]) rather than a hand-copied snippet, and runs it
|
|
/// against several `HOME` values to exercise the patsub-`&` trap as
|
|
/// well as the quote-literal trap. We truncate just before `mkdir -p`
|
|
/// so no filesystem side effects leak out of the test, and append a
|
|
/// marker `printf` to capture the resolved `install_dir`.
|
|
///
|
|
/// Gated to Unix because the test invokes `/bin/bash` (or `bash` from
|
|
/// PATH) directly. The bug only matters on Unix remotes anyway —
|
|
/// Warp's remote-server SSH wrapper doesn't target Windows hosts.
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn install_script_tilde_expansion_resolves_correctly() {
|
|
let bash = if std::path::Path::new("/bin/bash").exists() {
|
|
"/bin/bash"
|
|
} else {
|
|
"bash"
|
|
};
|
|
|
|
let script = install_script(None);
|
|
let cutoff = script.find("mkdir -p \"$install_dir\"").expect(
|
|
"install script no longer contains the `mkdir -p \"$install_dir\"` \
|
|
checkpoint this test relies on; update the test alongside the \
|
|
script change",
|
|
);
|
|
let probe = format!(
|
|
"{prefix}\nprintf '%s' \"$install_dir\"\nexit 0\n",
|
|
prefix = &script[..cutoff],
|
|
);
|
|
|
|
// Run the probe against a matrix of HOME values. The first is an
|
|
// ordinary path; the second contains `&`, which exercises bash
|
|
// 5.2's patsub_replacement (where it would otherwise expand to
|
|
// the matched `~`).
|
|
let cases = [
|
|
("/Users/test", "ordinary HOME"),
|
|
(
|
|
"/Users/A&B",
|
|
"HOME with `&` (bash 5.2 patsub_replacement trap)",
|
|
),
|
|
];
|
|
|
|
for (fake_home, label) in cases {
|
|
let output = Command::new(bash)
|
|
.arg("-c")
|
|
.arg(&probe)
|
|
.env("HOME", fake_home)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.expect("failed to spawn bash");
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"[{label}] bash exited with {:?}: stderr={}",
|
|
output.status,
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
|
|
let install_dir = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
!install_dir.contains('"'),
|
|
"[{label}] install_dir contains literal quote characters \
|
|
(bash 3.2 quote-literal regression): {install_dir:?}",
|
|
);
|
|
|
|
// Cross-check against the production layout: tilde must
|
|
// resolve to HOME, so the result equals `remote_server_dir()`
|
|
// with the leading `~` replaced.
|
|
let expected = remote_server_dir().replacen('~', fake_home, 1);
|
|
assert_eq!(
|
|
install_dir, expected,
|
|
"[{label}] install_dir resolved incorrectly",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Regression: guards against re-introducing the
|
|
/// `${var/pattern/replacement}` tilde-substitution form, which has two
|
|
/// known interpreter bugs (see
|
|
/// [`install_script_tilde_expansion_resolves_correctly`] for details).
|
|
/// Complements the live bash test — the live test catches behavioural
|
|
/// regressions, this static check fails fast and explains *why* in
|
|
/// the assertion message so a future contributor doesn't have to
|
|
/// re-discover the constraints from a CI failure.
|
|
#[test]
|
|
fn install_script_avoids_pattern_substitution_for_tilde_expansion() {
|
|
let template = INSTALL_SCRIPT_TEMPLATE;
|
|
assert!(
|
|
!template.contains(r"/#\~/"),
|
|
"install_remote_server.sh uses `${{var/#\\~/...}}` for tilde \
|
|
expansion. This form has two known interpreter bugs that \
|
|
silently mis-resolve the install path:\n\
|
|
\n\
|
|
1. bash 3.2 (macOS /bin/bash) keeps inner double-quotes \
|
|
around the replacement literal, so `\"$HOME\"` ends up \
|
|
as 6 literal characters including the quotes.\n\
|
|
2. bash 5.2+ enables `patsub_replacement` by default, which \
|
|
makes `&` in the replacement expand to the matched \
|
|
pattern, so a `$HOME` containing `&` resolves wrong.\n\
|
|
\n\
|
|
Use `case`/`${{var#\\~}}` instead — see install_remote_server.sh \
|
|
for the pattern.",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn version_hash_is_deterministic() {
|
|
// version_hash uses the compile-time GIT_RELEASE_TAG which is typically
|
|
// unset in test builds, so it returns None. We test the hashing logic
|
|
// directly instead.
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
let version = "v0.2026.05.13.09.15.stable_01";
|
|
let hash = |v: &str| -> String {
|
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
v.hash(&mut hasher);
|
|
format!("{:016x}", hasher.finish())[..8].to_string()
|
|
};
|
|
|
|
// Same input produces the same hash.
|
|
assert_eq!(hash(version), hash(version));
|
|
// Different inputs produce different hashes.
|
|
assert_ne!(hash(version), hash("v0.2026.05.14.09.15.stable_01"));
|
|
// Hash is exactly 8 hex chars.
|
|
assert_eq!(hash(version).len(), 8);
|
|
assert!(hash(version).chars().all(|c| c.is_ascii_hexdigit()));
|
|
}
|
|
|
|
#[test]
|
|
fn daemon_socket_name_is_short() {
|
|
// Without GIT_RELEASE_TAG (typical in tests), falls back to unversioned.
|
|
let name = daemon_socket_name();
|
|
// In test builds without GIT_RELEASE_TAG, we get "server.sock".
|
|
// In release builds, we get "server-{8hex}.sock" = 24 chars.
|
|
// Either way, the name must be ≤ 24 chars.
|
|
assert!(
|
|
name.len() <= 24,
|
|
"daemon_socket_name is too long ({} chars): {name}",
|
|
name.len()
|
|
);
|
|
assert!(name.starts_with("server"));
|
|
assert!(name.ends_with(".sock"));
|
|
}
|
|
|
|
#[test]
|
|
fn daemon_pid_name_is_short() {
|
|
let name = daemon_pid_name();
|
|
assert!(
|
|
name.len() <= 22,
|
|
"daemon_pid_name is too long ({} chars): {name}",
|
|
name.len()
|
|
);
|
|
assert!(name.starts_with("server"));
|
|
assert!(name.ends_with(".pid"));
|
|
}
|
|
|
|
#[test]
|
|
fn socket_path_fits_within_sun_path_worst_case() {
|
|
// Worst case: preview channel (longest base dir) + 32-char username
|
|
// (Linux max) + hashed identity (8 chars) + hashed socket (20 chars).
|
|
//
|
|
// Path: /home/{user}/.warp-preview/remote-server/{hash8}/server-{hash8}.sock
|
|
// 6 + 32 + 1 + 29 + 8 + 1 + 20 = 97 bytes → well under 103 (macOS)
|
|
let long_home = "/home/a]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]";
|
|
let identity_dir = remote_server_identity_dir_name("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
|
|
assert_eq!(identity_dir.len(), 8);
|
|
|
|
let hashed_socket = "server-a1b2c3d4.sock";
|
|
let old_socket = "server-v0.2026.05.13.09.15.stable_01.sock";
|
|
|
|
// Use .warp-preview (longest channel base dir) for worst case.
|
|
let daemon_dir = format!("{long_home}/.warp-preview/remote-server/{identity_dir}");
|
|
|
|
let hashed_path = format!("{daemon_dir}/{hashed_socket}");
|
|
|
|
// Must fit within macOS sun_path limit (103 bytes), the stricter of
|
|
// the two platforms.
|
|
assert!(
|
|
hashed_path.len() <= 103,
|
|
"hashed socket path exceeds macOS sun_path limit: {} bytes ({})",
|
|
hashed_path.len(),
|
|
hashed_path,
|
|
);
|
|
|
|
// The OLD naming scheme (full version + unhashed identity) should
|
|
// exceed the limit, confirming the regression.
|
|
let old_identity = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; // 36 chars unhashed
|
|
let old_daemon_dir = format!("{long_home}/.warp-preview/remote-server/{old_identity}");
|
|
let old_full_path = format!("{old_daemon_dir}/{old_socket}");
|
|
assert!(
|
|
old_full_path.len() > 107,
|
|
"old socket path should exceed Linux sun_path limit to confirm the \
|
|
regression: {} bytes ({})",
|
|
old_full_path.len(),
|
|
old_full_path,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_linux_amd64() {
|
|
let platform = parse_uname_output("Linux amd64").unwrap();
|
|
assert_eq!(platform.os, RemoteOs::Linux);
|
|
assert_eq!(platform.arch, RemoteArch::X86_64);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_uname_unsupported_armv7l() {
|
|
let result = parse_uname_output("Linux armv7l");
|
|
match result {
|
|
Err(crate::transport::Error::UnsupportedArch { arch }) => {
|
|
assert_eq!(arch, "armv7l");
|
|
}
|
|
other => panic!("expected UnsupportedArch, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_preinstall_missing_status_falls_open() {
|
|
// Garbled / partial script output — missing status field. Confirms
|
|
// the fail-open invariant: anything we can't positively classify as
|
|
// unsupported degrades to Unknown and is treated as supported, so a
|
|
// flaky probe doesn't block the install.
|
|
let stdout = "libc_family=glibc\nlibc_version=2.35\n";
|
|
let result = PreinstallCheckResult::parse(stdout);
|
|
assert_eq!(result.status, PreinstallStatus::Unknown);
|
|
assert!(result.is_supported());
|
|
}
|