diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 93c4cbf..a071572 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -1699,6 +1699,25 @@ mod tests { } } + /// A pulse votes a tempo in bounds, declares an urgency in bounds, or does both; a pulse that does neither is refused. + #[test] + fn a_pulse_is_in_bounds_and_says_something() { + let validated = |beat: Option, urgency: Option| { + PipelineDocument::Pulse(PipelinePulse { instance: slug(INSTANCE), label: l("2026-10-31t041200-t0"), tempo_vote_beat_s: beat, urgency_permille: urgency, note: "".into() }).validate() + }; + for (beat, urgency) in [(Some(60), None), (Some(86_400), None), (None, Some(0)), (None, Some(1000)), (Some(600), Some(250))] { + assert_eq!(validated(beat, urgency), Ok(()), "{beat:?} {urgency:?}"); + } + for (beat, urgency, field) in [ + (Some(59), None, "pulse.tempo_vote_beat_s"), + (Some(86_401), None, "pulse.tempo_vote_beat_s"), + (None, Some(1001), "pulse.urgency_permille"), + (None, None, "pulse.tempo_vote_beat_s"), + ] { + assert!(matches!(validated(beat, urgency), Err(PipelineRefusal::InvalidFormat { field: f, .. }) if f == field), "{beat:?} {urgency:?}"); + } + } + /// Bounds: a run's campaigns and claims stop at their maximums. #[test] fn a_runs_lists_are_bounded() { diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 9e91b62..302b27f 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -2448,6 +2448,20 @@ mod tests { ); } + /// A pulse is her own: one of another mind is a ForeignInstance, her own is admitted. + #[test] + fn a_pulse_of_another_mind_is_a_foreign_instance_and_her_own_is_admitted() { + let mut mind = with_specs(); + let pulse = |instance: &str| { + D::Pulse(eureka_pipeline::PipelinePulse { instance: slug(instance), label: l("p1"), tempo_vote_beat_s: Some(600), urgency_permille: None, note: "".into() }) + }; + assert_eq!( + refusal(admit(&mut mind, vec![pulse(OTHER_INSTANCE)])), + MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() } + ); + committed(admit(&mut mind, vec![pulse(INSTANCE)])); + } + /// A run ends: Recorded or Withdrawn, and no other outcome. #[test] fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() { diff --git a/crates/mind-body/src/heartbeat/tests.rs b/crates/mind-body/src/heartbeat/tests.rs index d41558f..32844fb 100644 --- a/crates/mind-body/src/heartbeat/tests.rs +++ b/crates/mind-body/src/heartbeat/tests.rs @@ -457,3 +457,51 @@ fn the_heartbeat_holds_no_usage_port_so_usage_cannot_change_a_launch() { again.turn(BOB, 1000); assert_eq!(first, again.poll()); } + +/// A document of the dollar-initiative days, standing in for what an old `heartbeat.cc` holds. +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache(type = "eureka.heartbeat.initiative.v1", schema = "OldInitiative")] +struct OldInitiative { + #[cultcache(key = 0)] + initiative: String, +} + +fn trace_at(seconds: i64, tempo_s: u32) -> PollTrace { + PollTrace { at: crate::testkit::at(seconds), tempo_s, instances: vec![], launches: vec!["ann".into()] } +} + +#[test] +fn the_file_store_keeps_the_state_and_the_traces_and_the_tempo_view_reads_the_newest_trace() { + let dir = TempDir::new().unwrap(); + let path = dir.path().join("heartbeat.cc"); + let store = FileStore::new(&path); + store.check().unwrap(); + assert_eq!((store.load().unwrap(), store.latest().unwrap()), (None, None), "no file is no state"); + let state = HeartbeatState { last_beat_at: crate::testkit::at(5), handled_mentions: vec!["ann:e1".into()] }; + store.save(&state).unwrap(); + assert_eq!(store.load().unwrap(), Some(state)); + store.record(&trace_at(20, 600)).unwrap(); + store.record(&trace_at(10, 900)).unwrap(); + assert_eq!(store.latest().unwrap().map(|trace| trace.tempo_s), Some(600), "the newest by time, not by write order"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o644, "Persona units read it"); + } +} + +#[test] +fn an_old_heartbeat_store_fails_closed_naming_the_file_and_is_never_migrated() { + let dir = TempDir::new().unwrap(); + let path = dir.path().join("heartbeat.cc"); + let mut cache = CultCache::new(); + cache.register_entry_type::().unwrap(); + cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap(); + cache.pull_all_backing_stores().unwrap(); + cache.put("eureka", &OldInitiative { initiative: "3.5".into() }).unwrap(); + let store = FileStore::new(&path); + for result in [store.check().map(|()| ()), store.load().map(|_| ()), store.latest().map(|_| ()), store.save(&HeartbeatState { last_beat_at: crate::testkit::at(1), handled_mentions: vec![] }), store.record(&trace_at(1, 900))] { + let error = format!("{:#}", result.unwrap_err()); + assert!(error.contains(&path.display().to_string()) && error.contains("archive it, nothing migrates it"), "{error}"); + } +} diff --git a/crates/mind-body/src/tempo/tests.rs b/crates/mind-body/src/tempo/tests.rs index 43d66df..aed0dd0 100644 --- a/crates/mind-body/src/tempo/tests.rs +++ b/crates/mind-body/src/tempo/tests.rs @@ -15,6 +15,18 @@ fn mind(slug: &str, waited_s: Option) -> Standing { } } +#[test] +fn the_score_is_the_wait_in_beats_times_ten_to_the_urgency_in_orders() { + let mut standing = mind("a", Some(1800)); + assert_eq!(score(&standing, 900), 2.0, "two beats waited, no urgency"); + assert_eq!(score(&standing, 600), 3.0); + standing.self_permille = 1000; + assert_eq!(score(&standing, 900), 20.0, "full urgency is worth one order of magnitude"); + standing.self_permille = 500; + assert!((score(&standing, 900) - 2.0 * 10f64.sqrt()).abs() < 1e-9); + assert_eq!(score(&mind("never", None), 900), f64::INFINITY); +} + #[test] fn no_votes_is_the_default_beat() { assert_eq!(tally(&[]), DEFAULT_BEAT_S);