mind-body: a night's day is anchored on the newest night that slept; the heartbeat's night rules are pinned

The day began at the previous Sleep run whatever became of it, so the memories past DAY_MAX and a failed
night's whole day never met the disposition cover-the-day requires. A night now closes with where its day was
covered through (the ordinal of the last memory it was shown), and the next day is what was admitted after the
newest night that closed 'slept:'; the first night's day is the 24 hours before it opened. A consolidation is
made by a night, whatever room it keeps, so it is not the next day's memory.

Night::from_settings carries the both-or-neither start check out of the binary where a test reaches it.
Tests pin last_sleep with the unit gone, the window's minutes and its 23 and 59 bounds, and the half-set start.
This commit is contained in:
Meta Vens 2026-11-08 12:03:18 +01:00
commit e3be529b1e
6 changed files with 255 additions and 51 deletions

View file

@ -61,12 +61,7 @@ fn config() -> Result<Config> {
.map(PersonaId::parse)
.collect::<Result<Vec<_>>>()?;
let operators = required("MIND_PERSONA_OPERATOR")?.split(',').map(|id| id.trim().to_string()).filter(|id| !id.is_empty()).collect();
// The nightly sleep is both settings or neither.
let night = match (setting("HEARTBEAT_SLEEP_UTC"), setting("HEARTBEAT_SLEEP_MINDS")) {
(Some(window), Some(minds)) => Some(Night::parse(&window, &minds)?),
(None, None) => None,
_ => bail!("HEARTBEAT_SLEEP_UTC and HEARTBEAT_SLEEP_MINDS are set together or not at all"),
};
let night = Night::from_settings(setting("HEARTBEAT_SLEEP_UTC").as_deref(), setting("HEARTBEAT_SLEEP_MINDS").as_deref())?;
Config::new(personas, operators, night)
}

View file

