@@ -3272,6 +3272,11 @@ mod tests {
32723272 /// "reconcile invalid_grant -> needs_reauth + suspend" test can assert
32733273 /// the orchestrator runs the V-F transition off the reconcile path.
32743274 reconcile_auth_fail : std:: sync:: atomic:: AtomicBool ,
3275+ /// 2026-08-14 follow-up: when `> 0`, `reconcile` fires the recover
3276+ /// sink this many times (bytes climbing 0 -> total) with the clock
3277+ /// STATIONARY - drives the "throttle emits only the first and final
3278+ /// Recovering states" test.
3279+ recover_ticks : AtomicU64 ,
32753280 /// R2-P2-1: when `> 0`, `execute` STREAMS this many op outcomes through
32763281 /// the per-op `on_outcome` sink (so their activity is persisted) and
32773282 /// THEN returns `Err` - simulating a crash/shutdown PART-WAY through a
@@ -3448,12 +3453,26 @@ mod tests {
34483453 async fn reconcile (
34493454 & self ,
34503455 source : & SourceRow ,
3451- _on_recover : & crate :: executor:: RecoverProgressSink < ' _ > ,
3456+ on_recover : & crate :: executor:: RecoverProgressSink < ' _ > ,
34523457 ) -> anyhow:: Result < ( ) > {
34533458 // Count every attempt. A configured transient failure returns Err
34543459 // WITHOUT recording the source as adopted, so the P1-1 test can
34553460 // assert the failed source is retried on the next cycle.
34563461 self . reconciles . fetch_add ( 1 , Ordering :: SeqCst ) ;
3462+ // 2026-08-14 follow-up: emulate a resume's progress stream (start
3463+ // tick at 0, evenly spaced acks, final tick at total).
3464+ let ticks = self . recover_ticks . load ( Ordering :: SeqCst ) ;
3465+ if ticks > 1 {
3466+ let total = 1_000u64 ;
3467+ for i in 0 ..ticks {
3468+ on_recover ( crate :: executor:: RecoverProgress {
3469+ path : "big/file.bin" . to_string ( ) ,
3470+ bytes_done : i * total / ( ticks - 1 ) ,
3471+ bytes_total : total,
3472+ } )
3473+ . await ;
3474+ }
3475+ }
34573476 // R-P2-2: a revoked token during the reconcile corrupt-trash retry
34583477 // surfaces as a CLASSIFIED error the orchestrator must turn into
34593478 // needs_reauth + suspend (not a plain transient retry).
@@ -6630,6 +6649,56 @@ mod tests {
66306649 ) ;
66316650 }
66326651
6652+ /// 2026-08-14 follow-up: the reconcile recovery sink's orchestrator-side
6653+ /// throttle. Five rapid ticks with a STATIONARY clock must surface exactly
6654+ /// TWO `Recovering` states: the first tick (the immediate flip out of the
6655+ /// indeterminate sweep) and the final tick (bytes_done == bytes_total,
6656+ /// exempt from the throttle). The middle ticks are within the 1s window
6657+ /// and suppressed.
6658+ #[ tokio:: test]
6659+ async fn reconcile_recovery_ticks_throttle_to_first_and_final ( ) {
6660+ let account = AccountId :: new_v4 ( ) ;
6661+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
6662+ let src = source_in ( account, tmp. path ( ) ) ;
6663+ let exec = Arc :: new ( RecordingExecutor :: default ( ) ) ;
6664+ exec. recover_ticks . store ( 5 , Ordering :: SeqCst ) ;
6665+ let state = Arc :: new ( FakeState :: with_sources ( vec ! [ src] ) ) ;
6666+ let clock = Arc :: new ( FakeClock :: new ( ) ) ;
6667+ clock. advance ( std:: time:: Duration :: from_millis ( 5_000 ) ) ;
6668+ let orch = SyncOrchestrator :: new (
6669+ account,
6670+ state. clone ( ) ,
6671+ exec. clone ( ) ,
6672+ Arc :: new ( FakePowerSource :: new ( power_on_ac ( ) ) ) ,
6673+ Arc :: new ( FakeNet :: online ( ) ) ,
6674+ clock. clone ( ) ,
6675+ OrchestratorConfig :: default ( ) ,
6676+ ) ;
6677+
6678+ let mut events = orch. subscribe ( ) ;
6679+ orch. run_cycle ( TickSource :: Scheduled ) . await . unwrap ( ) ;
6680+
6681+ let mut recovering: Vec < ( u64 , u64 ) > = Vec :: new ( ) ;
6682+ while let Ok ( ev) = events. try_recv ( ) {
6683+ if let OrchestratorEvent :: StateChanged {
6684+ state :
6685+ OrchestratorState :: Recovering {
6686+ bytes_done,
6687+ bytes_total,
6688+ ..
6689+ } ,
6690+ } = ev
6691+ {
6692+ recovering. push ( ( bytes_done, bytes_total) ) ;
6693+ }
6694+ }
6695+ assert_eq ! (
6696+ recovering,
6697+ vec![ ( 0 , 1_000 ) , ( 1_000 , 1_000 ) ] ,
6698+ "first and final ticks pass; the stationary-clock middle ticks are throttled"
6699+ ) ;
6700+ }
6701+
66336702 #[ tokio:: test]
66346703 async fn auth_invalid_grant_marks_needs_reauth_emits_event_and_stops_cycle ( ) {
66356704 // V-F (DESIGN s5.4): an op failing with `auth.invalid_grant` must move
0 commit comments