use crate::brain::rsi_sync::{
FetchError, FileSyncResult, cause_chain, retry_fetch, summarize_sync_failures,
};
#[test]
fn transport_error_is_retryable() {
let err = FetchError::Transport {
detail: "dns error: failed to lookup address information".to_string(),
};
assert!(
err.is_retryable(),
"a fetch that never reached the server must be retried once"
);
}
#[test]
fn body_error_is_retryable() {
let err = FetchError::Body {
detail: "connection closed before message completed".to_string(),
};
assert!(
err.is_retryable(),
"a body cut short is a transport problem, not an answer"
);
}
#[test]
fn status_error_is_not_retryable() {
let err = FetchError::Status { code: 404 };
assert!(
!err.is_retryable(),
"a 404 is the server's answer; retrying only doubles the wait"
);
}
#[test]
fn status_error_describe_names_the_code_and_the_next_step() {
let msg = FetchError::Status { code: 404 }.describe();
assert!(
msg.contains("HTTP 404"),
"the status code must survive into the message: {msg}"
);
assert!(
!msg.contains(".md"),
"the cause must not name a file: every file that hit it shares this \
text, and the collapse names them once: {msg}"
);
assert!(
msg.contains("code fix"),
"the user needs to know a retry will not help: {msg}"
);
}
#[test]
fn transport_describe_names_the_cause_and_the_retry() {
let msg = FetchError::Transport {
detail: "dns error: failed to lookup address information".to_string(),
}
.describe();
assert!(
msg.contains("dns error"),
"the inner cause must survive into the message: {msg}"
);
assert!(
msg.contains("retries"),
"the user needs to know the next cycle retries: {msg}"
);
}
#[derive(Debug)]
struct Leaf(&'static str);
impl std::fmt::Display for Leaf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for Leaf {}
#[derive(Debug)]
struct Wrapper(&'static str, Leaf);
impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for Wrapper {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.1)
}
}
#[test]
fn cause_chain_names_the_inner_error() {
let err = Wrapper("error sending request for url (...)", Leaf("dns error"));
let chain = cause_chain(&err);
assert!(
chain.contains("error sending request"),
"the wrapper is kept so the context is not lost: {chain}"
);
assert!(
chain.contains("dns error"),
"the inner cause is the whole point of walking source(): {chain}"
);
}
#[test]
fn cause_chain_does_not_repeat_a_self_describing_error() {
let err = Leaf("connection refused");
let chain = cause_chain(&err);
assert_eq!(
chain, "connection refused",
"an error with no source must not be printed twice"
);
}
async fn drive(
fail_times: u8,
fail_with: impl Fn() -> FetchError + Copy,
) -> (Result<String, FetchError>, u8) {
let mut calls = 0u8;
let result = retry_fetch("AGENTS.md", || {
calls += 1;
let this_call = calls;
let err = fail_with();
async move {
if this_call <= fail_times {
Err(err)
} else {
Ok("body".to_string())
}
}
})
.await;
(result, calls)
}
#[tokio::test]
async fn transport_failure_is_retried_once_and_can_succeed() {
let (result, calls) = drive(1, || FetchError::Transport {
detail: "dns error".to_string(),
})
.await;
assert!(
result.is_ok(),
"the second attempt must be allowed to succeed: {result:?}"
);
assert_eq!(
calls, 2,
"a transport failure gets exactly one retry, not more"
);
}
#[tokio::test]
async fn transport_failure_is_not_retried_forever() {
let (result, calls) = drive(99, || FetchError::Transport {
detail: "dns error".to_string(),
})
.await;
assert!(
result.is_err(),
"an unreachable host must surface the failure, not loop"
);
assert_eq!(
calls, 2,
"attempts are capped at TEMPLATE_MAX_ATTEMPTS so an outage is not a longer wall"
);
}
#[tokio::test]
async fn status_failure_is_not_retried() {
let (result, calls) = drive(99, || FetchError::Status { code: 404 }).await;
assert!(
matches!(result, Err(FetchError::Status { code: 404 })),
"the status must survive to the caller: {result:?}"
);
assert_eq!(
calls, 1,
"a 404 is the server's answer, so one attempt is the whole story"
);
}
#[tokio::test]
async fn body_failure_is_retried_once() {
let (result, calls) = drive(1, || FetchError::Body {
detail: "connection closed before message completed".to_string(),
})
.await;
assert!(
result.is_ok(),
"a body cut short must be retried: {result:?}"
);
assert_eq!(calls, 2, "body failures are retryable, once");
}
fn failed(filename: &str, cause: &str) -> FileSyncResult {
FileSyncResult {
filename: filename.to_string(),
synced: false,
sections_added: 0,
error: Some(cause.to_string()),
bailed_for_cap: None,
}
}
const DNS_CAUSE: &str = "could not reach the template host (dns error) after 2 attempts; \
check the network or proxy, the next RSI cycle retries";
const HTTP_CAUSE: &str = "upstream answered HTTP 404; the tracked path is stale, which \
needs a code fix rather than a retry";
#[test]
fn sync_failures_collapse_to_one_line() {
let results: Vec<FileSyncResult> = ["AGENTS.md", "TOOLS.md", "SOUL.md", "USER.md"]
.iter()
.map(|f| failed(f, DNS_CAUSE))
.collect();
let msg = summarize_sync_failures(&results).expect("failures must produce a line");
assert!(
!msg.contains('\n'),
"the whole point is one line for the user to read: {msg}"
);
assert!(
msg.contains("all 4 tracked files failed"),
"a cause shared by every failure collapses to one sentence with the count: {msg}"
);
assert_eq!(
msg.matches("dns error").count(),
1,
"the cause must appear once, not once per file: {msg}"
);
assert!(
!msg.contains("AGENTS.md: could not reach the template host for AGENTS.md"),
"the old join named the file twice; the cause must carry no filename: {msg}"
);
assert!(
msg.contains("retries"),
"the collapsed line must keep the next step: {msg}"
);
}
#[test]
fn distinct_causes_get_one_clause_each() {
let results = vec![
failed("AGENTS.md", DNS_CAUSE),
failed("TOOLS.md", DNS_CAUSE),
failed("SOUL.md", HTTP_CAUSE),
];
let msg = summarize_sync_failures(&results).expect("failures must produce a line");
assert!(
!msg.contains('\n'),
"distinct causes still render on one line: {msg}"
);
assert!(
msg.contains("2 (AGENTS.md, TOOLS.md) failed:"),
"a shared cause names its files once with the count: {msg}"
);
assert!(
msg.contains("1 (SOUL.md) failed:"),
"each distinct cause gets its own clause: {msg}"
);
assert_eq!(
msg.matches("HTTP 404").count(),
1,
"a cause is never repeated per file: {msg}"
);
}
#[test]
fn single_failure_names_its_file_once() {
let msg = summarize_sync_failures(&[failed("AGENTS.md", DNS_CAUSE)])
.expect("one failure must produce a line");
assert!(
msg.starts_with("AGENTS.md failed:"),
"with one failure there is no count worth naming, just the file: {msg}"
);
assert_eq!(
msg.matches("AGENTS.md").count(),
1,
"the file must be named exactly once: {msg}"
);
}
#[test]
fn clean_sync_emits_no_failure_line() {
let results = vec![FileSyncResult {
filename: "AGENTS.md".to_string(),
synced: true,
sections_added: 3,
error: None,
bailed_for_cap: None,
}];
assert!(
summarize_sync_failures(&results).is_none(),
"a clean pass must not emit a TemplateSyncFailed notification"
);
}
const SOURCE: &str = include_str!("../brain/rsi_sync.rs");
fn fn_body(name: &str) -> &'static str {
let needle = format!("fn {name}(");
let start = SOURCE
.find(&needle)
.unwrap_or_else(|| panic!("{name} must exist in rsi_sync.rs"));
let rest = &SOURCE[start..];
let end = rest
.find("\n}\n")
.unwrap_or_else(|| panic!("{name} must end with a closing brace at column 0"));
&rest[..end + 3]
}
#[test]
fn fetch_error_names_the_cause() {
let transport = FetchError::Transport {
detail: "dns error: failed to lookup address information".to_string(),
};
let msg = transport.describe();
assert!(
msg.contains("dns error: failed to lookup address information"),
"the cause itself must reach the user, not just reqwest's outer wrapper: {msg}"
);
assert!(
!msg.contains(".md"),
"the cause carries no filename; the join names the files once: {msg}"
);
let body = FetchError::Body {
detail: "connection closed before message completed".to_string(),
};
assert!(
body.describe()
.contains("connection closed before message completed"),
"a body failure must name what went wrong, not only that it did"
);
}
#[test]
fn one_fetch_per_tracked_file() {
let body = fn_body("sync_single_file");
assert!(
!body.contains("fetch_template("),
"sync_single_file must consume the body fetched by its caller; a second fetch here is the duplicate request #2169 removed"
);
assert!(
body.contains("upstream"),
"sync_single_file receives the already-fetched body as a parameter"
);
assert_eq!(
SOURCE.matches("fetch_template(").count(),
2,
"exactly one definition and one call site; a second call site doubles the requests per pass"
);
}
#[test]
fn template_client_has_timeouts() {
let body = fn_body("template_client");
assert!(
body.contains(".connect_timeout(TEMPLATE_CONNECT_TIMEOUT)"),
"a connect timeout is what stops a blackholed SYN from hanging forever"
);
assert!(
body.contains(".timeout(TEMPLATE_REQUEST_TIMEOUT)"),
"a whole-call timeout is what stops a stalled body from wedging the RSI engine"
);
assert!(
!SOURCE.contains("reqwest::get("),
"a bare reqwest::get has no timeout at all, which is the defect #2169 reports"
);
}