@ -119,6 +119,16 @@ pub struct Night {
}
impl Night {
/// The nightly sleep from the two settings that name it: both or neither, so a half-set deploy fails at start
/// instead of quietly never sleeping, or sleeping no one.
pub fn from_settings(window: Option<&str>, minds: Option<&str>) -> Result<Option<Self>> {
match (window, minds) {
(Some(window), Some(minds)) => Self::parse(window, minds).map(Some),
(None, None) => Ok(None),
_ => bail!("HEARTBEAT_SLEEP_UTC and HEARTBEAT_SLEEP_MINDS are set together or not at all"),
}
}
/// `window` is `HH:MM-HH:MM` in UTC, `minds` the slugs comma-separated.
pub fn parse(window: &str, minds: &str) -> Result<Self> {
let minute = |text: &str| -> Result<u32> {

View file

@ -1457,6 +1457,37 @@ fn the_window_wraps_midnight_and_names_when_it_opened() {
assert!(Night::parse("13:00-13:00", "ann").is_err() && Night::parse("25:00-03:00", "ann").is_err() && Night::parse("13:00-14:00", " , ").is_err());
}
#[test]
fn a_window_keeps_its_minutes_and_its_bounds_are_the_clock() {
let on = |hour, minute| Utc.with_ymd_and_hms(2026, 10, 9, hour, minute, 0).unwrap();
let night = Night::parse("22:30-04:15", "ann").unwrap();
assert_eq!((night.from_min, night.to_min), (22 * 60 + 30, 4 * 60 + 15));
assert_eq!(night.opened(on(22, 29)), None);
assert_eq!(night.opened(on(22, 30)), Some(on(22, 30)));
assert_eq!(night.opened(on(4, 14)), Some(on(22, 30) - Duration::days(1)));
assert_eq!(night.opened(on(4, 15)), None);
// The last minute of the day is a time, and the first one past it is not.
let late = Night::parse("23:59-00:00", "ann").unwrap();
assert_eq!((late.from_min, late.to_min), (23 * 60 + 59, 0));
assert_eq!(Night::parse("00:00-23:00", "ann").unwrap().to_min, 23 * 60);
assert_eq!(Night::parse("12:00-12:59", "ann").unwrap().to_min, 12 * 60 + 59);
for bad in ["24:00-03:00", "22:00-24:00", "22:60-03:00", "22:00-03:60", "22:00", "22-03", "aa:bb-03:00"] {
assert!(Night::parse(bad, "ann").is_err(), "{bad}");
}
}
#[test]
fn the_nightly_sleep_is_both_settings_or_neither() {
assert_eq!(Night::from_settings(None, None).unwrap(), None);
let night = Night::from_settings(Some("22:30-04:00"), Some("ann, bob")).unwrap().unwrap();
assert_eq!((night.from_min, night.to_min, night.minds), (22 * 60 + 30, 4 * 60, vec![Slug("ann".into()), Slug("bob".into())]));
for only in [Night::from_settings(Some("22:30-04:00"), None), Night::from_settings(None, Some("ann"))] {
let fault = only.expect_err("a half-set night is refused at start").to_string();
assert!(fault.contains("HEARTBEAT_SLEEP_UTC") && fault.contains("HEARTBEAT_SLEEP_MINDS"), "{fault}");
}
assert!(Night::from_settings(Some("nonsense"), Some("ann")).is_err(), "both set, but not a window");
}
#[test]
fn a_listed_idle_mind_sleeps_once_per_window_and_a_missed_night_is_not_made_up() {
let rig = nights("13:00-14:00", "ann");
@ -1466,6 +1497,8 @@ fn a_listed_idle_mind_sleeps_once_per_window_and_a_missed_night_is_not_made_up()
// Busy while the night lasts: nothing more. Then her night ends (failed or slept, it counts) and the window is spent for her.
assert_eq!(rig.poll(), Outcome::Polled { launched: vec![] });
end_launched(&rig, ANN);
// Her unit is gone as well, so she is idle and only her last Sleep run says the window is spent.
rig.launcher.kill_as(&Slug("ann".into()), RunTurn::Sleep);
assert_eq!(rig.poll(), Outcome::Polled { launched: vec![] }, "one night per window, whatever became of it");
assert_eq!(started_turns(&rig).len(), 1);
// The next window opens a day later and she sleeps again.

View file

@ -7120,7 +7120,7 @@ fn the_night_line_holds_the_day_whole_and_prescribes_nothing() {
// An earlier night of hers, opened 30 hours before this one, closed.
rig.mind.admit_at(at(NIGHT_AT_MS - 30 * HOUR_MS));
rig.mind.committed(vec![crate::testkit::run("slept-before", RunTurn::Sleep, RunOperator::Mind, &[])]);
rig.mind.committed(vec![crate::testkit::close(crate::testkit::run_ref("slept-before"), crate::testkit::recorded())]);
rig.mind.committed(vec![crate::testkit::close(crate::testkit::run_ref("slept-before"), ResolutionOutcome::Recorded { reason: "slept: ok".into() })]);
// Her day: after that night opened. A Sleep-origin memory of it is not part of it.
rig.remembered("d1", MemorySort::Decided, "Ship on Friday.", &["@op:chat"], room(), 20, &["$e1"]);
rig.remembered("d2", MemorySort::Learned, "A private thing.", &["@kiko:chat"], dm("!dm:chat", &["@kiko:chat"]), 10, &[]);

View file

@ -25,7 +25,7 @@ use crate::persona::{
ABOUT_MAX, AGENT, Config, Conversation, Failure, Organs, Outcome, PERSONA_POLL_S, REMEMBER_TEXT_MAX_BYTES, Remember, SOURCES_MAX, Started, WHY_MAX_CHARS, date, label_of, next_sequence, remember_item_schema, start_session,
structured_output,
};
use crate::queue::{MindPort, any_of, headers, in_force, of_kind};
use crate::queue::{MindPort, any_of, citing, headers, in_force, of_kind};
use crate::recall;
use crate::self_run::session::{Polled, SessionInbox, drive};
use crate::self_run::{SESSION_NAMESPACE, Why};
@ -153,60 +153,106 @@ fn parse_time(at: &str) -> Result<DateTime<Utc>> {
Ok(DateTime::parse_from_rfc3339(at).map_err(|_| anyhow!("an admission time could not be read"))?.with_timezone(&Utc))
}
/// Her memories in force, oldest admitted first: the one read of what she holds.
fn memories_in_force(mind: &dyn MindPort) -> Result<Vec<Held>> {
/// Every document a selection holds, paged whole.
fn documents(mind: &dyn MindPort, selection: &Selection) -> Result<Vec<huginn_mind::PipelineDocumentView>> {
let mut found = Vec::new();
let mut asking = Selection { projection: "document".into(), ..in_force(of_kind(PipelineKind::Memory)) };
let mut asking = Selection { projection: "document".into(), ..selection.clone() };
loop {
let page = mind.query(&asking)?;
let PipelinePageItems::Documents(views) = page.items else { bail!("the mind answered headers to a document selection") };
for view in views {
if let PipelineDocument::Memory(memory) = view.document {
found.push(Held { id: view.id.id.0, memory, at: parse_time(&view.admission.admitted_at)?, ordinal: view.admission.ordinal });
}
}
found.extend(views);
match page.next {
Some(cursor) => asking.cursor = Some(cursor),
None => break,
}
}
Ok(found)
}
/// Her memories in force, oldest admitted first: the one read of what she holds.
fn memories_in_force(mind: &dyn MindPort) -> Result<Vec<Held>> {
let mut found = Vec::new();
for view in documents(mind, &in_force(of_kind(PipelineKind::Memory)))? {
if let PipelineDocument::Memory(memory) = view.document {
found.push(Held { id: view.id.id.0, memory, at: parse_time(&view.admission.admitted_at)?, ordinal: view.admission.ordinal });
}
}
found.sort_by_key(|held| held.ordinal);
Ok(found)
}
/// Her Sleep runs, any status, oldest first, with when each was admitted.
fn sleeps(mind: &dyn MindPort) -> Result<Vec<(PipelineRef, DateTime<Utc>)>> {
/// One of her Sleep runs: which, when it was admitted, and its admission ordinal.
struct Night {
run: PipelineRef,
at: DateTime<Utc>,
ordinal: u64,
}
/// Her Sleep runs, any status, oldest first.
fn sleeps(mind: &dyn MindPort) -> Result<Vec<Night>> {
let runs = any_of(any_of(any_of(of_kind(PipelineKind::Run), "root", &[&mind.instance().0]), "turn", &["Sleep"]), "operated_by", &["Mind"]);
let mut found = headers(mind, &runs)?.into_iter().map(|run| Ok((run.id, run.admission.ordinal, parse_time(&run.admission.admitted_at)?))).collect::<Result<Vec<_>>>()?;
found.sort_by_key(|(_, ordinal, _)| *ordinal);
Ok(found.into_iter().map(|(id, _, at)| (id, at)).collect())
let mut found = headers(mind, &runs)?.into_iter().map(|run| Ok(Night { run: run.id, ordinal: run.admission.ordinal, at: parse_time(&run.admission.admitted_at)? })).collect::<Result<Vec<_>>>()?;
found.sort_by_key(|night| night.ordinal);
Ok(found)
}
const SLEPT: &str = "slept: ";
const THROUGH: &str = "; day through ordinal ";
/// Where the newest of `earlier` that closed `slept:` left her day: the admission ordinal of the last memory of
/// the day it was shown (its own run's ordinal when it was shown none). A night that failed, or whose run never
/// closed, left its day uncovered, so it is not an anchor.
fn covered_through(mind: &dyn MindPort, earlier: &[&Night]) -> Result<Option<u64>> {
for night in earlier.iter().rev() {
let closings = Selection { cites: Some(citing(&night.run, Some("subject"))), ..of_kind(PipelineKind::Resolution) };
let latest = documents(mind, &closings)?
.into_iter()
.filter_map(|view| match view.document {
PipelineDocument::Resolution(resolution) => Some(resolution),
_ => None,
})
.max_by_key(|resolution| resolution.sequence);
if let Some(resolution) = latest
&& let ResolutionOutcome::Recorded { reason } = &resolution.outcome
&& reason.as_str().starts_with(SLEPT)
{
let through = resolution.rationale.0.rsplit_once(THROUGH).and_then(|(_, ordinal)| ordinal.trim().parse().ok());
return Ok(Some(through.unwrap_or(night.ordinal)));
}
}
Ok(None)
}
fn is_open_sort(sort: MemorySort) -> bool {
matches!(sort, MemorySort::Asked | MemorySort::Committed)
}
/// Made by a night, not lived in the day: formed in sleep, or a consolidation (a DM's keeps its room and voices,
/// so its origin is not Sleep).
fn is_sleep_made(held: &Held) -> bool {
matches!(held.memory.origin, MemoryOrigin::Sleep { .. })
matches!(held.memory.origin, MemoryOrigin::Sleep { .. }) || !held.memory.consolidates.is_empty()
}
/// The night, derived. Her day: her memories in force admitted after her previous Sleep run opened (else in the
/// 24 hours before this run opened), formed otherwise than in sleep, oldest first, at most `DAY_MAX`. Her open
/// Asked and Committed items older than the day. A tide of `TIDE` older memories of other sorts, from an offset
/// that moves by `TIDE` with each earlier Sleep run of hers.
/// The night, derived. Her day: her memories in force admitted after what her newest night that closed `slept:`
/// covered (a night that failed covered nothing), else in the 24 hours before her first night opened; formed
/// otherwise than in sleep, oldest first, at most `DAY_MAX`, the rest waiting for the next night. Her open Asked
/// and Committed items older than the day. A tide of `TIDE` older memories of other sorts, from an offset that
/// moves by `TIDE` with each earlier Sleep run of hers.
pub(crate) fn context(mind: &dyn MindPort, run: &PipelineRef, now: DateTime<Utc>) -> Result<Context> {
let nights = sleeps(mind)?;
let run_at = nights.iter().find(|(other, _)| other == run).map(|(_, at)| *at).ok_or_else(|| anyhow!("the night's run is not in her mind"))?;
let earlier: Vec<&(PipelineRef, DateTime<Utc>)> = nights.iter().filter(|(other, _)| other != run).collect();
let day_start = earlier.last().map_or(run_at - Duration::hours(24), |(_, at)| *at);
let tonight = nights.iter().find(|night| &night.run == run).ok_or_else(|| anyhow!("the night's run is not in her mind"))?;
let earlier: Vec<&Night> = nights.iter().filter(|night| night.ordinal < tonight.ordinal).collect();
let covered = covered_through(mind, &earlier)?;
let first_at = earlier.first().map_or(tonight.at, |night| night.at);
let in_day = |held: &Held| covered.map_or(held.at > first_at - Duration::hours(24), |through| held.ordinal > through);
let memories = memories_in_force(mind)?;
let mut counts = BTreeMap::new();
for held in &memories {
*counts.entry(format!("{:?}", held.memory.sort)).or_insert(0) += 1;
}
let day: Vec<Held> = memories.iter().filter(|held| held.at > day_start && !is_sleep_made(held)).take(DAY_MAX).cloned().collect();
let open: Vec<Held> = memories.iter().filter(|held| held.at <= day_start && is_open_sort(held.memory.sort)).take(OPEN_MAX).cloned().collect();
let backlog: Vec<&Held> = memories.iter().filter(|held| held.at <= day_start && !is_open_sort(held.memory.sort) && !is_sleep_made(held)).collect();
let day: Vec<Held> = memories.iter().filter(|held| in_day(held) && !is_sleep_made(held)).take(DAY_MAX).cloned().collect();
let open: Vec<Held> = memories.iter().filter(|held| !in_day(held) && is_open_sort(held.memory.sort)).take(OPEN_MAX).cloned().collect();
let backlog: Vec<&Held> = memories.iter().filter(|held| !in_day(held) && !is_open_sort(held.memory.sort) && !is_sleep_made(held)).collect();
let tide: Vec<Held> = if backlog.is_empty() {
Vec::new()
} else {
@ -429,7 +475,8 @@ pub(crate) fn pack(units: Vec<Vec<PipelineDocument>>) -> Vec<Vec<PipelineDocumen
/// How a night ended.
enum Finish {
Slept { reply: SleepReply },
/// `through` is the ordinal of the last memory of the day the night was shown, if it was shown any.
Slept { reply: SleepReply, through: Option<u64> },
Failed { refusals: u32, why: String },
}
@ -505,7 +552,7 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for SleepInbox<'_, C, S> {
match decode(&text, self.context) {
Ok(reply) => match self.admit(&reply) {
Ok(()) => {
self.finish = Some(Finish::Slept { reply });
self.finish = Some(Finish::Slept { reply, through: self.context.day.last().map(|held| held.ordinal) });
Ok((true, false))
}
Err(error) => {
@ -518,22 +565,29 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for SleepInbox<'_, C, S> {
}
}
/// Closes the night's run Recorded with her line and the counts, or the failure.
fn close(organs: &Organs, run: &PipelineRef, finish: &Finish) -> Result<()> {
let (reason, rationale) = match finish {
Finish::Slept { reply } => (
format!("slept: {}", reply.night),
/// What a night closes with: the reason (her line, or the failure) and the rationale (the counts, and where her
/// day was covered through).
fn closing(finish: &Finish) -> (String, String) {
match finish {
Finish::Slept { reply, through } => (
format!("{SLEPT}{}", reply.night),
format!(
"kept {}, consolidated {} from {}, retired {}, formed {}",
"kept {}, consolidated {} from {}, retired {}, formed {}{}",
reply.keep.len(),
reply.consolidate.len(),
reply.consolidate.iter().map(|consolidation| consolidation.consumes.len()).sum::<usize>(),
reply.retire.len(),
reply.remember.len()
reply.remember.len(),
through.map_or(String::new(), |ordinal| format!("{THROUGH}{ordinal}"))
),
),
Finish::Failed { refusals, why } => (format!("failed after {refusals} refusals: {why}"), "The night ended without a reply the ledger took.".to_string()),
};
}
}
/// Closes the night's run Recorded with her line and the counts, or the failure.
fn close(organs: &Organs, run: &PipelineRef, finish: &Finish) -> Result<()> {
let (reason, rationale) = closing(finish);
let resolution = PipelineResolution {
subject: run.clone(),
sequence: next_sequence(organs.mind, run)?,

View file

@ -239,24 +239,68 @@ fn retirements_and_remembered_things_become_resolutions_and_sleep_memories() {
assert_eq!((remembered.label.0.as_str(), &remembered.origin), ("mind-night-r0", &MemoryOrigin::Sleep { run: "mind-night".into() }));
}
/// Her mind with a Sleep run opened at `T0 + 1000` (and `earlier` Sleep runs before it), and `memories`
/// admitted at the given offsets.
/// Her mind with a Sleep run opened at `T0 + 1000` (and `earlier` Sleep runs, slept, a hundred hours before it),
/// and `memories` admitted at the given offsets: everything admitted in the order of its time.
fn night_over(earlier: usize, memories: &[(String, MemorySort, MemoryOrigin, i64)]) -> (TestMind, PipelineRef) {
let mind = TestMind::seeded();
let mut events: Vec<(i64, Vec<Vec<PipelineDocument>>)> = Vec::new();
for n in 0..earlier {
mind.admit_at(at(T0 - 100 * 3_600_000 + n as i64));
mind.committed(vec![run(&format!("before{n}"), RunTurn::Sleep, RunOperator::Mind, &[])]);
mind.committed(vec![crate::testkit::close(run_ref(&format!("before{n}")), crate::testkit::recorded())]);
let label = format!("before{n}");
let closed = crate::testkit::close(run_ref(&label), ResolutionOutcome::Recorded { reason: "slept: ok".into() });
events.push((T0 - 100 * 3_600_000 + n as i64, vec![vec![run(&label, RunTurn::Sleep, RunOperator::Mind, &[])], vec![closed]]));
}
for (label, sort, origin, offset) in memories {
mind.admit_at(at(T0 + offset));
mind.committed(vec![memory_from(label, *sort, "Text.", &[], origin.clone(), 500)]);
events.push((T0 + offset, vec![vec![memory_from(label, *sort, "Text.", &[], origin.clone(), 500)]]));
}
events.push((T0 + 1000, vec![vec![run("tonight", RunTurn::Sleep, RunOperator::Mind, &[])]]));
events.sort_by_key(|(when, _)| *when);
for (when, commits) in events {
mind.admit_at(at(when));
for commit in commits {
mind.committed(commit);
}
}
mind.admit_at(at(T0 + 1000));
mind.committed(vec![run("tonight", RunTurn::Sleep, RunOperator::Mind, &[])]);
(mind, run_ref("tonight"))
}
/// A night's own ending, admitted: slept through what it was shown, or failed.
fn close_night(mind: &TestMind, run: &PipelineRef, finish: &Finish) {
let (reason, rationale) = closing(finish);
mind.committed(vec![PipelineDocument::Resolution(PipelineResolution {
subject: run.clone(),
sequence: 1,
outcome: ResolutionOutcome::Recorded { reason: reason.as_str().into() },
rationale: rationale.as_str().into(),
resolved_on: eureka_pipeline::Date("2026-10-09".into()),
})]);
}
fn slept_over(context: &Context) -> Finish {
Finish::Slept { reply: SleepReply { night: "n".into(), keep: vec![], consolidate: vec![], retire: vec![], remember: vec![] }, through: context.day.last().map(|held| held.ordinal) }
}
fn failed() -> Finish {
Finish::Failed { refusals: 3, why: "no reply the ledger took".into() }
}
/// Opens another night of hers at `when` and reads its day.
fn next_night(mind: &TestMind, label: &str, when: i64) -> (PipelineRef, Context) {
mind.admit_at(at(T0 + when));
mind.committed(vec![run(label, RunTurn::Sleep, RunOperator::Mind, &[])]);
let next = run_ref(label);
let context = context(mind, &next, at(T0 + when + 5000)).unwrap();
(next, context)
}
fn day_ids(context: &Context) -> Vec<String> {
context.day.iter().map(|held| held.id.clone()).collect()
}
fn remember_at(mind: &TestMind, label: &str, origin: MemoryOrigin, when: i64) {
mind.admit_at(at(T0 + when));
mind.committed(vec![memory_from(label, MemorySort::Learned, "Text.", &[], origin, 500)]);
}
#[test]
fn the_day_is_capped_oldest_first_the_open_items_too_and_the_rest_wait() {
// No earlier night: her day is the 24 hours before the run opened; the asks are older than that.
@ -314,3 +358,71 @@ fn the_night_schema_states_the_reply_the_decoder_takes() {
assert_eq!(schema["properties"]["remember"]["maxItems"], json!(16));
assert_eq!(schema["properties"]["retire"]["items"]["properties"]["outcome"]["enum"], json!(["done", "declined", "faded", "wrong"]));
}
#[test]
fn what_the_cap_left_waits_for_the_next_night_and_is_not_dropped_to_the_backlog() {
let days: Vec<_> = (0..100).map(|n| (format!("day{n:03}"), MemorySort::Learned, wake(), 10 + n as i64)).collect();
let (mind, tonight) = night_over(0, &days);
let shown = context(&mind, &tonight, at(T0 + 5000)).unwrap();
assert_eq!(shown.day.len(), DAY_MAX);
assert!(shown.tide.is_empty() && shown.open.is_empty(), "what waits is neither tide nor open");
mind.admit_at(at(T0 + 2000));
close_night(&mind, &tonight, &slept_over(&shown));
let (_, next) = next_night(&mind, "tomorrow", 3000);
assert_eq!(day_ids(&next), (96..100).map(|n| id(&format!("day{n:03}"))).collect::<Vec<_>>(), "the four past the cap are tomorrow's day, and only they");
assert_eq!(next.tide.len(), 32, "the ninety-six she was shown are backlog now");
assert!(next.tide.iter().all(|held| !day_ids(&next).contains(&held.id)), "a day memory is not also in the tide");
}
#[test]
fn a_night_that_failed_or_never_closed_covers_nothing_so_its_day_comes_back() {
let days: Vec<_> = (0..3).map(|n| (format!("f{n}"), MemorySort::Learned, wake(), 10 + n as i64)).collect();
let (mind, first) = night_over(0, &days);
let shown = context(&mind, &first, at(T0 + 5000)).unwrap();
assert_eq!(day_ids(&shown).len(), 3);
mind.admit_at(at(T0 + 2000));
close_night(&mind, &first, &failed());
let (second, again) = next_night(&mind, "second", 3000);
assert_eq!(day_ids(&again), vec![id("f0"), id("f1"), id("f2")], "a failed first night: the day is still the 24 hours before the first night opened");
// The second night never closes (a crash); a third night still owes the same day.
remember_at(&mind, "f3", wake(), 3500);
let (_, third) = next_night(&mind, "third", 4000);
assert_eq!(day_ids(&third), vec![id("f0"), id("f1"), id("f2"), id("f3")]);
// Once a night sleeps through what it was shown, that is covered and the next day begins after it.
mind.admit_at(at(T0 + 4500));
close_night(&mind, &second, &slept_over(&again));
remember_at(&mind, "g0", wake(), 5000);
let (_, fourth) = next_night(&mind, "fourth", 6000);
assert_eq!(day_ids(&fourth), vec![id("f3"), id("g0")], "the night that slept covered f0 to f2; f3 arrived while it was open and is still owed");
}
#[test]
fn the_first_nights_day_is_the_twenty_four_hours_before_it_opened_to_the_millisecond() {
let hour = 3_600_000;
let memories = vec![
("on_the_line".to_string(), MemorySort::Learned, wake(), 1000 - 24 * hour),
("just_after".to_string(), MemorySort::Learned, wake(), 1001 - 24 * hour),
];
let (mind, run) = night_over(0, &memories);
let night = context(&mind, &run, at(T0 + 5000)).unwrap();
assert_eq!(day_ids(&night), vec![id("just_after")]);
assert!(night.tide.iter().any(|held| held.id == id("on_the_line")), "the older one is backlog");
}
#[test]
fn a_consolidation_a_night_made_is_not_the_next_days_memory_whatever_its_origin() {
let (mind, tonight) = night_over(0, &[("d1".to_string(), MemorySort::Learned, dm("!a:chat", &["@a:chat"]), 10)]);
let shown = context(&mind, &tonight, at(T0 + 5000)).unwrap();
// The night consolidates the DM memory: the consolidation keeps the room, so its origin is Direct, admitted after the night opened.
mind.admit_at(at(T0 + 2000));
let PipelineDocument::Memory(mut merged) = memory_from("dm-merged", MemorySort::Learned, "Merged.", &[], dm("!a:chat", &["@a:chat"]), 500) else { unreachable!() };
merged.consolidates = vec![crate::testkit::reference(eureka_pipeline::PipelineKind::Memory, &id("d1"))];
let by = crate::testkit::reference(eureka_pipeline::PipelineKind::Memory, &id("dm-merged"));
mind.committed(vec![PipelineDocument::Memory(merged), crate::testkit::close(crate::testkit::reference(eureka_pipeline::PipelineKind::Memory, &id("d1")), ResolutionOutcome::Superseded { by: vec![by] })]);
mind.admit_at(at(T0 + 2500));
close_night(&mind, &tonight, &slept_over(&shown));
remember_at(&mind, "fresh", wake(), 2800);
let (_, next) = next_night(&mind, "tomorrow", 3000);
assert_eq!(day_ids(&next), vec![id("fresh")], "the consolidation is not shown as her day");
assert!(next.tide.iter().all(|held| held.id != id("dm-merged")), "nor does it come round as backlog");
}