Skip to main content

tatara_process/
requeue.rs

1//! Requeue-action primitives — the small typed layer over the
2//! `kube::runtime::controller::Action::requeue(std::time::Duration
3//! ::from_secs(<secs>))` two-link chain every phase-machine handler,
4//! error-policy sink, and Signals ingest arm in the workspace exits
5//! through.
6//!
7//! `kube_runtime` exposes only ONE requeue constructor —
8//! `Action::requeue(std::time::Duration)` — but every consumer in
9//! this workspace already carries its retry / heartbeat / short-
10//! retry budget as a whole-second `u64` (the module-level
11//! `TICK_RETRY` / `HEARTBEAT` / `SHORT_RETRY` constants, the
12//! per-call literals `1`, `5`, `10`, `15`, `30`, `60`, `120`, and
13//! the `ctx.config.heartbeat_seconds` slot). This module owns the
14//! one-line chain from that `u64` to the returned `Action`, so a
15//! future normalization (an injectable-jitter overlay, a bounded-
16//! rate limiter, a per-controller floor / ceiling clamp, a
17//! deterministic-clock test hook) lands at ONE substrate primitive
18//! and every downstream retry sink inherits the upgrade mechanically.
19
20use kube::runtime::controller::Action;
21use std::time::Duration;
22
23/// A `kube::runtime::controller::Action` that re-enqueues the
24/// current object `secs` seconds from now — the one-line
25/// `Action::requeue(Duration::from_secs(secs))` two-link chain
26/// lifted to ONE typed owner past the ★★ PRIME-DIRECTIVE ≥ 2
27/// duplication threshold.
28///
29/// Pre-lift the SAME chain was hand-authored at 35 workspace-wide
30/// consumer sites across 5 files in the two ACTIVE reconciler
31/// crates:
32///
33/// * `tatara-reconciler::phase_machine` — 22 sites feeding the
34///   module-level `TICK_RETRY` / `HEARTBEAT` / `SHORT_RETRY`
35///   constants at every FSM handler's tail (the `pending` /
36///   `forking` / `execing` / `running` / `attested` / `reconverging`
37///   / `releasing` / `exiting` / `failed` / `zombie` / `reaped`
38///   arms).
39/// * `tatara-reconciler::controller` — 5 sites feeding literal `1`
40///   (deletion-preemption + signal-arm re-poll), the
41///   `ctx.config.heartbeat_seconds` slot (suspend), and `30`
42///   (reconcile-error sink + `error_policy`).
43/// * `tatara-reconciler::table_controller` — 2 sites feeding `30`
44///   at the ProcessTable heartbeat + error policy.
45/// * `tatara-pool-reconciler::controller_pool` — 3 sites feeding
46///   the reconcile-interval slot + literal `15` (the two `15`
47///   sites — the pool reconciler's `error_policy` sink + the
48///   Allocation reconciler's peer sink — subsequently reached
49///   [`error_backoff`] on the named-intent axis; count preserved
50///   here for the pre-lift audit).
51/// * `tatara-pool-reconciler::controller_allocation` — 3 sites
52///   feeding literal `5` (bind-retry), the reconcile-interval
53///   slot, and literal `15`.
54///
55/// All 35 sites walked the SAME two-link chain — take a whole-
56/// second `u64` (a literal, a named constant, or a config slot),
57/// wrap it in a `std::time::Duration` via `from_secs`, then hand
58/// the duration to `Action::requeue`. Differing only in the
59/// second-count operand. Post-lift each callsite reads
60/// `tatara_process::requeue::after_secs(N)` and the wrap +
61/// requeue chain lives at ONE substrate owner.
62///
63/// Return-form axis: `kube::runtime::controller::Action` — the
64/// exact type every kube-runtime `reconcile` fn returns as
65/// `Ok(...)` and every `error_policy` returns bare. The `u64`
66/// `secs` parameter matches `Duration::from_secs`'s own signature
67/// so the migration is byte-identical: every pre-lift site fed a
68/// `u64` (either a literal, a named `pub const N: u64 = ...;`, or
69/// a config field typed as `u64`) directly into `Duration::
70/// from_secs`, and the same feed continues to work at
71/// `after_secs`.
72///
73/// A future normalization — an injectable jitter overlay that
74/// randomizes ±10% of `secs` to avoid a thundering herd of
75/// synchronized reconcile ticks, a per-controller
76/// floor/ceiling clamp so a mis-configured heartbeat can't drive
77/// the API server, an injectable deterministic clock so
78/// integration tests can advance requeue budgets without waiting
79/// wall-clock time, a per-fleet rate limiter that spreads bursts
80/// across a sliding window, a `tracing`-annotated span carrying
81/// the requeue reason for post-hoc audit — lands at THIS ONE
82/// substrate primitive and every downstream retry sink across
83/// the two active reconciler crates inherits the upgrade
84/// mechanically. No per-site edit at any of the 35 listed
85/// callers or at future consumers (a new phase handler, a new
86/// controller crate, a per-Kind retry sink).
87///
88/// Sibling to the timed-decision primitives in [`crate::time`]
89/// on the "second-count → typed timed value" axis: `seconds_ago(N)
90/// -> DateTime<Utc>` seeds a wall-clock anchor `N` seconds in the
91/// past for `elapsed_since` consumers; `after_secs(N) -> Action`
92/// seeds a kube-runtime requeue `N` seconds in the future. Both
93/// carry the same "`u64` seconds is the workspace's canonical
94/// short-time unit" invariant so a switch to a finer-grained unit
95/// (a millisecond-precision retry budget for tight probes) would
96/// land at both primitives together rather than as scattered per-
97/// site conversions.
98#[must_use]
99pub fn after_secs(secs: u64) -> Action {
100    Action::requeue(Duration::from_secs(secs))
101}
102
103/// Second-count budget for the "fast re-poll after a state change"
104/// requeue intent (1s). The wire-form value binds the
105/// `Ok(after_secs(TICK_RETRY))` / `Ok(after_secs(1))` chains every
106/// FSM handler tail + `controller.rs` signal-consumed / deletion-
107/// preempt arm exit through — pre-lift restated as the `TICK_RETRY`
108/// local const in `tatara-reconciler::phase_machine` and as a bare
109/// `1` literal in `tatara-reconciler::controller`, past the ★★
110/// PRIME-DIRECTIVE ≥ 2 duplication threshold across two files that
111/// each named the same intent independently. Post-lift both bind
112/// through this ONE workspace-wide constant so a future budget
113/// normalization (a bounded-rate limiter's floor, a jitter overlay's
114/// pivot) lands at ONE owner.
115pub const TICK_SECONDS: u64 = 1;
116
117/// Second-count budget for the "short retry after transient
118/// failure" requeue intent (5s). Bound by the `SHORT_RETRY` const in
119/// `tatara-reconciler::phase_machine` (execing evaluation / running
120/// evaluation / attested probe / reconverging), and by the bare `5`
121/// literal in `tatara-pool-reconciler::controller_allocation`'s
122/// bind-retry arm, past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
123/// threshold across two files.
124pub const SHORT_RETRY_SECONDS: u64 = 5;
125
126/// Second-count budget for the "steady-state heartbeat" requeue
127/// intent (30s). Bound by the `HEARTBEAT` const in
128/// `tatara-reconciler::phase_machine` (7 handler-tail sites), by the
129/// bare `30` literal in `tatara-reconciler::controller`'s
130/// reconcile-error sink + `error_policy`, and by the bare `30`
131/// literal in `tatara-reconciler::table_controller`'s ProcessTable
132/// heartbeat + `error_policy`, past the ★★ PRIME-DIRECTIVE ≥ 2
133/// duplication threshold across three files that each named the
134/// same intent independently.
135pub const HEARTBEAT_SECONDS: u64 = 30;
136
137/// Second-count budget for the "back off after a reconcile error"
138/// requeue intent (15s). Bound by the bare `15` literal in
139/// `tatara-pool-reconciler::controller_pool::error_policy` (Pool
140/// reconcile-failure backoff) and by the bare `15` literal in
141/// `tatara-pool-reconciler::controller_allocation::error_policy`
142/// (Allocation reconcile-failure backoff), past the ★★
143/// PRIME-DIRECTIVE ≥ 2 duplication threshold across two files that
144/// each named the same intent independently.
145///
146/// Sits between [`SHORT_RETRY_SECONDS`] (5s — a per-branch bounded
147/// retry inside a reconcile pass) and [`HEARTBEAT_SECONDS`] (30s —
148/// the steady-state re-observation cadence). The pool-reconciler
149/// deliberately chose a *shorter* error-backoff than its own
150/// `heartbeat_seconds` slot (default 30) because a Pool /
151/// EphemeralAllocation reconcile failure signals a mis-computed
152/// desired-count or a stale claim — either of which the operator
153/// wants observed again ahead of the next heartbeat tick — whereas
154/// the two `tatara-reconciler` error_policy sinks fall through to
155/// [`heartbeat`] directly because a Process-reconcile failure holds
156/// its own retry budget in the FSM handlers themselves.
157pub const ERROR_BACKOFF_SECONDS: u64 = 15;
158
159/// The "fast re-poll" requeue action — `after_secs(TICK_SECONDS)`.
160///
161/// The ONE substrate owner of the "immediate re-poll after a signal
162/// consumption or state-change latch" requeue intent every FSM
163/// handler tail and top-level dispatcher exits through. Pre-lift the
164/// intent was hand-authored at 12 workspace-wide sites past the ★★
165/// PRIME-DIRECTIVE ≥ 2 duplication threshold across two files, each
166/// naming the same intent independently:
167///
168/// * `tatara-reconciler::phase_machine` — 10 sites fed the local
169///   `const TICK_RETRY: u64 = 1;` binding at pending / forking-
170///   heartbeat / attested-heartbeat / exiting-tail / zombie-poll /
171///   reaped handler tails.
172/// * `tatara-reconciler::controller` — 2 sites fed a bare `1`
173///   literal at the deletion-preempt requeue and the signal-
174///   consumed requeue (both semantically "re-poll immediately").
175///
176/// Post-lift each callsite reads `tatara_process::requeue::tick()`
177/// and the intent → second-count → requeue chain lives at ONE
178/// substrate owner. Peer to the [`short_retry`] and [`heartbeat`]
179/// helpers on the "named requeue intent" axis; sibling to the raw-
180/// second-count [`after_secs`] on the "typed requeue constructor"
181/// axis (the semantic helpers here compose through it).
182///
183/// A future normalization on the tick intent alone (e.g. a
184/// millisecond-precision path for tight controller inner loops,
185/// bounded by a workspace-wide floor) lands at THIS ONE substrate
186/// primitive and every downstream tick site inherits the upgrade
187/// mechanically. No per-site edit at any of the 12 listed callers
188/// or at future consumers (a new phase handler tail, a new signal
189/// arm, a new top-level dispatcher).
190///
191/// Theory anchor: THEORY.md §VI.1 (generation over composition —
192/// the intent recurred at 12 hand-authored production sites past
193/// the PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
194/// ONE owner here). THEORY.md §II.1 invariant 5 (composition
195/// preserves proofs — the intent → second-count mapping lives at
196/// ONE typed algebra projection; a regression that drifted the
197/// second-count would fail at the byte-shape pin below rather than
198/// as silent operator-visible cadence skew across the 12
199/// downstream requeue sites).
200#[must_use]
201pub fn tick() -> Action {
202    after_secs(TICK_SECONDS)
203}
204
205/// The "short retry after transient failure" requeue action —
206/// `after_secs(SHORT_RETRY_SECONDS)`.
207///
208/// The ONE substrate owner of the "short back-off after a
209/// transient error / not-yet-ready state" requeue intent. Pre-lift
210/// bound at 5 workspace-wide sites past the ★★ PRIME-DIRECTIVE
211/// ≥ 2 duplication threshold across two files:
212///
213/// * `tatara-reconciler::phase_machine` — 4 sites fed the local
214///   `const SHORT_RETRY: u64 = 5;` at handler branches that
215///   re-check a slow-converging condition (execing evaluator /
216///   running evaluator retry / attested-attestation retry /
217///   reconverging spec-hash check).
218/// * `tatara-pool-reconciler::controller_allocation` — 1 site fed
219///   a bare `5` literal at the Bind-arm patch-failure retry.
220///
221/// Post-lift each callsite reads
222/// `tatara_process::requeue::short_retry()`. Peer to [`tick`] and
223/// [`heartbeat`] on the "named requeue intent" axis; each future
224/// consumer that observes a transient error and wants a short
225/// bounded retry lands as ONE new callsite here instead of another
226/// hand-authored `after_secs(5)` chain.
227#[must_use]
228pub fn short_retry() -> Action {
229    after_secs(SHORT_RETRY_SECONDS)
230}
231
232/// The "steady-state heartbeat" requeue action —
233/// `after_secs(HEARTBEAT_SECONDS)`.
234///
235/// The ONE substrate owner of the "default periodic reconcile
236/// heartbeat" requeue intent. Pre-lift bound at 11 workspace-wide
237/// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold
238/// across three files:
239///
240/// * `tatara-reconciler::phase_machine` — 7 sites fed the local
241///   `const HEARTBEAT: u64 = 30;` at handler tails that maintain
242///   steady-state re-observation (forking / execing / running /
243///   attested / reconverging / releasing / failed).
244/// * `tatara-reconciler::controller` — 2 sites fed a bare `30`
245///   literal at the reconcile-error sink and the `error_policy`
246///   return (both semantically "back off to the default
247///   heartbeat after an error").
248/// * `tatara-reconciler::table_controller` — 2 sites fed a bare
249///   `30` literal at the ProcessTable heartbeat return and its
250///   `error_policy` return.
251///
252/// Post-lift each callsite reads
253/// `tatara_process::requeue::heartbeat()`. Peer to [`tick`] and
254/// [`short_retry`] on the "named requeue intent" axis; each future
255/// controller that wants the workspace-canonical heartbeat cadence
256/// lands as ONE new callsite here instead of another hand-authored
257/// `after_secs(30)` chain, and a future workspace-wide heartbeat
258/// re-tuning (a config-slot override, a per-controller adaptive
259/// cadence) lands at THIS primitive rather than as a scatter-
260/// gather sweep across every reconciler.
261#[must_use]
262pub fn heartbeat() -> Action {
263    after_secs(HEARTBEAT_SECONDS)
264}
265
266/// The "back off after a reconcile error" requeue action —
267/// `after_secs(ERROR_BACKOFF_SECONDS)`.
268///
269/// The ONE substrate owner of the "re-enqueue after an
270/// `error_policy` sink caught a reconcile failure" requeue intent.
271/// Pre-lift bound at 2 workspace-wide `error_policy` return-sites
272/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across
273/// one crate:
274///
275/// * `tatara-pool-reconciler::controller_pool::error_policy` — the
276///   `EphemeralPool` reconciler's failure sink; a bare `15`
277///   literal fed [`after_secs`].
278/// * `tatara-pool-reconciler::controller_allocation::error_policy`
279///   — the `EphemeralAllocation` reconciler's failure sink; a bare
280///   `15` literal fed [`after_secs`].
281///
282/// Post-lift each callsite reads
283/// `tatara_process::requeue::error_backoff()`. Peer to [`tick`],
284/// [`short_retry`], and [`heartbeat`] on the "named requeue intent"
285/// axis; the pool-reconciler's `error_policy` deliberately chose a
286/// backoff shorter than its own `heartbeat_seconds` slot (see
287/// [`ERROR_BACKOFF_SECONDS`] for the "pool-failure signals stale
288/// desired-count / claim" rationale). `tatara-reconciler`'s two
289/// `error_policy` sinks fall through to [`heartbeat`] directly
290/// because a Process-reconcile failure holds its own retry budget
291/// in the FSM handlers themselves — so the two intents are named
292/// separately on purpose rather than folded.
293///
294/// A future normalization on the error-backoff intent alone (an
295/// exponential-backoff overlay bounded by `HEARTBEAT_SECONDS`, a
296/// per-reconciler injectable ceiling for a mis-configured pool, a
297/// jitter overlay to avoid a thundering herd of pool + allocation
298/// error retries) lands at THIS ONE substrate primitive and both
299/// downstream `error_policy` sites inherit the upgrade
300/// mechanically. No per-site edit at either of the 2 listed
301/// callers or at future consumers (a new pool-adjacent controller,
302/// a new failure-sink axis).
303///
304/// Theory anchor: THEORY.md §VI.1 (generation over composition —
305/// the intent recurred at 2 hand-authored production sites past
306/// the PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
307/// ONE owner here). THEORY.md §II.1 invariant 5 (composition
308/// preserves proofs — the intent → second-count mapping lives at
309/// ONE typed algebra projection; a regression that drifted the
310/// second-count would fail at the byte-shape pin below rather than
311/// as silent operator-visible cadence skew across both downstream
312/// `error_policy` return-sites).
313#[must_use]
314pub fn error_backoff() -> Action {
315    after_secs(ERROR_BACKOFF_SECONDS)
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    // ─── after_secs substrate pins ─────────────────────────────────────
323    //
324    // Bind [`after_secs`] at fail-before-pass-after granularity so a
325    // regression that swapped the unit (`from_millis` instead of
326    // `from_secs`), flipped the requeue constructor (`Action::await_change`
327    // instead of `Action::requeue`), dropped the return through a stale
328    // hardcoded fallback, or reshaped the return form (an owned `Duration`
329    // instead of the `Action`) surfaces HERE rather than as silent
330    // operator-visible drift at the 35 downstream consumer sites across
331    // both active reconciler crates.
332
333    #[test]
334    fn after_secs_returns_action_carrying_the_second_count_duration() {
335        // Primary shape asserted end-to-end: the returned Action carries
336        // the exact `Duration::from_secs(secs)` value on its requeue slot.
337        // `Action` doesn't expose its inner Duration as a public getter,
338        // so verify the shape via `Debug` — `Action::requeue(Duration::
339        // from_secs(30))` formats with `30s` somewhere in the debug output;
340        // a regression that unit-swapped to `from_millis` would show `30ms`.
341        let debug = format!("{:?}", after_secs(30));
342        assert!(
343            debug.contains("30s") || debug.contains("30 s") || debug.contains("30.0s"),
344            "after_secs(30) debug {debug:?} must mention 30s — a regression that swapped the unit would drift here"
345        );
346    }
347
348    #[test]
349    fn after_secs_zero_returns_action_that_requeues_immediately() {
350        // Boundary corner: `secs = 0` yields `Action::requeue(Duration::
351        // ZERO)`, the "requeue this reconciliation immediately" shape
352        // kube-runtime treats as a tight re-tick. A regression that
353        // clamped the zero arm to a minimum (a "no requeue faster than
354        // 1s" clamp) would silently delay every tight-loop consumer that
355        // legitimately wants an immediate re-tick — the signal-consumed
356        // arm in `controller.rs`, a pending-phase check that just
357        // observed a wire update.
358        let debug = format!("{:?}", after_secs(0));
359        assert!(
360            debug.contains("0s") || debug.contains("0 s") || debug.contains("0ns"),
361            "after_secs(0) debug {debug:?} must reflect a zero requeue duration — a regression that clamped to a minimum would drift here"
362        );
363    }
364
365    #[test]
366    fn after_secs_matches_hand_authored_pre_lift_chain_shape() {
367        // Byte-identical parity with the pre-lift `Action::requeue(
368        // Duration::from_secs(N))` block that all 35 hand-authored
369        // callsites restated verbatim, swept across the representative
370        // second-counts every pre-lift consumer used:
371        //
372        //   1  → deletion-preempt + signal re-poll (`controller.rs`)
373        //   5  → bind-retry (`controller_allocation.rs`)
374        //   15 → cluster fallback (`controller_pool` / `controller_allocation`)
375        //   30 → reconcile-error + table-controller (`controller.rs`,
376        //        `table_controller.rs`)
377        //   60 → `TICK_RETRY` / `HEARTBEAT` scale (`phase_machine.rs`)
378        //   3600 → hour-scale sanity check
379        //
380        // `Action` doesn't derive `PartialEq`, so parity is asserted via
381        // `Debug` output — both blocks build the exact same requeue
382        // Action, so their debug reps must string-equal.
383        for secs in [1_u64, 5, 15, 30, 60, 3_600] {
384            let composed = format!("{:?}", after_secs(secs));
385            let hand_authored = format!("{:?}", Action::requeue(Duration::from_secs(secs)));
386            assert_eq!(
387                composed, hand_authored,
388                "after_secs({secs}) debug {composed:?} must match hand-authored Action::requeue(Duration::from_secs({secs})) debug {hand_authored:?} — a regression that reshaped either link would drift here"
389            );
390        }
391    }
392
393    #[test]
394    fn after_secs_composes_at_reconcile_return_position() {
395        // The canonical consumer shape end-to-end: a `reconcile` fn
396        // returns `Ok(after_secs(N))` where the caller expects a
397        // `Result<Action, _>` back. A regression that returned a
398        // different type (an owned `Duration`, a `Result` wrapper,
399        // an `Option<Action>`) would fail to type-check at every
400        // consumer. Pin the return shape by explicitly annotating
401        // the `Ok` arm so a regression that widened the return
402        // surfaces HERE rather than as a workspace-wide type error.
403        let out: Result<Action, ()> = Ok(after_secs(5));
404        assert!(out.is_ok());
405    }
406
407    #[test]
408    fn after_secs_composes_at_error_policy_return_position() {
409        // Peer to the reconcile-return shape: `error_policy` returns
410        // bare `Action` (no `Result` wrapper). Pin that shape too so a
411        // regression that changed the return to `Result<Action, _>`
412        // would surface HERE rather than as a workspace-wide type
413        // error at every `Controller::run(...).error_policy(...)`
414        // callsite.
415        let _out: Action = after_secs(30);
416    }
417
418    #[test]
419    fn after_secs_accepts_config_slot_typed_as_u64() {
420        // The `ctx.config.heartbeat_seconds` slot and every
421        // `TICK_RETRY` / `HEARTBEAT` / `SHORT_RETRY` module-level
422        // `pub const N: u64` binding is typed as `u64`; the primitive
423        // must accept those without a cast. A regression that
424        // narrowed the parameter to `u32` or widened it to `i64`
425        // would break either the `phase_machine.rs` const-fed
426        // callsites or the `controller.rs` config-fed slot at the
427        // migration boundary.
428        let heartbeat_seconds: u64 = 60;
429        let _out: Action = after_secs(heartbeat_seconds);
430    }
431
432    // ─── Named requeue-intent helpers ───────────────────────────────
433    //
434    // Fail-before-pass-after pins for [`tick`], [`short_retry`], and
435    // [`heartbeat`]. Each helper binds ONE named requeue intent to
436    // its second-count budget through [`after_secs`]. The pins here
437    // catch three regression axes at the substrate boundary:
438    //
439    //   1. The intent → second-count binding drifts (e.g. `heartbeat`
440    //      shifts from 30s to 60s under a partial re-tuning), silently
441    //      changing the reconcile cadence across every downstream
442    //      consumer.
443    //   2. A helper is retargeted onto a different underlying primitive
444    //      (`Action::await_change` instead of `Action::requeue`) or
445    //      unit (`from_millis` instead of `from_secs`), silently
446    //      inverting or scale-shifting the requeue semantics.
447    //   3. The public second-count constant and the helper drift out
448    //      of lockstep (the helper still returns 30s but the const
449    //      reports 60s, misleading any consumer that reads the const
450    //      to build its own tuned requeue).
451
452    #[test]
453    fn tick_binds_to_one_second_and_matches_pre_lift_hand_authored_chain() {
454        // The `TICK_RETRY = 1` local const the pre-lift 10
455        // `phase_machine.rs` sites + the bare `1` literal the pre-
456        // lift 2 `controller.rs` sites walked, both now bound at ONE
457        // owner here. A drift would silently change the tick cadence
458        // across all 12 downstream sites.
459        assert_eq!(TICK_SECONDS, 1);
460        let composed = format!("{:?}", tick());
461        let hand_authored = format!("{:?}", after_secs(1));
462        assert_eq!(
463            composed, hand_authored,
464            "tick() must byte-shape-match after_secs(1); the intent → second-count binding drifted",
465        );
466    }
467
468    #[test]
469    fn short_retry_binds_to_five_seconds_and_matches_pre_lift_hand_authored_chain() {
470        // The `SHORT_RETRY = 5` local const the pre-lift 4
471        // `phase_machine.rs` sites + the bare `5` literal the pre-
472        // lift 1 `controller_allocation.rs` bind-retry site walked,
473        // both now bound at ONE owner here.
474        assert_eq!(SHORT_RETRY_SECONDS, 5);
475        let composed = format!("{:?}", short_retry());
476        let hand_authored = format!("{:?}", after_secs(5));
477        assert_eq!(
478            composed, hand_authored,
479            "short_retry() must byte-shape-match after_secs(5); the intent → second-count binding drifted",
480        );
481    }
482
483    #[test]
484    fn heartbeat_binds_to_thirty_seconds_and_matches_pre_lift_hand_authored_chain() {
485        // The `HEARTBEAT = 30` local const the pre-lift 7
486        // `phase_machine.rs` sites + the bare `30` literal the pre-
487        // lift 4 `controller.rs` / `table_controller.rs` sites
488        // walked, all now bound at ONE owner here. A drift would
489        // silently change the heartbeat cadence across all 11
490        // downstream sites, potentially exceeding a per-cluster
491        // reconcile-rate budget or delaying steady-state
492        // re-observation past a control-loop deadline.
493        assert_eq!(HEARTBEAT_SECONDS, 30);
494        let composed = format!("{:?}", heartbeat());
495        let hand_authored = format!("{:?}", after_secs(30));
496        assert_eq!(
497            composed, hand_authored,
498            "heartbeat() must byte-shape-match after_secs(30); the intent → second-count binding drifted",
499        );
500    }
501
502    #[test]
503    fn error_backoff_binds_to_fifteen_seconds_and_matches_pre_lift_hand_authored_chain() {
504        // The bare `15` literal both `tatara-pool-reconciler::
505        // controller_pool::error_policy` and `tatara-pool-
506        // reconciler::controller_allocation::error_policy` fed
507        // into `after_secs` pre-lift, now bound at ONE owner
508        // here. A drift would silently change the pool + allocation
509        // error-backoff cadence across both downstream sites,
510        // either racing the API server on a genuinely stuck pool
511        // (if shortened) or delaying observation of a stale
512        // desired-count / claim past the operator's expectation
513        // (if lengthened).
514        assert_eq!(ERROR_BACKOFF_SECONDS, 15);
515        let composed = format!("{:?}", error_backoff());
516        let hand_authored = format!("{:?}", after_secs(15));
517        assert_eq!(
518            composed, hand_authored,
519            "error_backoff() must byte-shape-match after_secs(15); the intent → second-count binding drifted",
520        );
521    }
522
523    #[test]
524    fn named_requeue_intents_compose_at_reconcile_return_position() {
525        // Every helper returns `Action` and composes at the
526        // canonical `Result<Action, _>` return position every
527        // `reconcile` fn signature expects. A regression that
528        // widened any helper's return (an `Option<Action>` sink,
529        // a `Result` wrapper) would fail to type-check here rather
530        // than at the 28 downstream migration sites.
531        let _tick_ok: Result<Action, ()> = Ok(tick());
532        let _short_retry_ok: Result<Action, ()> = Ok(short_retry());
533        let _heartbeat_ok: Result<Action, ()> = Ok(heartbeat());
534        let _error_backoff_ok: Result<Action, ()> = Ok(error_backoff());
535    }
536
537    #[test]
538    fn named_requeue_intents_compose_at_error_policy_return_position() {
539        // Peer to the reconcile-return shape: `error_policy` returns
540        // bare `Action` (no `Result` wrapper). `heartbeat()` is the
541        // canonical error-policy return for both `controller.rs` and
542        // `table_controller.rs`; `error_backoff()` is the canonical
543        // error-policy return for both `controller_pool.rs` and
544        // `controller_allocation.rs`. Pin the shape so a regression
545        // that added a wrapper surfaces HERE rather than at the four
546        // `error_policy` sites.
547        let _tick: Action = tick();
548        let _short_retry: Action = short_retry();
549        let _heartbeat: Action = heartbeat();
550        let _error_backoff: Action = error_backoff();
551    }
552
553    #[test]
554    fn named_requeue_intents_project_distinct_second_counts() {
555        // Cross-intent coherence pin: the four named intents must
556        // resolve to four distinct second-counts. A regression that
557        // collapsed two intents onto the same constant (e.g.
558        // `TICK_SECONDS == SHORT_RETRY_SECONDS` after an over-eager
559        // "unify budgets" refactor, or `ERROR_BACKOFF_SECONDS ==
560        // HEARTBEAT_SECONDS` after folding the pool-reconciler's
561        // error sink into the process-reconciler's) would fold a
562        // semantic distinction into a numeric one, and the
563        // downstream sites would silently lose the intent name's
564        // meaning even though the helpers still compile.
565        assert_ne!(TICK_SECONDS, SHORT_RETRY_SECONDS);
566        assert_ne!(TICK_SECONDS, ERROR_BACKOFF_SECONDS);
567        assert_ne!(TICK_SECONDS, HEARTBEAT_SECONDS);
568        assert_ne!(SHORT_RETRY_SECONDS, ERROR_BACKOFF_SECONDS);
569        assert_ne!(SHORT_RETRY_SECONDS, HEARTBEAT_SECONDS);
570        assert_ne!(ERROR_BACKOFF_SECONDS, HEARTBEAT_SECONDS);
571        assert!(
572            TICK_SECONDS < SHORT_RETRY_SECONDS
573                && SHORT_RETRY_SECONDS < ERROR_BACKOFF_SECONDS
574                && ERROR_BACKOFF_SECONDS < HEARTBEAT_SECONDS,
575            "named requeue intents must project onto strictly increasing second-counts (tick < short_retry < error_backoff < heartbeat); a regression that flipped the ordering would silently invert the retry-cadence hierarchy",
576        );
577    }
578}