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:
atlas 2026-09-02 13:47:26 +02:00 committed by mara
commit 1e67f56249
3 changed files with 193 additions and 36 deletions

View file

@ -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()
);
}
}