use std::fs::OpenOptions;
use std::path::Path;
use std::time::Duration;
use fs2::FileExt;
#[path = "common/mod.rs"]
mod common;
use common::{
envelope_error_code, json_string, parse_json_envelope, run, write_minimal_manifest,
PatchEntry,
};
fn setup_socket_dir(socket_dir: &Path) {
write_minimal_manifest(
socket_dir,
"pkg:npm/lockfixture@1.0.0",
"22222222-2222-4222-8222-222222222222",
&[PatchEntry {
file_name: "package/index.js",
before_hash: &"a".repeat(64),
after_hash: &"b".repeat(64),
}],
);
}
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 lock_held_returned_to_second_process() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
let _external = take_external_lock(&socket_dir);
let (code, stdout, stderr) = run(dir.path(), &["apply", "--json"]);
assert_eq!(
code, 1,
"expected lock contention to exit 1.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let env = parse_json_envelope(&stdout);
assert_eq!(
envelope_error_code(&env),
Some("lock_held"),
"expected errorCode=lock_held.\nenvelope: {env}"
);
assert_eq!(json_string(&env, "status"), Some("error"));
}
#[test]
fn lock_held_human_mode_mentions_other_process() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
let _external = take_external_lock(&socket_dir);
let (code, _stdout, stderr) = run(dir.path(), &["apply"]);
assert_eq!(code, 1);
assert!(
stderr.to_lowercase().contains("another")
&& stderr.to_lowercase().contains("process"),
"stderr should mention another process holding the lock, got:\n{stderr}"
);
}
#[test]
fn lock_released_after_external_drop() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
{
let _external = take_external_lock(&socket_dir);
}
let (_code, stdout, _stderr) = run(dir.path(), &["apply", "--json"]);
assert!(
!stdout.contains("lock_held"),
"fresh apply after lock release must not report lock_held.\nstdout:\n{stdout}"
);
}
#[test]
fn lock_file_persists_across_runs() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
let _ = run(dir.path(), &["apply", "--json"]);
assert!(
socket_dir.join("apply.lock").is_file(),
"apply.lock should persist between runs"
);
let (_code, stdout, _stderr) = run(dir.path(), &["apply", "--json"]);
assert!(
!stdout.contains("lock_held"),
"second run on persistent lock file must succeed in acquiring.\nstdout:\n{stdout}"
);
}
#[test]
fn two_apply_subprocesses_serialize() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
let external = take_external_lock(&socket_dir);
let (code, stdout, _) = run(dir.path(), &["apply", "--json"]);
assert_eq!(code, 1);
let env = parse_json_envelope(&stdout);
assert_eq!(envelope_error_code(&env), Some("lock_held"));
drop(external);
let (_code2, stdout2, _) = run(dir.path(), &["apply", "--json"]);
assert!(
!stdout2.contains("lock_held"),
"after lock release apply should acquire.\nstdout:\n{stdout2}"
);
}
#[test]
fn helper_lock_is_actually_exclusive() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
std::fs::create_dir_all(&socket_dir).unwrap();
let _first = take_external_lock(&socket_dir);
let path = socket_dir.join("apply.lock");
let second = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
let result = second.try_lock_exclusive();
assert!(
result.is_err(),
"second flock on same file should fail while first is held"
);
}
#[test]
fn break_lock_removes_stale_file_and_records_warning() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
std::fs::write(socket_dir.join("apply.lock"), b"").unwrap();
let (_code, stdout, _stderr) = run(dir.path(), &["apply", "--json", "--break-lock"]);
let env = parse_json_envelope(&stdout);
let events = env["events"].as_array().expect("events array");
let has_lock_broken = events.iter().any(|e| {
e.get("action").and_then(|v| v.as_str()) == Some("skipped")
&& e.get("errorCode").and_then(|v| v.as_str()) == Some("lock_broken")
});
assert!(
has_lock_broken,
"apply --break-lock should emit a lock_broken skipped event.\nstdout:\n{stdout}"
);
}
#[test]
fn lock_timeout_waits_then_reports_held() {
let dir = tempfile::tempdir().unwrap();
let socket_dir = dir.path().join(".socket");
setup_socket_dir(&socket_dir);
let _external = take_external_lock(&socket_dir);
let start = std::time::Instant::now();
let (code, stdout, _stderr) = run(dir.path(), &["apply", "--json", "--lock-timeout=1"]);
let elapsed = start.elapsed();
assert_eq!(code, 1);
let env = parse_json_envelope(&stdout);
assert_eq!(envelope_error_code(&env), Some("lock_held"));
assert!(
elapsed >= Duration::from_millis(700),
"expected at least ~700ms wait under --lock-timeout=1, got {:?}",
elapsed
);
}
#[allow(dead_code)]
fn _compile_witness() -> Duration {
Duration::from_secs(0)
}