cross last-seen over the wire as seconds-ago per source
TASK-08(f): EvidenceSeen held the stamps internally and nothing outside the daemon could read them. to_ipc_json now carries evidence_last_seen_secs_ago; null stays distinct from stale, because never-reported and long-ago are different claims.
This commit is contained in:
parent
6dfe04e785
commit
0dd8aaaba0
1 changed files with 41 additions and 0 deletions
|
|
@ -3315,6 +3315,15 @@ impl DeviceStateMachine {
|
|||
"light": self.is_fresh(self.evidence_seen.light),
|
||||
"touch": self.is_fresh(self.evidence_seen.touch),
|
||||
},
|
||||
// Seconds since each source last reported; null means never.
|
||||
// `fresh` is a threshold answer, this is the raw number — a page
|
||||
// rendering "last seen 4 s ago" needs the number, not the bool.
|
||||
"evidence_last_seen_secs_ago": {
|
||||
"proximity": Self::secs_since(self.evidence_seen.proximity),
|
||||
"accel": Self::secs_since(self.evidence_seen.accel),
|
||||
"light": Self::secs_since(self.evidence_seen.light),
|
||||
"touch": Self::secs_since(self.evidence_seen.touch),
|
||||
},
|
||||
// Health is the other axis: `fresh` says whether the reading may
|
||||
// be believed, `health` says whether the source is there at all.
|
||||
"sensor_health": self.source_health.as_json(),
|
||||
|
|
@ -3327,6 +3336,10 @@ impl DeviceStateMachine {
|
|||
Instant::now().saturating_duration_since(t) <= self.policy.evidence_ttl
|
||||
})
|
||||
}
|
||||
|
||||
fn secs_since(seen: Option<Instant>) -> Option<u64> {
|
||||
seen.map(|t| Instant::now().saturating_duration_since(t).as_secs())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DeviceState {
|
||||
|
|
@ -4966,6 +4979,34 @@ mod tests {
|
|||
assert_eq!(json["evidence_fresh"]["proximity"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_seen_crosses_the_wire_as_seconds_ago() {
|
||||
// TASK-08(f): the machine held last-seen internally and nothing
|
||||
// outside could read it. null is "never reported", a different claim
|
||||
// from "long ago".
|
||||
let mut sm = DeviceStateMachine::new();
|
||||
let json = sm.to_ipc_json();
|
||||
assert_eq!(
|
||||
json["evidence_last_seen_secs_ago"]["proximity"],
|
||||
serde_json::Value::Null
|
||||
);
|
||||
|
||||
sm.mark_evidence_seen_at(
|
||||
SensorSource::Proximity,
|
||||
Instant::now() - Duration::from_secs(30),
|
||||
);
|
||||
let json = sm.to_ipc_json();
|
||||
let secs = json["evidence_last_seen_secs_ago"]["proximity"]
|
||||
.as_u64()
|
||||
.expect("a stamp 30 s old crosses as a number");
|
||||
assert!((30..=31).contains(&secs), "got {secs}");
|
||||
assert_eq!(
|
||||
json["evidence_last_seen_secs_ago"]["light"],
|
||||
serde_json::Value::Null,
|
||||
"a source that never reported stays null"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_repeated_reading_is_not_a_trail_entry() {
|
||||
// Reporters heartbeat every 30s so silence is meaningful. If each
|
||||
|
|
|
|||
Loading…
Reference in a new issue