use super::*;
use crate::mirror::FILE;
fn now() -> chrono::DateTime<chrono::Utc> {
chrono::Utc::now()
}
fn attempted(seconds: i64, error: Option<&str>) -> SyncState {
let when = (now() - chrono::Duration::seconds(seconds)).to_rfc3339();
SyncState {
attempted_at: Some(when.clone()),
synced_at: Some(when),
last_error: error.map(str::to_string),
..Default::default()
}
}
fn mirror() -> (tempfile::TempDir, Mirror) {
let dir = tempfile::tempdir().expect("a scratch directory");
let mirror = Mirror::open(&dir.path().join(FILE)).expect("a mirror");
(dir, mirror)
}
fn issue(number: i64) -> Document {
Document::of(
number.to_string(),
serde_json::json!({
"number": number,
"title": "an issue",
"state": "open",
"updated_at": "2026-01-01T00:00:00Z",
}),
)
}
fn checks_of(repo: &str, commit: &str) -> Scope {
Scope::Checks {
repo: repo.to_string(),
commit: commit.to_string(),
}
}
fn targets(repos: &[(&str, &[&str])], boards: &[&str]) -> Targets {
Targets {
repositories: repos
.iter()
.map(|(repo, commits)| {
(
repo.to_string(),
commits.iter().map(|c| c.to_string()).collect(),
)
})
.collect(),
boards: boards.iter().map(|b| b.parse().expect("a board")).collect(),
problems: Vec::new(),
}
}
fn board(id: &str, owner: &str, number: i64) -> Document {
Document::of(
id,
serde_json::json!({
"id": id, "number": number, "title": format!("board {number}"),
"url": format!("https://github.com/orgs/{owner}/projects/{number}"),
"short_description": null, "closed": false,
"updated_at": "2026-01-01T00:00:00Z", "owner": owner, "fields": [],
}),
)
}
#[test]
fn test10009_a_fresh_scope_is_not_re_asked() {
let issues = Scope::Issues("owner/repo".to_string());
assert!(
!due(&issues, &attempted(5, None), now()),
"a scope asked five seconds ago is current"
);
assert!(
due(&issues, &attempted(SWEEP.as_secs() as i64 + 5, None), now()),
"a scope older than the sweep is due"
);
assert!(due(&issues, &SyncState::default(), now()));
}
#[test]
fn test10039_a_scope_that_cannot_be_conditional_is_asked_less_often() {
let issues = Scope::Issues("owner/repo".to_string());
let checks = checks_of("owner/repo", "abc");
let traffic = Scope::Traffic("owner/repo".to_string());
let cards = Scope::ProjectItems("PVT_1".to_string());
assert_eq!(interval(&issues), SWEEP);
assert_eq!(interval(&checks), SWEEP_UNCONDITIONAL);
assert_eq!(interval(&cards), SWEEP_UNCONDITIONAL);
assert_eq!(interval(&traffic), SWEEP_TRAFFIC);
assert!(
SWEEP < SWEEP_UNCONDITIONAL && SWEEP_UNCONDITIONAL < SWEEP_TRAFFIC,
"the intervals must be ordered by what an ask costs"
);
let three_minutes = attempted(180, None);
assert!(due(&issues, &three_minutes, now()));
assert!(!due(&checks, &three_minutes, now()));
assert!(!due(&traffic, &three_minutes, now()));
let an_hour = attempted(3600, None);
assert!(due(&checks, &an_hour, now()));
assert!(!due(&traffic, &an_hour, now()));
assert!(due(&traffic, &attempted(SWEEP_TRAFFIC.as_secs() as i64 + 60, None), now()));
}
#[test]
fn test10010_a_failed_scope_waits_longer_before_being_retried() {
let scope = Scope::Gists;
assert!(
!due(&scope, &attempted(SWEEP.as_secs() as i64 + 5, Some("status 403")), now()),
"a failing scope must not be retried at sweep speed"
);
assert!(
due(&scope, &attempted(AFTER_FAILURE.as_secs() as i64 + 5, Some("status 403")), now()),
"it is retried once the back-off is over"
);
}
#[tokio::test]
async fn test10011_a_departed_repository_is_forgotten() {
let (_dir, mirror) = mirror();
let kept = Scope::Issues("owner/kept".to_string());
let gone = Scope::Issues("owner/gone".to_string());
let gone_releases = Scope::Releases("owner/gone".to_string());
for scope in [&kept, &gone, &gone_releases] {
mirror.put(scope, &[issue(1)]).expect("stored");
mirror.synced(scope, None, None).expect("recorded");
}
let report = sync(&mirror, None, &targets(&[("owner/kept", &[])], &[]), false).await;
assert!(mirror.documents(&gone).expect("read").is_empty());
assert!(mirror.documents(&gone_releases).expect("read").is_empty(), "every kind goes");
assert!(
report.changed.contains(&gone.key()) && report.changed.contains(&gone_releases.key()),
"the readers must be told what went: {report:?}"
);
assert!(!report.changed.contains(&kept.key()), "{report:?}");
assert_eq!(
mirror.documents(&kept).expect("read").len(),
1,
"a repository that is still here must be left alone"
);
}
#[tokio::test]
async fn test10012_no_token_is_not_a_failure_of_any_scope() {
let (_dir, mirror) = mirror();
let issues = Scope::Issues("owner/repo".to_string());
mirror.put(&issues, &[issue(1)]).expect("stored");
mirror.synced(&issues, Some("etag"), None).expect("recorded");
mirror.put(&Scope::Gists, &[Document::of("g", serde_json::json!({"id":"g","files":{}}))]).expect("stored");
let report = sync(&mirror, None, &targets(&[("owner/repo", &[])], &[]), true).await;
assert!(report.no_token, "the pass must say there was no token");
assert!(report.failed.is_empty(), "no scope failed — none was asked: {report:?}");
assert!(!report.touched_anything(), "nothing was fetched, so nothing changed: {report:?}");
assert_eq!(mirror.documents(&issues).expect("read").len(), 1);
assert_eq!(mirror.documents(&Scope::Gists).expect("read").len(), 1);
assert_eq!(mirror.state(&issues).expect("read").etag.as_deref(), Some("etag"));
}
#[tokio::test]
async fn nothing_is_forgotten_while_a_project_could_not_be_read() {
let (_dir, mirror) = mirror();
let issues = Scope::Issues("owner/unread".to_string());
mirror.put(&issues, &[issue(1)]).expect("stored");
mirror.synced(&issues, None, None).expect("recorded");
let mut unread = targets(&[], &[]);
unread.problems.push("alpha/fix: git could not read the commit".to_string());
let report = sync(&mirror, None, &unread, false).await;
assert_eq!(
mirror.documents(&issues).expect("read").len(),
1,
"a repository was forgotten because its project could not be read"
);
assert_eq!(report.problems, unread.problems, "the problem must be reported");
}
#[tokio::test]
async fn checks_follow_the_commits_worktrees_are_on() {
let (_dir, mirror) = mirror();
let current = checks_of("owner/repo", "c1");
let left = checks_of("owner/repo", "c0");
for scope in [¤t, &left] {
mirror
.put(scope, &[Document::of(SINGLETON, serde_json::json!({"state":"success"}))])
.expect("stored");
mirror.synced(scope, None, None).expect("recorded");
}
let wanted_now = targets(&[("owner/repo", &["c1"])], &[]);
assert!(
wanted(&mirror, &wanted_now).expect("read").contains(¤t),
"the commit a worktree is on must be asked about"
);
sync(&mirror, None, &wanted_now, false).await;
assert!(mirror.only(¤t).expect("read").is_some());
assert!(
mirror.only(&left).expect("read").is_none(),
"CI for a commit no worktree is on any more was kept"
);
}
#[tokio::test]
async fn cards_follow_the_boards_projects_reference() {
let (_dir, mirror) = mirror();
let lists = Scope::Projects("machinefabric".to_string());
mirror
.put(&lists, &[board("PVT_7", "machinefabric", 7), board("PVT_8", "machinefabric", 8)])
.expect("stored");
mirror.synced(&lists, None, None).expect("recorded");
for id in ["PVT_7", "PVT_8"] {
let cards = Scope::ProjectItems(id.to_string());
mirror.put(&cards, &[Document::of("PVTI_1", serde_json::json!({"id":"PVTI_1"}))]).expect("stored");
mirror.synced(&cards, None, None).expect("recorded");
}
let referencing_7 = targets(&[], &["machinefabric/7"]);
let (scopes, problems) = board_scopes(&mirror, &referencing_7).expect("resolved");
assert_eq!(scopes, vec![Scope::ProjectItems("PVT_7".to_string())]);
assert!(problems.is_empty(), "{problems:?}");
sync(&mirror, None, &referencing_7, false).await;
assert_eq!(mirror.documents(&Scope::ProjectItems("PVT_7".to_string())).expect("read").len(), 1);
assert!(
mirror.documents(&Scope::ProjectItems("PVT_8".to_string())).expect("read").is_empty(),
"the cards of a board nobody references were kept"
);
}
#[tokio::test]
async fn an_unresolvable_board_is_reported_and_forgets_nothing() {
let (_dir, mirror) = mirror();
let lists = Scope::Projects("machinefabric".to_string());
mirror.put(&lists, &[board("PVT_7", "machinefabric", 7)]).expect("stored");
mirror.synced(&lists, None, None).expect("recorded");
let held = Scope::ProjectItems("PVT_7".to_string());
mirror.put(&held, &[Document::of("PVTI_1", serde_json::json!({"id":"PVTI_1"}))]).expect("stored");
let report = sync(
&mirror,
None,
&targets(&[], &["machinefabric/7", "machinefabric/9", "someone/1"]),
false,
)
.await;
assert!(
report.problems.iter().any(|p| p == "machinefabric has no board 9"),
"{report:?}"
);
assert!(
report.problems.iter().any(|p| p.starts_with("someone/1: someone's boards have not been read")),
"{report:?}"
);
assert_eq!(mirror.documents(&held).expect("read").len(), 1);
}
#[test]
fn a_reports_failures_are_named() {
let report = SyncReport {
failed: vec![("issues:o/r".to_string(), "status 403".to_string())],
..SyncReport::default()
};
let written = serde_json::to_value(&report).expect("serialises");
assert_eq!(
written["failed"],
serde_json::json!([{ "scope": "issues:o/r", "error": "status 403" }])
);
assert_eq!(written["no_token"], false);
}