use std::fs::OpenOptions;
use std::path::Path;
use fs2::FileExt;
#[path = "common/mod.rs"]
mod common;
use common::{json_string, parse_json_envelope, run};
fn take_external_lock(socket_dir: &Path) -> std::fs::File {
std::fs::create_dir_all(socket_dir).unwrap();
let path = socket_dir.join("apply.lock");
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("open lock file");
file.try_lock_exclusive()
.expect("test could not take initial lock");
file
}
#[test]
fn unlock_reports_free_when_no_socket_dir() {
let dir = tempfile::tempdir().unwrap();
let (code, stdout, stderr) = run(dir.path(), &["unlock", "--json"]);
assert_eq!(code, 0, "stdout={stdout}\nstderr={stderr}");
let env = parse_json_envelope(&stdout);
assert_eq!(json_string(&env, "status"), Some("free"));
assert_eq!(json_string(&env, "command"), Some("unlock"));
}
#[test]
fn unlock_reports_held_when_lock_actively_held() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
let _external = take_external_lock(&socket_dir);
let (code, stdout, stderr) = run(dir.path(), &["unlock", "--json"]);
assert_eq!(code, 1, "stdout={stdout}\nstderr={stderr}");
let env = parse_json_envelope(&stdout);
assert_eq!(json_string(&env, "status"), Some("error"));
let code_field = env
.get("error")
.and_then(|e| e.get("code"))
.and_then(|c| c.as_str());
assert_eq!(code_field, Some("lock_held"));
}
#[test]
fn unlock_release_deletes_lock_file_when_free() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
std::fs::create_dir_all(&socket_dir).unwrap();
let lock_file = socket_dir.join("apply.lock");
std::fs::write(&lock_file, b"").unwrap();
assert!(lock_file.is_file(), "pre-stage failed");
let (code, stdout, stderr) = run(dir.path(), &["unlock", "--json", "--release"]);
assert_eq!(code, 0, "stdout={stdout}\nstderr={stderr}");
let env = parse_json_envelope(&stdout);
assert_eq!(json_string(&env, "status"), Some("free"));
assert_eq!(env.get("released").and_then(|v| v.as_bool()), Some(true));
assert!(
!lock_file.exists(),
"--release should have deleted the lock file"
);
}
#[test]
fn unlock_release_reports_not_released_when_no_lock_file() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
std::fs::create_dir_all(&socket_dir).unwrap();
let lock_file = socket_dir.join("apply.lock");
assert!(!lock_file.exists(), "pre-stage: no lock file expected");
let (code, stdout, stderr) = run(dir.path(), &["unlock", "--json", "--release"]);
assert_eq!(code, 0, "stdout={stdout}\nstderr={stderr}");
let env = parse_json_envelope(&stdout);
assert_eq!(json_string(&env, "status"), Some("free"));
assert_eq!(
env.get("released").and_then(|v| v.as_bool()),
Some(false),
"nothing pre-existed, so released must be false: {stdout}"
);
assert!(
!lock_file.exists(),
"--release should not leave a probe-created lock file behind"
);
}
#[test]
fn unlock_release_reports_not_released_when_no_socket_dir() {
let dir = tempfile::tempdir().unwrap();
let (code, stdout, stderr) = run(dir.path(), &["unlock", "--json", "--release"]);
assert_eq!(code, 0, "stdout={stdout}\nstderr={stderr}");
let env = parse_json_envelope(&stdout);
assert_eq!(json_string(&env, "status"), Some("free"));
assert_eq!(
env.get("released").and_then(|v| v.as_bool()),
Some(false),
"no .socket/ existed, so released must be false: {stdout}"
);
}
#[test]
fn unlock_release_refuses_when_held() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
let _external = take_external_lock(&socket_dir);
let (code, _stdout, _stderr) = run(dir.path(), &["unlock", "--release"]);
assert_eq!(code, 1);
assert!(
socket_dir.join("apply.lock").is_file(),
"lock file must survive a refused --release"
);
}
#[test]
fn unlock_human_mode_hints_at_break_lock_when_held() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
let _external = take_external_lock(&socket_dir);
let (code, _stdout, stderr) = run(dir.path(), &["unlock"]);
assert_eq!(code, 1);
assert!(
stderr.to_lowercase().contains("break-lock"),
"stderr should point operator at --break-lock, got:\n{stderr}"
);
}