tests/lease_server_test.rs
Ref: Size: 13.1 KiB History
mod common;
use std::process::Output;
use common::ServerHarness;
fn stdout_json(output: &Output) -> serde_json::Value {
let text = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(text.trim()).unwrap_or_else(|e| {
panic!(
"expected JSON on stdout, got {:?} (stderr: {})\nparse error: {e}",
text,
String::from_utf8_lossy(&output.stderr)
)
})
}
fn code(output: &Output) -> i32 {
output.status.code().unwrap_or(-1)
}
/// A harness with one open issue pushed to the server, returning its full id.
/// Leases are keyed on the issue id, and `acquire` resolves it against the
/// server's copy of `refs/collab/*` — so the refs have to be there first.
fn harness_with_issue(name: &str) -> (ServerHarness, String) {
let harness = ServerHarness::new(name);
harness.push_head();
let (_ref_name, id) = common::open_issue(
&harness.work_repo_git2(),
&common::alice(),
"parser chokes on empty input",
);
harness.push_collab_refs();
(harness, id)
}
#[test]
fn acquire_open_issue_succeeds() {
let (harness, id) = harness_with_issue("lease-acquire");
let out = harness.ssh_exec(&format!(
"collab-lease acquire 'lease-acquire.git' '{}'",
id
));
assert_eq!(
code(&out),
0,
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let json = stdout_json(&out);
assert_eq!(json["status"], "acquired");
assert_eq!(json["issue"], id);
assert_eq!(json["token"], 1);
// No --ttl: an open-ended lease, i.e. a human assignment.
assert_eq!(json["expires_at"], serde_json::Value::Null);
}
#[test]
fn acquire_with_ttl_reports_expiry() {
let (harness, id) = harness_with_issue("lease-ttl");
let out = harness.ssh_exec(&format!(
"collab-lease acquire 'lease-ttl.git' '{}' --ttl 300",
id
));
assert_eq!(code(&out), 0);
let json = stdout_json(&out);
let expires = json["expires_at"].as_str().expect("expires_at set");
assert!(
expires.ends_with('Z') && expires.contains('T'),
"expected an RFC3339 instant, got {expires}"
);
}
#[test]
fn acquire_by_issue_id_prefix_reports_full_id() {
let (harness, id) = harness_with_issue("lease-prefix");
let out = harness.ssh_exec(&format!(
"collab-lease acquire 'lease-prefix.git' '{}'",
&id[..8]
));
assert_eq!(code(&out), 0);
// The reply carries the full id, so a client that claimed by prefix can
// renew and release without re-resolving.
assert_eq!(stdout_json(&out)["issue"], id);
}
#[test]
fn acquire_conflict_reports_holder_with_exit_4() {
let (harness, id) = harness_with_issue("lease-conflict");
let first = harness.ssh_client_key();
let second = harness.second_authorized_key();
let mine = harness.ssh_exec_as(
&first,
&format!(
"collab-lease acquire 'lease-conflict.git' '{}' --ttl 300",
id
),
b"",
);
assert_eq!(code(&mine), 0);
let my_holder = stdout_json(&mine)["holder"].as_str().unwrap().to_string();
let theirs = harness.ssh_exec_as(
&second,
&format!(
"collab-lease acquire 'lease-conflict.git' '{}' --ttl 300",
id
),
b"",
);
assert_eq!(
code(&theirs),
4,
"a lost race must exit 4, not 1: stdout {} stderr {}",
String::from_utf8_lossy(&theirs.stdout),
String::from_utf8_lossy(&theirs.stderr)
);
let json = stdout_json(&theirs);
assert_eq!(json["status"], "held");
assert_eq!(json["holder"], my_holder);
}
#[test]
fn reacquire_by_same_key_is_idempotent() {
let (harness, id) = harness_with_issue("lease-idem");
let cmd = format!("collab-lease acquire 'lease-idem.git' '{}' --ttl 300", id);
let first = harness.ssh_exec(&cmd);
let second = harness.ssh_exec(&cmd);
assert_eq!(code(&first), 0);
assert_eq!(code(&second), 0, "re-acquire by the holder must succeed");
assert_eq!(stdout_json(&first)["token"], stdout_json(&second)["token"]);
}
#[test]
fn renew_extends_and_wrong_key_exits_4() {
let (harness, id) = harness_with_issue("lease-renew");
let first = harness.ssh_client_key();
let second = harness.second_authorized_key();
harness.ssh_exec_as(
&first,
&format!("collab-lease acquire 'lease-renew.git' '{}' --ttl 60", id),
b"",
);
let mine = harness.ssh_exec_as(
&first,
&format!("collab-lease renew 'lease-renew.git' '{}' --ttl 600", id),
b"",
);
assert_eq!(code(&mine), 0);
assert_eq!(stdout_json(&mine)["status"], "renewed");
let theirs = harness.ssh_exec_as(
&second,
&format!("collab-lease renew 'lease-renew.git' '{}' --ttl 600", id),
b"",
);
assert_eq!(code(&theirs), 4);
assert_eq!(stdout_json(&theirs)["status"], "not-holder");
}
#[test]
fn release_frees_the_issue_and_bumps_the_next_tenure() {
let (harness, id) = harness_with_issue("lease-release");
let first = harness.ssh_client_key();
let second = harness.second_authorized_key();
harness.ssh_exec_as(
&first,
&format!(
"collab-lease acquire 'lease-release.git' '{}' --ttl 300",
id
),
b"",
);
// Someone else's live lease is not theirs to drop.
let refused = harness.ssh_exec_as(
&second,
&format!("collab-lease release 'lease-release.git' '{}'", id),
b"",
);
assert_eq!(code(&refused), 4);
let released = harness.ssh_exec_as(
&first,
&format!("collab-lease release 'lease-release.git' '{}'", id),
b"",
);
assert_eq!(code(&released), 0);
assert_eq!(stdout_json(&released)["status"], "released");
let next = harness.ssh_exec_as(
&second,
&format!(
"collab-lease acquire 'lease-release.git' '{}' --ttl 300",
id
),
b"",
);
assert_eq!(code(&next), 0);
// A new tenure, so the fencing token moves on.
assert_eq!(stdout_json(&next)["token"], 2);
}
#[test]
fn release_without_a_lease_succeeds_and_says_not_held() {
let (harness, id) = harness_with_issue("lease-release-noop");
// Idempotent: a client retrying after a dropped connection must not fail.
let out = harness.ssh_exec(&format!(
"collab-lease release 'lease-release-noop.git' '{}'",
id
));
assert_eq!(code(&out), 0);
// This assertion used to read "released", which is what let the prefix bug
// of 2026-09-06 pass every test: a release that freed nothing reported the
// same status as one that freed a lease, so nothing could tell them apart.
assert_eq!(stdout_json(&out)["status"], "not-held");
}
#[test]
fn list_shows_live_leases() {
let (harness, id) = harness_with_issue("lease-list");
let empty = harness.ssh_exec("collab-lease list 'lease-list.git'");
assert_eq!(code(&empty), 0);
assert_eq!(stdout_json(&empty)["leases"].as_array().unwrap().len(), 0);
harness.ssh_exec(&format!(
"collab-lease acquire 'lease-list.git' '{}' --ttl 300",
id
));
let out = harness.ssh_exec("collab-lease list 'lease-list.git'");
let rows = stdout_json(&out)["leases"].as_array().unwrap().clone();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["issue"], id);
assert_eq!(rows[0]["token"], 1);
}
#[test]
fn acquire_unknown_issue_fails() {
let (harness, _id) = harness_with_issue("lease-unknown");
let out = harness.ssh_exec("collab-lease acquire 'lease-unknown.git' 'deadbeef'");
assert_eq!(code(&out), 1);
assert!(
String::from_utf8_lossy(&out.stdout).contains("issue not found"),
"stdout: {}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn acquire_closed_issue_is_refused() {
let harness = ServerHarness::new("lease-closed");
harness.push_head();
let repo = harness.work_repo_git2();
let (ref_name, id) = common::open_issue(&repo, &common::alice(), "already handled");
common::close_issue(&repo, &ref_name, &common::alice());
harness.push_collab_refs();
let out = harness.ssh_exec(&format!("collab-lease acquire 'lease-closed.git' '{}'", id));
assert_eq!(code(&out), 1);
assert!(
String::from_utf8_lossy(&out.stdout).contains("closed"),
"stdout: {}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn unknown_repo_and_unauthorized_repo_answer_identically() {
let (harness, id) = harness_with_issue("lease-policy");
let unknown = harness.ssh_exec(&format!("collab-lease acquire 'no-such-repo.git' '{}'", id));
// Readable but not writable: acquiring is a write.
harness.write_repo_server_policy(
"visibility = \"public\"\n[access]\nread = [\"*\"]\nwrite = []\n",
);
let unauthorized =
harness.ssh_exec(&format!("collab-lease acquire 'lease-policy.git' '{}'", id));
assert_eq!(code(&unknown), 1);
assert_eq!(code(&unauthorized), 1);
// The same reply either way, so the error cannot be used to probe which
// private repos exist.
assert_eq!(
String::from_utf8_lossy(&unknown.stdout),
String::from_utf8_lossy(&unauthorized.stdout),
);
assert!(
String::from_utf8_lossy(&unauthorized.stdout).contains("repository not found"),
"stdout: {}",
String::from_utf8_lossy(&unauthorized.stdout)
);
}
#[test]
fn read_only_principal_may_list_but_not_acquire() {
let (harness, id) = harness_with_issue("lease-readonly");
harness.write_repo_server_policy(
"visibility = \"public\"\n[access]\nread = [\"*\"]\nwrite = []\n",
);
let list = harness.ssh_exec("collab-lease list 'lease-readonly.git'");
assert_eq!(code(&list), 0, "listing needs only read");
let acquire = harness.ssh_exec(&format!(
"collab-lease acquire 'lease-readonly.git' '{}'",
id
));
assert_eq!(code(&acquire), 1, "acquiring needs write");
}
/// A claim made by prefix must be releasable by that prefix.
///
/// The regression test for the bug two real workers found on 2026-09-06:
/// lease rows are keyed by the full issue id, `acquire` resolved a prefix to
/// one, and `release` took its argument literally — so releasing by prefix
/// updated a key no row used and reported `released` anyway. The lease stayed
/// held until its TTL, blocking every other worker, and the CLI abbreviates
/// ids everywhere, so this was the ordinary path rather than a corner.
#[test]
fn a_claim_made_by_prefix_is_released_by_that_prefix() {
let (harness, id) = harness_with_issue("lease-prefix-release");
let first = harness.ssh_client_key();
let second = harness.second_authorized_key();
let prefix = &id[..8];
let claimed = harness.ssh_exec_as(
&first,
&format!("collab-lease acquire 'lease-prefix-release.git' '{prefix}' --ttl 300"),
b"",
);
assert_eq!(code(&claimed), 0);
let released = harness.ssh_exec_as(
&first,
&format!("collab-lease release 'lease-prefix-release.git' '{prefix}'"),
b"",
);
assert_eq!(code(&released), 0);
let json = stdout_json(&released);
assert_eq!(json["status"], "released", "a held lease must be freed");
// The reply names the full id, so a caller learns what it actually freed.
assert_eq!(json["issue"], id);
// The proof is not the reply: another worker must be able to take it.
let next = harness.ssh_exec_as(
&second,
&format!("collab-lease acquire 'lease-prefix-release.git' '{prefix}' --ttl 300"),
b"",
);
assert_eq!(
code(&next),
0,
"the issue must really be free, not merely reported free: {}",
String::from_utf8_lossy(&next.stdout)
);
assert_eq!(stdout_json(&next)["token"], 2, "a new tenure");
}
/// A prefix that matches two leases is refused rather than picking one.
#[test]
fn an_ambiguous_prefix_is_refused() {
let harness = ServerHarness::new("lease-ambiguous");
harness.push_head();
let repo = harness.work_repo_git2();
// Issue ids are content-derived, so create enough real issues that two
// must share their first hex character.
let mut ids = Vec::new();
for n in 0..17 {
let (_r, id) = common::open_issue(&repo, &common::alice(), &format!("Issue {n}"));
ids.push(id);
}
harness.push_collab_refs();
let (a, b) = ids
.iter()
.find_map(|a| {
ids.iter()
.find(|b| *b != a && b.as_bytes()[0] == a.as_bytes()[0])
.map(|b| (a.clone(), b.clone()))
})
.expect("17 ids over 16 first hex characters must share one");
for id in [&a, &b] {
let out = harness.ssh_exec(&format!(
"collab-lease acquire 'lease-ambiguous.git' '{id}' --ttl 300"
));
assert_eq!(code(&out), 0);
}
let out = harness.ssh_exec(&format!(
"collab-lease release 'lease-ambiguous.git' '{}'",
&a[..1]
));
assert_eq!(code(&out), 1, "an ambiguous prefix must not pick one");
assert!(
String::from_utf8_lossy(&out.stdout).contains("matches 2 leases"),
"stdout: {}",
String::from_utf8_lossy(&out.stdout)
);
}