use super::*;
use crate::work_item::WorkKind;
fn item(branch: &str) -> WorkItem {
return WorkItem {
kind: WorkKind::Push,
repository: String::from("/srv/repo"),
git_dir: String::from("/srv/repo/.git"),
branch_ref: branch.to_owned(),
oid: String::from("0123456789abcdef0123456789abcdef01234567"),
parent: String::new(),
created: String::from("2026-11-03T05:40:00.000Z"),
tries: 0,
};
}
#[test]
fn the_pauses_double_and_stop_at_the_longest() {
let first: Duration = Duration::from_secs(10);
let pauses: Vec<Duration> = (1..=4).map(|attempt| { return pause_before(attempt, first); }).collect();
assert_eq!(pauses, vec![first, first * 2, first * 4, first * 8]);
assert_eq!(pause_before(1_000, first), LONGEST_PAUSE);
assert_eq!(pause_before(u32::MAX, first), LONGEST_PAUSE);
assert!(pause_before(0, first) == first, "a try number of zero is the first");
}
#[test]
fn the_variable_sets_the_first_pause() {
assert_eq!(first_pause_from(None), FIRST_PAUSE);
assert_eq!(first_pause_from(Some(std::ffi::OsStr::new("3"))), Duration::from_secs(3));
for unusable in ["0", "-1", "1.5", "soon", ""] {
assert_eq!(first_pause_from(Some(std::ffi::OsStr::new(unusable))), FIRST_PAUSE, "{unusable:?}");
}
assert_eq!(first_pause_from(Some(std::ffi::OsStr::new("99999"))), LONGEST_PAUSE);
}
#[test]
fn a_plan_is_per_branch_and_due_after_its_pause() {
let mut retries: PushRetries = PushRetries::with_first_pause(Duration::from_secs(10));
let now: Instant = Instant::now();
let here: &Path = Path::new("/srv/repo/.git");
let elsewhere: &Path = Path::new("/srv/other/.git");
assert_eq!(retries.schedule(here, &item("refs/heads/main"), 1, now), Duration::from_secs(10));
assert_eq!(retries.schedule(here, &item("refs/heads/main"), 2, now), Duration::from_secs(20), "the same branch has one plan");
retries.schedule(here, &item("refs/heads/topic"), 1, now);
assert_eq!(retries.next_wait(here, now), Some(Duration::from_secs(10)));
assert_eq!(retries.next_wait(elsewhere, now), None);
assert!(retries.take_due(here, now + Duration::from_secs(9)).is_empty());
assert!(retries.take_due(elsewhere, now + Duration::from_secs(60)).is_empty());
let due: Vec<Due> = retries.take_due(here, now + Duration::from_secs(10));
assert_eq!(due.len(), 1);
assert_eq!(due[0].item.branch_ref, "refs/heads/topic");
retries.forget(here, "refs/heads/main");
assert_eq!(retries.next_wait(here, now), None);
}
#[test]
fn only_a_conflict_needs_the_person() {
assert!(needs_the_person("cli-git: auto-push: not published: merging origin/main into main conflicts in: a.txt, b.txt. Nothing was pushed"));
for output in [
"cli-git: git push made no progress for 30 seconds and was stopped (limit=stall, bytes_moved=0, elapsed_seconds=30)",
"cli-git: git push ran for 1800 seconds and was stopped (limit=ceiling)",
"fatal: unable to access 'https://example.invalid/': Could not resolve host: example.invalid",
"",
] {
assert!(!needs_the_person(output), "{output}");
}
}
const PRE_RECEIVE_DECLINED: &str = "remote: protected: pushes to main need a review \nTo /srv/remote.git\n ! [remote rejected] main -> main (pre-receive hook declined)\nerror: failed to push some refs to '/srv/remote.git'\n";
const UPDATE_DECLINED: &str = "remote: update says no \nremote: error: hook declined to update refs/heads/main \nTo /srv/remote.git\n ! [remote rejected] main -> main (hook declined)\nerror: failed to push some refs to '/srv/remote.git'\n";
const PROTECTED_BRANCH_DECLINED: &str = " ! [remote rejected] main -> main (protected branch hook declined)\n";
const AUTHENTICATION_FAILED: &str = "remote: login needed\nfatal: Authentication failed for 'http://127.0.0.1:39197/r.git/'\n";
const PROMPTS_DISABLED: &str = "fatal: could not read Username for 'http://127.0.0.1:39197': terminal prompts disabled\n";
const FORBIDDEN: &str = "remote: forbidden\nfatal: unable to access 'http://127.0.0.1:39871/r.git/': The requested URL returned error: 403\n";
const SSH_REFUSED: &str = "user@127.0.0.1: Permission denied (publickey).\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n";
const CONNECTION_REFUSED: &str = "fatal: unable to access 'http://127.0.0.1:1/r.git/': Failed to connect to 127.0.0.1:1 after 0 ms: Could not connect to server\n";
const UNRESOLVED: &str = "fatal: unable to access 'http://nonexistent.invalid/r.git/': Could not resolve host: nonexistent.invalid\n";
#[test]
fn an_authentication_failure_is_not_tried_again() {
for output in [AUTHENTICATION_FAILED, PROMPTS_DISABLED, FORBIDDEN, SSH_REFUSED] {
assert_eq!(final_failure(output), Some(FinalFailure::Login), "{output}");
assert!(needs_the_person(output), "{output}");
}
}
#[test]
fn a_server_hook_decline_is_not_tried_again() {
for output in [PRE_RECEIVE_DECLINED, UPDATE_DECLINED, PROTECTED_BRANCH_DECLINED] {
assert_eq!(final_failure(output), Some(FinalFailure::Declined), "{output}");
assert_eq!(unrecognised_rejection(output), None, "a decline that is known is not reported as unknown: {output}");
}
}
#[test]
fn an_offline_failure_is_still_tried_again() {
for output in [
CONNECTION_REFUSED,
UNRESOLVED,
"cli-git: git push made no progress for 30 seconds and was stopped (limit=stall, bytes_moved=0, elapsed_seconds=30)",
] {
assert_eq!(final_failure(output), None, "{output}");
}
}
#[test]
fn a_rejection_nobody_knows_is_retried_and_named() {
let output: &str = "To /srv/remote.git\n ! [remote rejected] main -> main (prohibited by the review system)\nerror: failed to push some refs\n";
assert_eq!(final_failure(output), None);
assert_eq!(unrecognised_rejection(output).as_deref(), Some("! [remote rejected] main -> main (prohibited by the review system)"));
assert_eq!(unrecognised_rejection(CONNECTION_REFUSED), None, "a failure that is not a rejection names nothing");
}