use std::fs;
const SCRIPT: &str = "src/scripts/install.sh";
fn script() -> String {
fs::read_to_string(SCRIPT).expect("install.sh readable")
}
fn code_lines() -> Vec<String> {
script()
.lines()
.filter(|l| {
let t = l.trim_start();
!t.starts_with('#') && !t.is_empty()
})
.map(|l| l.to_string())
.collect()
}
#[cfg(unix)]
#[test]
fn the_installer_parses_as_bash() {
let out = std::process::Command::new("bash")
.arg("-n")
.arg(SCRIPT)
.output()
.expect("bash available");
assert!(
out.status.success(),
"install.sh does not parse: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn the_installer_never_waits_for_input() {
for line in code_lines() {
let t = line.trim();
assert!(
!t.starts_with("read "),
"install.sh calls `read`, which blocks forever with no stdin: {line}"
);
assert!(
!t.contains("read -p") && !t.contains("read -r"),
"install.sh prompts for input, which hangs an unattended run: {line}"
);
}
}
#[test]
fn sudo_never_prompts_unless_a_terminal_is_present() {
let src = script();
assert!(
src.contains("INTERACTIVE=1"),
"install.sh must decide whether a terminal exists before using sudo"
);
assert!(
src.contains("sudo -n"),
"the unattended path must use `sudo -n`, which fails instead of prompting"
);
for line in code_lines() {
let t = line.trim();
if !t.starts_with("sudo ") || t.starts_with("sudo -n") {
continue;
}
assert!(
t == "sudo \"$@\"",
"install.sh runs a bare `sudo` outside the guarded interactive arm, \
which hangs an unattended run: {line}"
);
}
assert!(
src.contains("if [ -t 2 ] && [ -r /dev/tty ]"),
"the interactive decision must check for a real terminal, not just stdin \
(stdin is the curl pipe in `curl | bash`)"
);
}
#[test]
fn the_macos_asset_name_matches_what_the_releases_publish() {
let src = script();
assert!(
src.contains(r#"darwin) OS="macos""#),
"install.sh must map darwin to macos before building the asset name, or \
the download 404s on every Mac"
);
assert!(
src.contains(r#"FILENAME="opencrabs-${TAG}-${OS}-${ARCH}.tar.gz""#),
"the asset name must come from the mapped OS, not from uname"
);
assert!(
!src.contains("darwin-${ARCH}") && !src.contains("darwin-amd64"),
"install.sh builds a darwin asset name, which no release publishes"
);
}
#[test]
fn the_installer_verifies_the_download_before_unpacking() {
let src = script();
let verify = src
.find("SHA256SUMS")
.expect("install.sh must fetch SHA256SUMS");
let extract = src
.find("tar xzf")
.expect("install.sh must extract the archive");
assert!(
verify < extract,
"the checksum must be checked before the archive is unpacked, otherwise \
a corrupt download is already on disk when it fails"
);
assert!(
src.contains("Checksum mismatch"),
"a mismatch must be a hard failure with a named reason"
);
}
#[test]
fn the_installer_installs_the_helper_binary_beside_the_agent() {
let src = script();
assert!(
src.contains("install -m 755 \"${TMPDIR}/rtk\""),
"install.sh must install rtk, which every release archive ships"
);
}
#[test]
fn the_installer_handles_both_archive_layouts() {
let src = script();
assert!(
src.contains("OpenCrabs.app"),
"install.sh must handle the bundled macOS layout from #2115"
);
assert!(
src.contains("\"${TMPDIR}/opencrabs\""),
"install.sh must still handle the loose-binary layout of older releases"
);
}
#[test]
fn the_installer_offers_a_dry_run_and_a_dependency_opt_out() {
let src = script();
for flag in ["--dry-run", "--no-deps", "--dir", "--version"] {
assert!(
src.contains(flag),
"install.sh must accept {flag}: a caller that cannot inspect what \
will happen before it happens is a caller that cannot verify"
);
}
let dry = src.find("Dry run: would install").expect("dry run branch");
let download = src.find("Downloading ${TAG}").expect("download step");
assert!(
dry < download,
"the dry run must exit before the download, not after it"
);
}