hive-sh4re: one saturating_age for every loose-end producer
`age_seconds` is documented on the LooseEnd enum as saturating to zero on any clock anomaly, but the derivation was in three places: hive-c0re had a named `saturating_age` helper with tests, and the in-agent socket server hand-rolled the same two lines twice, untested. Move the helper to hive-sh4re::inbox, beside the enum whose contract it implements and inside the one crate both producers already depend on. Its three tests move with it (not dropped) and gain two arms: the whole-i64 range, where the saturating_sub is what stops the subtraction overflowing, and a far-past control so those zeros are the clamp firing rather than the function bottoming out on large inputs. The two clamps are not redundant, which is what `to_loose_end`'s doc got wrong: it credited "saturating" for the zero, but saturating_sub bottoms out at i64::MIN, still negative. The try_from is what yields 0. Also cover the two projections themselves, which is the part the shared helper cannot: that a reminder ages from created_at rather than due_at, and a todo from updated_at, with a future timestamp reading 0 through both and a past-timestamp control on each.
This commit is contained in:
parent
576294012b
commit
1e67f56249
3 changed files with 193 additions and 36 deletions
|
|
@ -188,6 +188,23 @@ pub enum LooseEnd {
|
|||
},
|
||||
}
|
||||
|
||||
/// Age in seconds between two unix timestamps, clamped to 0 when `then`
|
||||
/// is in the future. This is what backs the `age_seconds` promise on
|
||||
/// every [`LooseEnd`] variant above, so it lives beside the enum rather
|
||||
/// than in either producer: hive-c0re derives approval ages here, and
|
||||
/// hive-agent's in-agent socket server derives reminder + todo ages.
|
||||
///
|
||||
/// Both clamps are load-bearing and neither substitutes for the other:
|
||||
/// `saturating_sub` keeps the subtraction itself from overflowing on
|
||||
/// absurd inputs, and the `try_from` is what turns a negative delta into
|
||||
/// 0 — on its own `saturating_sub` saturates towards `i64::MIN`, which is
|
||||
/// still negative and would wrap to ~`u64::MAX` in an `as` cast.
|
||||
#[must_use]
|
||||
pub fn saturating_age(now: i64, then: i64) -> u64 {
|
||||
let delta = now.saturating_sub(then);
|
||||
u64::try_from(delta).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Kind discriminator for `CancelLooseEnd`. Per-kind store +
|
||||
/// authorisation rules live in
|
||||
/// `docs/process/conventions.md::Loose-ends wire shape`.
|
||||
|
|
@ -198,3 +215,49 @@ pub enum CancelLooseEndKind {
|
|||
/// Withdraw a pending approval (manager surface only).
|
||||
Approval,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn saturating_age_normal_case() {
|
||||
assert_eq!(saturating_age(1_000_000, 999_990), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saturating_age_zero_when_equal() {
|
||||
assert_eq!(saturating_age(42, 42), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saturating_age_handles_clock_back_step() {
|
||||
// `now` < `then`: the clock went backwards between rows. 0 beats
|
||||
// both a negative and the ~u64::MAX an `as` cast would produce,
|
||||
// which renders as "27 billion years ago" in the wake prompt.
|
||||
assert_eq!(saturating_age(100, 200), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_far_future_timestamp_stays_zero_rather_than_wrapping() {
|
||||
// The back-step case above is one second of skew; this is the
|
||||
// whole i64 range, where the subtraction itself saturates. Both
|
||||
// must land on 0, and the pair is what pins the two clamps
|
||||
// together — either one alone passes one of these and not both.
|
||||
assert_eq!(saturating_age(i64::MIN, i64::MAX), 0);
|
||||
assert_eq!(saturating_age(0, i64::MAX), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_far_past_timestamp_is_a_real_age_not_a_clamp() {
|
||||
// Control for the two arms above: the maximum representable
|
||||
// delta still comes back as itself, so their 0 is the clamp
|
||||
// firing and not this function bottoming out on large inputs.
|
||||
let age = saturating_age(i64::MAX, i64::MIN);
|
||||
assert_eq!(age, u64::try_from(i64::MAX).unwrap());
|
||||
assert_eq!(
|
||||
saturating_age(i64::MAX, 0),
|
||||
u64::try_from(i64::MAX).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue