ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
631 lines
27 KiB
Rust
631 lines
27 KiB
Rust
#[test]
|
|
fn quiet_update_keeps_prebuilt_enabled_by_default() {
|
|
let install = include_str!("../install.sh");
|
|
assert!(install.contains("use_prebuilt=\"${DOSH_USE_PREBUILT:-1}\""));
|
|
assert!(
|
|
!install.contains("use_prebuilt=0"),
|
|
"quiet updates must not force source builds"
|
|
);
|
|
assert!(install.contains("[ \"$quiet\" != \"1\" ] && echo \"Trying Dosh prebuilt"));
|
|
assert!(install.contains("[ \"$quiet\" != \"1\" ] && echo \"Building Dosh $role\""));
|
|
assert!(
|
|
!install.contains("[ \"$quiet\" = \"1\" ] && echo"),
|
|
"quiet updates must suppress informational installer output"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn release_scripts_skip_stale_artifacts_when_auto_discovering() {
|
|
let upload = include_str!("../scripts/upload-gitea-release.sh");
|
|
assert!(upload.contains("skipping $artifact: version"));
|
|
assert!(upload.contains("expected_version"));
|
|
|
|
let publish = include_str!("../scripts/publish-gitea-release.sh");
|
|
assert!(publish.contains("skipping stale artifact"));
|
|
assert!(publish.contains("actual=\"$(artifact_version \"$artifact\" || true)\""));
|
|
}
|
|
|
|
#[test]
|
|
fn release_upload_refuses_partial_platform_sets_by_default() {
|
|
let upload = include_str!("../scripts/upload-gitea-release.sh");
|
|
assert!(upload.contains("require_complete_release_artifacts \"$@\""));
|
|
assert!(upload.contains("DOSH_RELEASE_ALLOW_PARTIAL=1"));
|
|
assert!(upload.contains("refusing partial release upload"));
|
|
for name in [
|
|
"dosh-linux-x86_64.tar.gz",
|
|
"dosh-macos-aarch64.tar.gz",
|
|
"dosh-macos-x86_64.tar.gz",
|
|
"dosh-windows-x86_64.zip",
|
|
"dosh-windows-aarch64.zip",
|
|
] {
|
|
assert!(
|
|
upload.contains(name),
|
|
"release upload completeness check must require {name}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn release_upload_marks_prerelease_tags_as_prereleases() {
|
|
let upload = include_str!("../scripts/upload-gitea-release.sh");
|
|
assert!(upload.contains("release_prerelease=false"));
|
|
assert!(upload.contains("*-*) release_prerelease=true"));
|
|
assert!(upload.contains("\"prerelease\":%s"));
|
|
assert!(upload.contains("metadata_payload="));
|
|
assert!(
|
|
upload.contains("-X PATCH"),
|
|
"existing Gitea releases must be corrected when prerelease metadata changes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn release_gates_test_all_targets() {
|
|
let makefile = include_str!("../Makefile");
|
|
let ci = include_str!("../.github/workflows/ci.yml");
|
|
assert!(makefile.contains("test:\n\tcargo test --all-targets"));
|
|
assert!(makefile.contains("cargo test --all-targets\n\t$(MAKE) tui-harness"));
|
|
assert!(ci.contains("run: sh -n install.sh scripts/*.sh"));
|
|
assert!(ci.contains("run: cargo clippy --all-targets -- -D warnings"));
|
|
assert!(ci.contains("run: cargo test --all-targets"));
|
|
assert!(ci.contains("run: sh scripts/tui-harness.sh"));
|
|
let macos_client = ci
|
|
.split("macos-client:")
|
|
.nth(1)
|
|
.and_then(|tail| tail.split("\n windows-client:").next())
|
|
.expect("macos-client job");
|
|
assert!(macos_client.contains("name: macos-aarch64"));
|
|
assert!(macos_client.contains("name: macos-x86_64"));
|
|
assert!(macos_client.contains("os: macos-14"));
|
|
assert!(macos_client.contains("os: macos-13"));
|
|
assert!(macos_client.contains("name: Test macOS client"));
|
|
assert!(
|
|
macos_client.contains("run: cargo test --all-targets"),
|
|
"macOS CI must run Rust tests, not only release packaging"
|
|
);
|
|
assert!(
|
|
macos_client.contains("run: node --test vscode-extension/extension.test.js"),
|
|
"macOS CI must exercise VS Code helper quoting"
|
|
);
|
|
let windows_client = ci
|
|
.split("windows-client:")
|
|
.nth(1)
|
|
.and_then(|tail| tail.split("\n package-release:").next())
|
|
.expect("windows-client job");
|
|
assert!(windows_client.contains("runs-on: windows-latest"));
|
|
assert!(windows_client.contains("name: Test Windows client"));
|
|
assert!(windows_client.contains("name: PowerShell syntax check"));
|
|
assert!(windows_client.contains("shell: pwsh"));
|
|
assert!(
|
|
windows_client
|
|
.contains("[System.Management.Automation.Language.Parser]::ParseFile(\"install.ps1\""),
|
|
"Windows CI must parse the installer with PowerShell"
|
|
);
|
|
assert!(
|
|
windows_client.contains("run: cargo test --all-targets"),
|
|
"Windows CI must run Rust tests, not only build/package"
|
|
);
|
|
assert!(
|
|
windows_client.contains("run: node --test vscode-extension/extension.test.js"),
|
|
"Windows CI must exercise VS Code helper quoting"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn one_dot_zero_gates_default_to_full_length_soaks() {
|
|
let makefile = include_str!("../Makefile");
|
|
let reconnect_check = makefile
|
|
.split("reconnect-check:")
|
|
.nth(1)
|
|
.and_then(|tail| tail.split("\nhostile-network-check:").next())
|
|
.expect("reconnect-check target");
|
|
assert!(reconnect_check.contains("DOSH_SOAK_SECONDS=$${DOSH_SOAK_SECONDS:-1800}"));
|
|
assert!(!reconnect_check.contains("DOSH_SOAK_SECONDS=$${DOSH_SOAK_SECONDS:-300}"));
|
|
|
|
let hostile_check = makefile
|
|
.split("hostile-network-check:")
|
|
.nth(1)
|
|
.and_then(|tail| tail.split("\npersistence-check:").next())
|
|
.expect("hostile-network-check target");
|
|
assert!(hostile_check.contains("DOSH_BADNET_SOAK_SECONDS=$${DOSH_BADNET_SOAK_SECONDS:-1800}"));
|
|
assert!(!hostile_check.contains("DOSH_BADNET_SOAK_SECONDS=$${DOSH_BADNET_SOAK_SECONDS:-300}"));
|
|
}
|
|
|
|
#[test]
|
|
fn installers_skip_stale_latest_release_prebuilts() {
|
|
let install = include_str!("../install.sh");
|
|
assert!(install.contains("latest_release_is_stale"));
|
|
assert!(install.contains("current_source_version"));
|
|
assert!(install.contains("latest release $latest is older than source $current"));
|
|
assert!(install.contains("version_prerelease"));
|
|
assert!(install.contains("prerelease_less_than"));
|
|
assert!(
|
|
install.contains("if latest_release_is_stale; then"),
|
|
"unix installer must skip stale latest before downloading a prebuilt"
|
|
);
|
|
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("Latest-ReleaseIsStale"));
|
|
assert!(ps1.contains("Current-SourceVersion"));
|
|
assert!(ps1.contains("latest release $latestVersion is older than source $current"));
|
|
assert!(ps1.contains("Version-Prerelease"));
|
|
assert!(ps1.contains("Compare-Prerelease"));
|
|
assert!(
|
|
ps1.contains("if (Latest-ReleaseIsStale)"),
|
|
"windows installer must skip stale latest before downloading a prebuilt"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn installers_refuse_prebuilts_without_checksums() {
|
|
let install = include_str!("../install.sh");
|
|
assert!(install.contains("verify_archive_checksum"));
|
|
assert!(install.contains("refusing unverified binary for $url"));
|
|
assert!(
|
|
!install.contains("continuing without sidecar verification"),
|
|
"unix installer must not silently install an unverifiable prebuilt"
|
|
);
|
|
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("Verify-ArchiveChecksum"));
|
|
assert!(ps1.contains("refusing unverified binary for $Url"));
|
|
assert!(
|
|
!ps1.contains("continuing without sidecar verification"),
|
|
"windows installer must not silently install an unverifiable prebuilt"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_reuses_persistent_source_update_cache() {
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("DOSH_UPDATE_CACHE"));
|
|
assert!(ps1.contains("elseif ($env:LOCALAPPDATA)"));
|
|
assert!(ps1.contains("Join-Path $env:LOCALAPPDATA \"dosh\\source\""));
|
|
assert!(ps1.contains("Join-Path $HOME \".cache\\dosh\\source\""));
|
|
assert!(ps1.contains("Assert-NoRelativePathSegments($Path)"));
|
|
assert!(ps1.contains("Assert-NoRelativePathSegments $Path"));
|
|
assert!(ps1.contains("$segment -eq \".\" -or $segment -eq \"..\""));
|
|
assert!(ps1.contains("$localAppData = Normalize-PathForCompare $env:LOCALAPPDATA"));
|
|
assert!(ps1.contains(
|
|
"$localAppDataDosh = Normalize-PathForCompare (Join-Path $env:LOCALAPPDATA \"dosh\")"
|
|
));
|
|
assert!(ps1.contains("$full -eq $localAppData -or $full -eq $localAppDataDosh"));
|
|
assert!(ps1.contains("$sourceCache = Assert-SafeUpdateCache $UpdateCache"));
|
|
assert!(ps1.contains(
|
|
"$fetchArgs = @(\"-C\", $sourceCache, \"fetch\", \"--depth\", \"1\", \"origin\", \"main\")"
|
|
));
|
|
assert!(ps1.contains("$fetchArgs = @(\"-C\", $sourceCache, \"fetch\", \"-q\", \"--depth\", \"1\", \"origin\", \"main\")"));
|
|
assert!(ps1.contains("git @fetchArgs"));
|
|
assert!(ps1.contains("git -C $sourceCache checkout -q -B main FETCH_HEAD"));
|
|
assert!(ps1.contains(
|
|
"$cloneArgs = @(\"clone\", \"--depth\", \"1\", \"--branch\", \"main\", $Repo, $sourceCache)"
|
|
));
|
|
assert!(ps1.contains("$cloneArgs = @(\"clone\", \"-q\", \"--depth\", \"1\", \"--branch\", \"main\", $Repo, $sourceCache)"));
|
|
assert!(ps1.contains("git @cloneArgs | Out-Null"));
|
|
assert!(
|
|
!ps1.contains("git clone --depth 1 $Repo $tmp"),
|
|
"Windows source updates should reuse a persistent cache instead of recloning into temp"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_quiet_source_updates_silence_git_like_unix() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(install.contains("git -C \"$update_cache\" fetch -q --depth 1 origin main"));
|
|
assert!(install.contains("git clone -q --depth 1 --branch main \"$repo\" \"$update_cache\""));
|
|
assert!(ps1.contains("if ($Quiet)"));
|
|
assert!(ps1.contains("\"fetch\", \"-q\", \"--depth\""));
|
|
assert!(ps1.contains("\"clone\", \"-q\", \"--depth\""));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_bootstraps_rust_for_source_fallback_like_unix() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(install.contains("ensure_cargo()"));
|
|
assert!(install.contains("cargo not found; installing Rust toolchain with rustup"));
|
|
assert!(ps1.contains("function Ensure-Cargo"));
|
|
assert!(ps1.contains("cargo not found; installing Rust toolchain with winget/rustup"));
|
|
assert!(ps1.contains("winget install --id Rustlang.Rustup"));
|
|
assert!(ps1.contains("--accept-package-agreements --accept-source-agreements"));
|
|
assert!(ps1.contains("PathList-Contains $env:Path $cargoBin"));
|
|
assert!(ps1.contains("$env:Path = \"$cargoBin;$env:Path\""));
|
|
assert!(ps1.contains("Ensure-Cargo"));
|
|
assert!(!ps1.contains("Require-Command cargo"));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_bootstraps_git_for_source_fallback_like_unix() {
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("function Ensure-Git"));
|
|
assert!(ps1.contains("git not found; installing Git with winget"));
|
|
assert!(ps1.contains("winget install --id Git.Git"));
|
|
assert!(ps1.contains("--accept-package-agreements --accept-source-agreements"));
|
|
assert!(ps1.contains("Join-Path $env:ProgramFiles \"Git\\cmd\""));
|
|
assert!(ps1.contains("Join-Path ${env:ProgramFiles(x86)} \"Git\\cmd\""));
|
|
assert!(ps1.contains("Join-Path $env:LOCALAPPDATA \"Programs\\Git\\cmd\""));
|
|
assert!(ps1.contains("PathList-Contains $env:Path $gitBin"));
|
|
assert!(ps1.contains("$env:Path = \"$gitBin;$env:Path\""));
|
|
assert!(ps1.contains("Ensure-Git"));
|
|
assert!(!ps1.contains("Require-Command git"));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_cmd_installer_wraps_native_powershell_installer() {
|
|
let cmd = include_str!("../install.cmd");
|
|
let readme = include_str!("../README.md");
|
|
assert!(cmd.contains("@echo off"));
|
|
assert!(cmd.contains("https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1"));
|
|
assert!(cmd.contains("for %%P in (powershell.exe pwsh.exe powershell pwsh) do"));
|
|
assert!(cmd.contains("set \"TMP_BINDIR=%TEMP%\\dosh-install-%RANDOM%-%RANDOM%.bindir\""));
|
|
assert!(cmd.contains("set \"DOSH_INSTALL_BINDIR_FILE=%TMP_BINDIR%\""));
|
|
assert!(cmd.contains("Invoke-WebRequest -UseBasicParsing"));
|
|
assert!(cmd.contains("-ExecutionPolicy Bypass -File \"%TMP_PS%\" %*"));
|
|
assert!(cmd.contains(
|
|
"for /f \"usebackq delims=\" %%D in (\"%TMP_BINDIR%\") do if not defined DOSH_CMD_BINDIR set \"DOSH_CMD_BINDIR=%%D\""
|
|
));
|
|
assert!(cmd.contains("if not defined DOSH_CMD_BINDIR ("));
|
|
assert!(
|
|
cmd.contains(
|
|
"if \"%DOSH_CMD_PREFIX%\"==\"\" set \"DOSH_CMD_PREFIX=%USERPROFILE%\\.local\""
|
|
)
|
|
);
|
|
assert!(cmd.contains("set \"DOSH_CMD_BINDIR=%DOSH_CMD_PREFIX%\\bin\""));
|
|
assert!(cmd.contains("echo ;%PATH%; | find /I \";%DOSH_CMD_BINDIR%;\""));
|
|
assert!(cmd.contains("set \"DOSH_NEXT_PATH=%DOSH_CMD_BINDIR%;%PATH%\""));
|
|
assert!(cmd.contains("endlocal & set \"PATH=%DOSH_NEXT_PATH%\" & exit /b %RC%"));
|
|
assert!(readme.contains("Windows client from `cmd.exe`:"));
|
|
assert!(readme.contains("install.cmd"));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_honors_quiet_update_mode() {
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("$Quiet = $env:DOSH_UPDATE_QUIET"));
|
|
assert!(ps1.contains("function Write-Info($Message)"));
|
|
assert!(ps1.contains("if (-not $Quiet)"));
|
|
assert!(ps1.contains("Write-Info \"Trying Dosh prebuilt"));
|
|
assert!(ps1.contains("Write-Info \"Falling back to source build\""));
|
|
assert!(ps1.contains("Write-Info \"Installed Dosh client to $bindir\""));
|
|
assert!(
|
|
!ps1.contains("Write-Host \"Trying Dosh prebuilt"),
|
|
"Windows quiet update must not print prebuilt progress directly"
|
|
);
|
|
assert!(
|
|
!ps1.contains("Write-Host \"Installed Dosh client"),
|
|
"Windows quiet update must not print install summary directly"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_source_fallback_builds_client_only_like_unix() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(install.contains("cargo build -q --release --bin dosh-client"));
|
|
assert!(install.contains("cargo build --release --bin dosh-client"));
|
|
assert!(ps1.contains("$buildArgs = @(\"build\", \"--release\", \"--bin\", \"dosh-client\")"));
|
|
assert!(
|
|
ps1.contains(
|
|
"$buildArgs = @(\"build\", \"-q\", \"--release\", \"--bin\", \"dosh-client\")"
|
|
),
|
|
"Windows quiet source fallback should use cargo -q like Unix"
|
|
);
|
|
assert!(ps1.contains("cargo @buildArgs"));
|
|
assert!(
|
|
!ps1.contains("cargo build --release --bin dosh-client --bin dosh-bench"),
|
|
"Windows client source fallback must not compile dosh-bench on every update"
|
|
);
|
|
assert!(ps1.contains("if (Test-Path \"target\\release\\dosh-bench.exe\")"));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_prebuilt_install_requires_client_but_not_bench() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(install.contains("install_extracted_binary \"$tmpdir/extract\" dosh-client"));
|
|
assert!(install.contains("if found_bench=\"$(find_extracted_binary"));
|
|
assert!(ps1.contains("throw \"prebuilt archive missing dosh-client.exe\""));
|
|
assert!(ps1.contains("-Filter \"dosh-bench.exe\""));
|
|
assert!(ps1.contains("if ($bench)"));
|
|
assert!(ps1.contains("Install-Binary $client.FullName (Join-Path $bindir \"dosh.exe\")"));
|
|
assert!(!ps1.contains("Install-Binary (Join-Path $bindir \"dosh-client.exe\")"));
|
|
assert!(
|
|
!ps1.contains("throw \"prebuilt archive missing dosh-bench.exe\""),
|
|
"Windows prebuilt install should not fall back to source just because dosh-bench is absent"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_stages_locked_binary_replacements() {
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("function Start-DeferredBinaryReplacement"));
|
|
assert!(ps1.contains("function Apply-PendingBinaryReplacements"));
|
|
assert!(ps1.contains("$pending = \"$Destination.pending.$PID\""));
|
|
assert!(ps1.contains("Move-Item -LiteralPath $tmp -Destination $pending -Force"));
|
|
assert!(ps1.contains("Start-DeferredBinaryReplacement $pending $Destination"));
|
|
assert!(ps1.contains("binary is in use; staged replacement"));
|
|
assert!(ps1.contains("for (`$i = 0; `$i -lt 3600; `$i++)"));
|
|
assert!(ps1.contains(
|
|
"Move-Item -LiteralPath `$payload.Pending -Destination `$payload.Destination -Force"
|
|
));
|
|
assert!(ps1.contains("[System.Text.Encoding]::Unicode.GetBytes($script)"));
|
|
assert!(ps1.contains("Start-Process -FilePath $ps"));
|
|
assert!(ps1.contains("Get-ChildItem -LiteralPath $Dir -File -Filter \"*.pending.*\""));
|
|
assert!(ps1.contains("$marker = \".pending.\""));
|
|
assert!(ps1.contains("$destinationName = $name.Substring(0, $index)"));
|
|
assert!(ps1.contains("Move-Item -LiteralPath $pending -Destination $destination -Force"));
|
|
assert!(ps1.contains("Apply-PendingBinaryReplacements $bindir"));
|
|
let recovery = ps1.find("Apply-PendingBinaryReplacements $bindir").unwrap();
|
|
let install = ps1.find("if ($UsePrebuilt)").unwrap();
|
|
assert!(
|
|
recovery < install,
|
|
"pending binary replacements must be recovered before installing new binaries"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_adds_user_path_idempotently() {
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("function Add-UserPath($PathToAdd)"));
|
|
assert!(ps1.contains("function PathList-Contains($PathValue, $PathToFind)"));
|
|
assert!(ps1.contains("$bindir = Normalize-PathForCompare (Join-Path $Prefix \"bin\")"));
|
|
assert!(ps1.contains("if ($env:DOSH_INSTALL_BINDIR_FILE)"));
|
|
assert!(ps1.contains("Write-Utf8NoBom $env:DOSH_INSTALL_BINDIR_FILE $bindir"));
|
|
assert!(ps1.contains("$wanted = Normalize-PathForCompare $PathToFind"));
|
|
assert!(ps1.contains("foreach ($entry in ($PathValue -split ';' | Where-Object { $_ }))"));
|
|
assert!(ps1.contains("$wanted = Normalize-PathForCompare $PathToAdd"));
|
|
assert!(ps1.contains("-ieq $wanted"));
|
|
assert!(ps1.contains("(@($entries) + $PathToAdd) -join ';'"));
|
|
assert!(ps1.contains("PathList-Contains $env:Path $PathToAdd"));
|
|
assert!(ps1.contains("$env:Path = \"$PathToAdd;$env:Path\""));
|
|
assert!(
|
|
ps1.find("PathList-Contains $env:Path $PathToAdd").unwrap()
|
|
< ps1.find("foreach ($entry in $entries)").unwrap(),
|
|
"Windows installer must refresh the current shell PATH even when user PATH already contains Dosh"
|
|
);
|
|
assert!(
|
|
!ps1.contains("\"$userPath;$bindir\""),
|
|
"Windows installer must not write a leading semicolon when user PATH is empty"
|
|
);
|
|
assert!(ps1.contains("Add-UserPath $bindir"));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_prefers_native_wow64_arch_for_prebuilts() {
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(ps1.contains("PROCESSOR_ARCHITEW6432"));
|
|
assert!(ps1.contains("$arch = if ($env:PROCESSOR_ARCHITEW6432)"));
|
|
assert!(ps1.contains("\"ARM64\" { \"aarch64\"; break }"));
|
|
assert!(ps1.contains("\"dosh-windows-$(Normalize-Arch).zip\""));
|
|
}
|
|
|
|
#[test]
|
|
fn unix_installer_rejects_unsafe_source_update_cache_paths() {
|
|
let install = include_str!("../install.sh");
|
|
assert!(install.contains("normalize_update_cache_path()"));
|
|
assert!(install.contains("safe_update_cache_path()"));
|
|
assert!(install.contains("update_cache=\"$(safe_update_cache_path \"$update_cache\")\""));
|
|
for pattern in [
|
|
"\"\"|\"/\"|\"$home_norm\"|\"$home_cache_norm\"",
|
|
"\".\"|\"..\"|./*|../*|*/.|*/..|*/./*|*/../*",
|
|
] {
|
|
assert!(
|
|
install.contains(pattern),
|
|
"Unix installer must reject unsafe update cache pattern {pattern}"
|
|
);
|
|
}
|
|
assert!(
|
|
install.contains("rm -rf \"$update_cache\""),
|
|
"source cache replacement remains the destructive operation guarded by safe_update_cache_path"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn installers_generate_same_native_auth_client_defaults() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
for setting in [
|
|
"auth_preference = \"native,ssh\"",
|
|
"trust_on_first_use = false",
|
|
"native_auth_timeout_ms = 700",
|
|
"known_hosts = \"~/.config/dosh/known_hosts\"",
|
|
"identity_files = [\"~/.ssh/id_ed25519\"]",
|
|
"use_ssh_agent = true",
|
|
"forward_agent = false",
|
|
] {
|
|
assert!(
|
|
install.contains(setting),
|
|
"Unix installer missing client default {setting}"
|
|
);
|
|
assert!(
|
|
ps1.contains(setting),
|
|
"Windows installer missing client default {setting}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_uses_native_credential_cache_path() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
assert!(install.contains("credential_cache = \"~/.local/share/dosh/credentials\""));
|
|
assert!(ps1.contains("$credentialCache = if ($env:LOCALAPPDATA)"));
|
|
assert!(ps1.contains("Join-Path $env:LOCALAPPDATA \"dosh\\credentials\""));
|
|
assert!(ps1.contains(".Replace('\\', '/')"));
|
|
assert!(ps1.contains("credential_cache = \"$credentialCache\""));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_installer_generates_hosts_config_like_unix() {
|
|
let install = include_str!("../install.sh");
|
|
let ps1 = include_str!("../install.ps1");
|
|
for setting in [
|
|
"hosts.toml",
|
|
"# default_command = \"tm\"",
|
|
"[default]",
|
|
"predict = true",
|
|
] {
|
|
assert!(
|
|
install.contains(setting),
|
|
"Unix installer missing host config setting {setting}"
|
|
);
|
|
assert!(
|
|
ps1.contains(setting),
|
|
"Windows installer missing host config setting {setting}"
|
|
);
|
|
}
|
|
assert!(install.contains("host_udp_line=\"# dosh_host = \\\"server.example.com\\\"\""));
|
|
assert!(
|
|
ps1.contains("$hostUdpLine = if ($DoshHost)")
|
|
&& ps1.contains("# dosh_host = `\"server.example.com`\""),
|
|
"Windows installer should not bake an SSH target into dosh_host"
|
|
);
|
|
assert!(
|
|
ps1.contains("Write-Utf8NoBom $hostsConfig $hostsToml"),
|
|
"Windows installer should write TOML as UTF-8 without BOM"
|
|
);
|
|
assert!(
|
|
!ps1.contains("Set-Content -NoNewline -Encoding utf8 $hostsConfig"),
|
|
"PowerShell Set-Content -Encoding utf8 can write a BOM on older hosts"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn package_check_verifies_only_artifacts_it_builds() {
|
|
let makefile = include_str!("../Makefile");
|
|
let verify = include_str!("../scripts/verify-release-artifacts.sh");
|
|
assert!(
|
|
verify.contains("if [ \"$#\" -gt 0 ]; then"),
|
|
"release verifier must accept an explicit artifact list"
|
|
);
|
|
let package_check = makefile
|
|
.split("package-check:")
|
|
.nth(1)
|
|
.and_then(|tail| tail.split("\ninstall:").next())
|
|
.expect("package-check target");
|
|
assert!(package_check.contains("package-release-linux"));
|
|
assert!(package_check.contains("package-release-windows"));
|
|
assert!(package_check.contains("package-release-windows-arm64"));
|
|
assert!(package_check.contains("aarch64-w64-mingw32-clang"));
|
|
assert!(package_check.contains("aarch64-pc-windows-gnullvm"));
|
|
assert!(package_check.contains("dosh-linux-x86_64.tar.gz"));
|
|
assert!(package_check.contains("dosh-windows-x86_64.zip"));
|
|
assert!(
|
|
!package_check.contains("dosh-macos-aarch64.tar.gz"),
|
|
"Linux package-check does not build macOS artifacts, so it must not depend on one"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_release_verifier_requires_all_desktop_architectures() {
|
|
let verify = include_str!("../scripts/verify-release-artifacts.sh");
|
|
assert!(verify.contains("dosh-linux-x86_64.tar.gz"));
|
|
assert!(verify.contains("dosh-macos-aarch64.tar.gz"));
|
|
assert!(verify.contains("dosh-macos-x86_64.tar.gz"));
|
|
assert!(verify.contains("dosh-windows-x86_64.zip"));
|
|
assert!(verify.contains("dosh-windows-aarch64.zip"));
|
|
}
|
|
|
|
#[test]
|
|
fn package_release_workflow_builds_both_windows_architectures() {
|
|
let ci = include_str!("../.github/workflows/ci.yml");
|
|
assert!(ci.contains("name: windows-x86_64"));
|
|
assert!(ci.contains("name: windows-aarch64"));
|
|
assert!(ci.contains("DOSH_PACKAGE_ARCH: ${{ matrix.package_arch }}"));
|
|
assert!(ci.contains("aarch64-pc-windows-msvc"));
|
|
}
|
|
|
|
#[test]
|
|
fn release_verifier_checks_checksums_and_expected_binaries() {
|
|
let verify = include_str!("../scripts/verify-release-artifacts.sh");
|
|
assert!(verify.contains("verify_checksum()"));
|
|
assert!(verify.contains("release checksum mismatch"));
|
|
for member in [
|
|
"dosh/bin/dosh",
|
|
"dosh/bin/dosh-client",
|
|
"dosh/bin/dosh-server",
|
|
"dosh/bin/dosh-auth",
|
|
"dosh/bin/dosh-bench",
|
|
"dosh/bin/dosh.exe",
|
|
"dosh/bin/dosh-client.exe",
|
|
"dosh/bin/dosh-bench.exe",
|
|
] {
|
|
assert!(
|
|
verify.contains(member),
|
|
"release verifier must require archive member {member}"
|
|
);
|
|
}
|
|
assert!(verify.contains("artifact missing expected member"));
|
|
}
|
|
|
|
#[test]
|
|
fn package_release_stamps_git_build_info() {
|
|
let package = include_str!("../scripts/package-release.sh");
|
|
let build = include_str!("../build.rs");
|
|
for name in [
|
|
"DOSH_BUILD_GIT_HASH",
|
|
"DOSH_BUILD_COMMIT_DATE",
|
|
"DOSH_BUILD_GIT_DIRTY",
|
|
] {
|
|
assert!(package.contains(name), "package script must export {name}");
|
|
assert!(build.contains(name), "build.rs must consume {name}");
|
|
}
|
|
assert!(package.contains("git rev-parse --short=12 HEAD"));
|
|
assert!(package.contains("git show -s --format=%cs HEAD"));
|
|
assert!(build.contains("cargo:rerun-if-env-changed=DOSH_BUILD_GIT_HASH"));
|
|
}
|
|
|
|
#[test]
|
|
fn package_release_maps_windows_cross_arch_targets() {
|
|
let package = include_str!("../scripts/package-release.sh");
|
|
assert!(package.contains("host_arch=\"$(normalize_arch)\""));
|
|
assert!(package.contains("elif [ \"$arch\" != \"$host_arch\" ]; then"));
|
|
assert!(package.contains("x86_64-pc-windows-msvc"));
|
|
assert!(package.contains("aarch64-pc-windows-msvc"));
|
|
}
|
|
|
|
#[test]
|
|
fn package_release_windows_zip_tries_classic_and_core_powershell() {
|
|
let package = include_str!("../scripts/package-release.sh");
|
|
assert!(package.contains("powershell_compress_archive()"));
|
|
assert!(package.contains("powershell_word()"));
|
|
assert!(package.contains("sed \"s/'/''/g\""));
|
|
assert!(package.contains("for ps in powershell.exe powershell pwsh.exe pwsh; do"));
|
|
assert!(package.contains("\"$ps\" -NoProfile -Command"));
|
|
assert!(package.contains("Compress-Archive -Force -LiteralPath $(powershell_word \"$stage\")"));
|
|
assert!(package.contains("-DestinationPath $(powershell_word \"$out_dir/$artifact\")"));
|
|
assert!(package.contains("windows packaging requires PowerShell or zip"));
|
|
}
|
|
|
|
#[test]
|
|
fn systemd_service_does_not_sandbox_remote_shells() {
|
|
let service = include_str!("../packaging/systemd/dosh-server.service");
|
|
let install = include_str!("../install.sh");
|
|
for raw in [service, install] {
|
|
assert!(raw.contains("KillMode=process"));
|
|
assert!(
|
|
raw.contains("Restart=always"),
|
|
"dosh-server should come back after accidental SIGTERM while preserving child shells"
|
|
);
|
|
for directive in [
|
|
"NoNewPrivileges=",
|
|
"PrivateTmp=",
|
|
"ProtectSystem=",
|
|
"ProtectHome=",
|
|
"RestrictAddressFamilies=",
|
|
"RestrictRealtime=",
|
|
"LockPersonality=",
|
|
"MemoryDenyWriteExecute=",
|
|
] {
|
|
assert!(
|
|
!raw.contains(directive),
|
|
"dosh sessions are user shells; systemd sandbox directive {directive} changes terminal/app behavior"
|
|
);
|
|
}
|
|
assert!(
|
|
!raw.contains("ReadWritePaths="),
|
|
"remote shells must not be restricted to dosh config/data paths"
|
|
);
|
|
}
|
|
}
|