use super::*;
use crate::Array;
fn dummy_array() -> Array {
Array::from_slice::<f32>(&[0.0_f32], &[1i32]).unwrap()
}
#[test]
fn new_has_no_obligation() {
let s = SessionRetryState::new();
assert!(!s.has_obligation());
assert!(s.resume_at().is_none());
assert!(s.finalize_queue().is_empty());
}
#[test]
fn enqueue_finalize_creates_obligation() {
let mut s = SessionRetryState::new();
s.enqueue_finalize(dummy_array());
assert!(s.has_obligation());
assert_eq!(s.finalize_queue().len(), 1);
assert!(!s.finalize_queue()[0].fallback_consumed);
}
#[test]
fn stage_stop_encoder_feed_then_clear_all_clears() {
let mut s = SessionRetryState::new();
s.stage_stop_encoder_feed(dummy_array());
assert!(s.has_pending_stop_encoder_feed());
s.clear_all();
assert!(!s.has_pending_stop_encoder_feed());
assert!(!s.has_obligation());
}
#[test]
fn clear_all_drops_every_obligation_in_one_call() {
let mut s = SessionRetryState::new();
s.enqueue_finalize(dummy_array());
s.arm_decode_owed();
assert!(s.has_obligation());
s.clear_all();
assert!(!s.has_obligation());
assert!(s.finalize_queue().is_empty());
assert!(s.resume_at().is_none());
}
#[test]
fn decode_owed_is_distinct_from_throttled_drain() {
let mut s = SessionRetryState::new();
assert!(!s.has_decode_owed());
s.arm_decode_owed();
assert!(s.has_decode_owed());
s.clear_decode_owed();
assert!(!s.has_decode_owed());
}
#[test]
fn take_stop_partial_decode_features_returns_none_when_not_set() {
let mut s = SessionRetryState::new();
assert!(s.take_stop_partial_decode_features().is_none());
}
#[test]
fn take_stop_partial_decode_features_consumes_payload() {
let mut s = SessionRetryState::new();
s.arm_stop_partial_decode(Some(dummy_array()));
assert!(s.has_pending_stop_partial_decode());
let taken = s.take_stop_partial_decode_features().expect("set above");
assert!(taken.is_some());
assert!(!s.has_pending_stop_partial_decode());
}
#[test]
fn discharge_stop_mel_flush_noop_when_not_staged() {
let mut s = SessionRetryState::new();
let mut mel = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
let out = s
.discharge_stop_mel_flush(&mut mel)
.expect("noop must succeed");
assert!(out.is_none(), "no obligation ⇒ Ok(None)");
assert!(!s.has_obligation());
}
#[test]
fn discharge_stop_mel_flush_empty_overlap_clears_obligation() {
let mut s = SessionRetryState::new();
let mut mel = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
s.stage_stop_mel_flush();
assert!(s.has_pending_stop_mel_flush());
let out = s
.discharge_stop_mel_flush(&mut mel)
.expect("empty-overlap flush must succeed");
assert!(
out.is_none(),
"empty overlap ⇒ flush yields None ⇒ no StopEncoderFeed stage"
);
assert!(!s.has_pending_stop_mel_flush());
assert!(
!s.has_pending_stop_encoder_feed(),
"no mel rows ⇒ MUST NOT advance to StopEncoderFeed"
);
assert!(!s.has_obligation());
}
#[test]
fn discharge_stop_mel_flush_with_overlap_advances_to_stop_encoder_feed() {
let mut s = SessionRetryState::new();
let mut mel = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
let _ = mel
.process(&[0.1_f32; 16])
.expect("process must succeed on small input");
assert!(
mel.overlap_buffer_len() > 0,
"test precondition: overlap populated"
);
s.stage_stop_mel_flush();
let out = s
.discharge_stop_mel_flush(&mut mel)
.expect("flush must succeed");
assert!(out.is_some(), "non-empty overlap ⇒ flush yields Some(mel)");
assert!(
s.has_pending_stop_encoder_feed(),
"successful flush with mel rows MUST advance resume_at to \
StopEncoderFeed for the downstream discharge to drain"
);
assert!(
!s.has_pending_stop_mel_flush(),
"successful flush MUST clear StopMelFlush"
);
}
#[test]
fn discharge_stop_mel_flush_lands_on_stop_encoder_feed_when_overlap_nonempty() {
let mut s = SessionRetryState::new();
let mut mel = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
let _ = mel
.process(&[0.1_f32; 16])
.expect("process must succeed on small input");
assert!(mel.overlap_buffer_len() > 0);
s.stage_stop_mel_flush();
let _ = s
.discharge_stop_mel_flush(&mut mel)
.expect("happy-path flush must succeed");
assert!(
!s.has_pending_stop_mel_flush(),
"StopMelFlush MUST NOT be re-armed after flush()'s overlap-clear"
);
assert!(
s.has_pending_stop_encoder_feed(),
"flushed mel MUST reach StopEncoderFeed (the only valid landing)"
);
assert_eq!(
mel.overlap_buffer_len(),
0,
"test precondition: flush commit clears overlap, so the \
obligation is the ONLY remaining source of the tail mel"
);
}
#[test]
fn stop_encoder_feed_obligation_recovers_staged_mel_into_encoder() {
struct RecordingEncoder {
window_size: usize,
call_count: std::cell::RefCell<usize>,
}
impl StreamingEncoderBackend for RecordingEncoder {
fn window_size(&self) -> usize {
self.window_size
}
fn encode_window(&self, mel_window: &Array, _valid_frames: usize) -> Result<Array> {
*self.call_count.borrow_mut() += 1;
let rows: usize = mel_window.shape().first().copied().unwrap_or(0);
let buf = vec![0.0_f32; rows * 2];
Array::from_slice::<f32>(&buf, &[rows as i32, 2i32])
}
}
let mut mel_proc = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
let _ = mel_proc
.process(&[0.1_f32; 16])
.expect("process must succeed");
let mel_array = mel_proc
.flush()
.expect("flush must succeed")
.expect("non-empty overlap ⇒ Some(mel)");
let n_mels: usize = mel_array.shape().get(1).copied().unwrap_or(0);
let mut s = SessionRetryState::new();
s.stage_stop_encoder_feed(mel_array);
assert!(s.has_pending_stop_encoder_feed());
let backend = RecordingEncoder {
window_size: 1, call_count: std::cell::RefCell::new(0),
};
let mut encoder: StreamingEncoder<RecordingEncoder> = StreamingEncoder::new(backend, 4, 0);
let drained = s
.discharge_stop_encoder_feed(&mut encoder)
.expect("path (b) discharge MUST consume the staged mel");
assert!(
*encoder.backend().call_count.borrow() > 0 || n_mels == 0,
"recovery: encoder.feed MUST receive the staged mel (call_count > 0)"
);
assert!(
!s.has_pending_stop_encoder_feed(),
"successful path-(b) drain MUST clear StopEncoderFeed"
);
if drained == 0 {
assert!(
!s.has_obligation(),
"drain=0 path: obligation fully cleared"
);
} else {
assert!(
s.has_decode_owed(),
"drain>0 path: obligation advanced to DecodeOwed"
);
}
}
#[test]
fn discharge_stop_mel_flush_try_clone_err_preserves_mel_as_stop_encoder_feed() {
let mut s = SessionRetryState::new();
let mut mel_proc = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
let _ = mel_proc
.process(&[0.1_f32; 16])
.expect("process must succeed on small input");
assert!(
mel_proc.overlap_buffer_len() > 0,
"test precondition: overlap populated so flush yields Some(mel)"
);
s.stage_stop_mel_flush();
assert!(s.has_pending_stop_mel_flush());
let result = s.discharge_stop_mel_flush_with_clone(&mut mel_proc, |_arr| {
Err(Error::InvariantViolation(
crate::error::InvariantViolationPayload::new(
"discharge_stop_mel_flush_with_clone",
"test-injected clone failure",
),
))
});
let err = result.expect_err("injected clone-Err MUST propagate as Err");
assert!(
matches!(err, Error::LayerKeyed(_)),
"discharge wraps the clone-Err in Error::LayerKeyed, got {err:?}"
);
match s.resume_at() {
Some(RetryStage::StopEncoderFeed(mel_frames)) => {
let rows: usize = mel_frames.shape().first().copied().unwrap_or(0);
let n_mels: usize = mel_frames.shape().get(1).copied().unwrap_or(0);
assert!(
rows > 0,
"staged mel_frames must carry the flushed rows (got rows=0)"
);
assert_eq!(
n_mels, 8,
"staged mel_frames must carry the configured n_mels=8"
);
}
other => panic!(
"REGRESSION: expected StopEncoderFeed obligation carrying \
the preserved mel, got {other:?} — this is the silent \
tail-loss path"
),
}
assert!(
!s.has_pending_stop_mel_flush(),
"StopMelFlush MUST NOT be re-armed after flush()'s overlap-clear"
);
assert_eq!(
mel_proc.overlap_buffer_len(),
0,
"test precondition: flush commit clears overlap, so the \
obligation is the ONLY remaining source of the tail mel"
);
struct RecordingEncoder {
window_size: usize,
call_count: std::cell::RefCell<usize>,
}
impl StreamingEncoderBackend for RecordingEncoder {
fn window_size(&self) -> usize {
self.window_size
}
fn encode_window(&self, mel_window: &Array, _valid_frames: usize) -> Result<Array> {
*self.call_count.borrow_mut() += 1;
let rows: usize = mel_window.shape().first().copied().unwrap_or(0);
let buf = vec![0.0_f32; rows * 2];
Array::from_slice::<f32>(&buf, &[rows as i32, 2i32])
}
}
let backend = RecordingEncoder {
window_size: 1, call_count: std::cell::RefCell::new(0),
};
let mut encoder: StreamingEncoder<RecordingEncoder> = StreamingEncoder::new(backend, 4, 0);
let _drained = s
.discharge_stop_encoder_feed(&mut encoder)
.expect("path (b) discharge MUST consume the preserved mel");
assert!(
*encoder.backend().call_count.borrow() > 0,
"end-to-end recovery: the preserved mel MUST reach the \
encoder backend on the next discharge (call_count > 0)"
);
assert!(
!s.has_pending_stop_encoder_feed(),
"successful path-(b) drain MUST clear StopEncoderFeed"
);
}
#[test]
fn discharge_stop_mel_flush_never_leaves_stop_mel_flush_armed_after_overlap_committed() {
let mut s = SessionRetryState::new();
let mut mel = IncrementalMelSpectrogram::new(16_000, 32, 16, 8).unwrap();
let _ = mel
.process(&[0.1_f32; 16])
.expect("process must succeed on small input");
assert!(mel.overlap_buffer_len() > 0);
s.stage_stop_mel_flush();
let _ = s.discharge_stop_mel_flush(&mut mel);
let stop_mel_flush_armed = matches!(s.resume_at(), Some(RetryStage::StopMelFlush));
let overlap_empty = mel.overlap_buffer_len() == 0;
assert!(
!(stop_mel_flush_armed && overlap_empty),
"non-regression: forbidden state — StopMelFlush re-armed \
against empty overlap → next retry's flush short-circuits to \
Ok(None) and Ended emits silent tail loss. Routing the \
flushed mel into StopEncoderFeed unconditionally keeps this state \
unreachable."
);
}