test(jimeng): cover poll state machine edges

Exercise the decision logic and async driver beyond the happy path:
- decide() consults the business code before data.status (fatal and
  retryable codes win over a bogus done status)
- not_found is terminal
- exponential backoff grows 1,2,4,8 and caps at 8x the interval
- backoff multiplier resets after a normal generating wait
- a retryable code followed by a fatal one stops the loop
This commit is contained in:
boos
2026-06-23 14:22:43 +08:00
parent d956b3f9f4
commit f1265d863a
+118
View File
@@ -349,4 +349,122 @@ mod tests {
.unwrap_err();
assert!(matches!(err, JimengError::Cancelled));
}
/// A non-success code carrying a (bogus) `done` status in `data`. Used to
/// prove `decide` consults the business code *before* `data.status`.
fn err_code_with_done_status(code: i64) -> GetResultResponse {
GetResultResponse {
code,
message: Some("m".into()),
data: Some(GetResultData {
status: Some("done".into()),
image_urls: Some(vec!["https://x/should-be-ignored.png".into()]),
binary_data_base64: None,
video_url: Some("https://x/should-be-ignored.mp4".into()),
}),
}
}
#[test]
fn decide_checks_code_before_status() {
// Fatal code wins over a "done" status: must Fail, never Done.
assert!(matches!(
decide(JimengCapability::Image, &err_code_with_done_status(50411)),
PollDecision::Fail(JimengError::Api { code: 50411, .. })
));
// Retryable code wins over a "done" status: must Backoff, never Done.
assert_eq!(
decide(JimengCapability::Video, &err_code_with_done_status(50429)),
PollDecision::Backoff(50429)
);
}
#[test]
fn decide_not_found_is_terminal() {
assert!(matches!(
decide(JimengCapability::Video, &resp(10000, Some("not_found"), None)),
PollDecision::Fail(JimengError::NotFound)
));
}
/// Driver variant that records every sleep duration instead of sleeping, so
/// backoff math is observable. `interval` is 1s to make the multipliers read
/// as whole seconds.
async fn run_sequence_capturing_sleeps(
capability: JimengCapability,
seq: Vec<GetResultResponse>,
) -> (Result<JimengOutput, JimengError>, Vec<Duration>) {
let idx = RefCell::new(0usize);
let seq = RefCell::new(seq);
let sleeps = RefCell::new(Vec::new());
let out = poll_until_done(
capability,
Duration::from_secs(1),
Duration::from_secs(3600),
|| async {
let mut i = idx.borrow_mut();
let s = seq.borrow();
let item = s.get(*i).cloned().unwrap_or_else(|| s.last().cloned().unwrap());
*i += 1;
Ok(item)
},
|| async { false },
|d| {
sleeps.borrow_mut().push(d);
async {}
},
)
.await;
(out, sleeps.into_inner())
}
#[tokio::test]
async fn driver_backoff_grows_exponentially_and_caps() {
// Five retryable codes then done: delays double (1,2,4,8) and cap at 8.
let seq = vec![
resp(50429, None, None),
resp(50429, None, None),
resp(50429, None, None),
resp(50429, None, None),
resp(50429, None, None),
resp(10000, Some("done"), Some("https://x/v.mp4")),
];
let (out, sleeps) = run_sequence_capturing_sleeps(JimengCapability::Video, seq).await;
assert!(out.is_ok());
let secs: Vec<u64> = sleeps.iter().map(Duration::as_secs).collect();
assert_eq!(secs, vec![1, 2, 4, 8, 8]);
}
#[tokio::test]
async fn driver_backoff_resets_after_a_normal_wait() {
// retryable, retryable, then a normal Wait (generating) must reset the
// multiplier back to 1 before the next retryable backs off afresh.
let seq = vec![
resp(50429, None, None), // backoff -> sleep 1, mult 1->2
resp(50429, None, None), // backoff -> sleep 2, mult 2->4
resp(10000, Some("generating"), None), // Wait -> sleep 1, mult reset to 1
resp(50429, None, None), // backoff -> sleep 1, mult 1->2
resp(10000, Some("done"), Some("https://x/v.mp4")),
];
let (out, sleeps) = run_sequence_capturing_sleeps(JimengCapability::Video, seq).await;
assert!(out.is_ok());
let secs: Vec<u64> = sleeps.iter().map(Duration::as_secs).collect();
assert_eq!(secs, vec![1, 2, 1, 1]);
}
#[tokio::test]
async fn driver_retryable_then_fatal_stops() {
// A transient code may be followed by a fatal one; the fatal must end
// the loop with its Api error rather than retrying forever.
let seq = vec![
resp(50429, None, None), // retryable QPS
resp(50411, None, None), // fatal input moderation
];
let (out, _sleeps) =
run_sequence_capturing_sleeps(JimengCapability::Image, seq).await;
assert!(matches!(
out,
Err(JimengError::Api { code: 50411, retryable: false, .. })
));
}
}