#![cfg(unix)]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
const FREENET_BIN: &str = env!("CARGO_BIN_EXE_freenet");
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace layout: crates/core/../../ should resolve")
.to_path_buf()
}
fn arm_github_cooldown(home: &Path) {
let dir = home.join(".local/state/freenet");
std::fs::create_dir_all(&dir).expect("create state dir");
let until = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_secs()
+ 600;
std::fs::write(dir.join("github_ratelimit_cooldown"), until.to_string())
.expect("write cooldown");
}
fn run_update_check(home: &Path, rust_log: Option<&str>) -> (String, String) {
let mut cmd = Command::new(FREENET_BIN);
cmd.args(["update", "--check", "--quiet"])
.env("HOME", home)
.env("FREENET_TELEMETRY_ENABLED", "false")
.env_remove("FREENET_DISABLE_LOGS")
.env_remove("FREENET_LOG_FORMAT")
.env_remove("FREENET_LOG_TO_STDERR")
.env_remove("FREENET_POST_STOP_EXIT_CODE");
match rust_log {
Some(v) => cmd.env("RUST_LOG", v),
None => cmd.env_remove("RUST_LOG"),
};
let out = cmd.output().expect("run freenet update --check");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn a_warning_from_the_update_process_reaches_stderr() {
let home = tempfile::tempdir().expect("tempdir");
arm_github_cooldown(home.path());
let (stdout, stderr) = run_update_check(home.path(), None);
assert!(
stderr.contains("Update check deferred"),
"#5244: the update process emitted no WARN, so it has no subscriber installed and every \
warn!/error! in the installer is a no-op — including the ones reporting that crash-loop \
rollback failed to arm.\nstderr was:\n{stderr}\nstdout was:\n{stdout}"
);
assert!(
stderr.contains("WARN"),
"the line reached stderr but without a level, so it came from somewhere other than the \
subscriber:\n{stderr}"
);
assert!(
stdout.trim().is_empty(),
"tracing output must not land on stdout:\n{stdout}"
);
}
#[test]
fn the_update_process_does_not_write_ansi_escapes_to_a_pipe() {
let home = tempfile::tempdir().expect("tempdir");
arm_github_cooldown(home.path());
let (_stdout, stderr) = run_update_check(home.path(), None);
assert!(
!stderr.contains('\u{1b}'),
"ANSI escape sequences must not reach a non-terminal stderr — they would be stored \
verbatim in the journal:\n{stderr:?}"
);
}
#[test]
fn the_update_arm_installs_the_cli_logger_at_warn_and_not_a_file_logger() {
let src = std::fs::read_to_string(workspace_root().join("crates/core/src/bin/freenet.rs"))
.expect("read bin/freenet.rs");
let arm_start = src
.find("Some(Command::Update(cmd)) =>")
.expect("the Update dispatch arm must still exist");
let arm = &src[arm_start..];
let arm_end = arm
.find("Some(Command::Uninstall(cmd))")
.expect("the Uninstall arm must still follow Update; re-anchor this pin if it moved");
let code: String = arm[..arm_end]
.lines()
.map(|l| match l.find("//") {
Some(i) => &l[..i],
None => l,
})
.collect::<Vec<_>>()
.join("\n");
assert!(
code.contains("set_cli_logger"),
"#5244: the Update arm must install a subscriber, or every warn!/error! in the \
installer is a no-op. Arm code (comments stripped):\n{code}"
);
assert!(
!code.contains("set_logger("),
"the Update arm must use `set_cli_logger`, NOT `set_logger`: the latter's log-dir \
argument routes output into the rolling log files, which systemd does not capture, so \
the journal would stay exactly as blind while the fix looked done. Arm code:\n{code}"
);
assert!(
code.contains("LevelFilter::WARN"),
"the level must stay WARN: this runs on every non-clean stop, so INFO would flood the \
journal of exactly the crash-looping node whose journal we need to read. Arm code:\n\
{code}"
);
}
#[test]
fn the_two_safety_state_reports_are_not_gated_on_quiet() {
const SRC: &str = include_str!("../src/bin/commands/update.rs");
const SIG: &str = "async fn download_and_install(";
const WINDOW: usize = 600;
let at = SRC
.find(SIG)
.expect("download_and_install not found — if it was renamed, update this pin deliberately");
let after = &SRC[at + SIG.len()..];
let open = after
.find('{')
.expect("no opening brace after the signature");
let mut depth = 0usize;
let mut end = None;
for (i, c) in after[open..].char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = Some(open + i);
break;
}
}
_ => {}
}
}
let body = &after[open..=end.expect("unbalanced braces in download_and_install")];
assert!(
!body.contains("\nasync fn ") && !body.contains("\n pub fn "),
"the brace match ran past download_and_install; this pin would be measuring more \
than one function"
);
for marker in [
"failed to snapshot the known-good binary",
"FAILED TO ARM crash-loop rollback",
] {
let i = body.find(marker).unwrap_or_else(|| {
panic!(
"safety-state message not found: {marker:?}. If the wording changed, update \
this pin deliberately — do NOT delete it; it is the only guard that the \
message is emitted regardless of --quiet."
)
});
let window = &body[i.saturating_sub(WINDOW)..i];
assert!(
!window.contains("!self.quiet"),
"the safety-state report {marker:?} is gated on --quiet. The supervisor runs \
`freenet update --quiet`, so gating it means #4073's brick-safety machinery \
reports that it is OFF to nobody. See #5244."
);
}
}