//! Tests for the heartbeat tick. Do not edit. mod support; use bxcore::{SessionId, Timestamp}; use loopd::channel::Context; use loopd::llama::Client; use loopd::memory::NoMemory; use loopd::scheduler::{Scheduler, TickWork}; use loopd::tools::Registry; use std::sync::Arc; use support::{FakeServer, Home, Reply, ScriptedPort}; const CHAT: &str = "/v1/chat/completions"; const PROMPT: &str = "Look around.\n"; fn sse() -> Reply { let data1 = r#"data: {"choices":[{"finish_reason":null,"index":0,"delta":{"content":"noted"}}],"id":"h","timings":{"cache_n":0,"prompt_n":10,"predicted_n":1}}"#; let data2 = r#"data: {"choices":[{"finish_reason":"stop","index":0,"delta":{}}],"id":"h","timings":{"cache_n":0,"prompt_n":10,"predicted_n":1}}"#; let body = format!("{data1}\n\n{data2}\n\ndata: [DONE]\n\n"); Reply::raw(format!( "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() )) } fn ts(s: &str) -> Timestamp { Timestamp::parse(s).unwrap() } fn world(every_s: u64) -> (Home, FakeServer, Arc) { let home = Home::new(); home.write("heartbeat.md", PROMPT); home.write("compact.md", "Summarise.\n"); let server = FakeServer::start(); let mut cfg = home.config(&server.socket); cfg.heartbeat.every_s = every_s; cfg.heartbeat.prompt = home.dir.join("heartbeat.md"); server.route(CHAT, vec![sse()]); let ctx = Arc::new(Context::new( cfg.clone(), Client::new(cfg), Box::new(ScriptedPort::new(vec![])), Registry::m2b(), Box::new(NoMemory), )); (home, server, ctx) } #[test] fn every_s_zero_does_nothing() { let (_home, server, ctx) = world(0); let mut sched = Scheduler::new(ts("2026-10-03T07:00:00.000Z")); let work = sched.tick(&ctx, ts("2026-10-03T08:00:00.000Z")); assert!(work.is_empty()); assert!(server.requests_to(CHAT).is_empty()); } #[test] fn a_due_heartbeat_runs_on_the_background_slot_with_a_time_line() { let (home, server, ctx) = world(60); let mut sched = Scheduler::new(ts("2026-10-03T07:00:00.000Z")); let work = sched.tick(&ctx, ts("2026-10-03T07:01:00.000Z")); assert_eq!(work, vec![TickWork::Heartbeat]); let sent = server.requests_to(CHAT); assert_eq!(sent.len(), 1); assert_eq!(sent[0].json()["id_slot"], 1); let body = sent[0].json(); let last = body["messages"].as_array().unwrap().last().unwrap(); assert_eq!(last["role"], "user"); let content = last["content"].as_str().unwrap(); assert!( content.starts_with("It is 2026-10-03T07:01:00.000Z."), "{content}" ); assert!(content.contains("Look around."), "{content}"); let s = loopd::session::Session::open(&home.dir, SessionId::new("heartbeat").unwrap()).unwrap(); assert_eq!(s.slot(), 1); } #[test] fn a_second_tick_before_every_s_does_not_run() { let (_home, server, ctx) = world(60); let mut sched = Scheduler::new(ts("2026-10-03T07:00:00.000Z")); sched.tick(&ctx, ts("2026-10-03T07:01:00.000Z")); let work = sched.tick(&ctx, ts("2026-10-03T07:01:30.000Z")); assert!(work.is_empty()); assert_eq!(server.requests_to(CHAT).len(), 1); } #[test] fn a_busy_heartbeat_session_is_skipped() { let (_home, server, ctx) = world(60); let held = ctx .hold(SessionId::new("heartbeat").unwrap()) .expect("hold"); let mut sched = Scheduler::new(ts("2026-10-03T07:00:00.000Z")); let work = sched.tick(&ctx, ts("2026-10-03T07:01:00.000Z")); assert_eq!( work, vec![TickWork::SkippedBusy { session: SessionId::new("heartbeat").unwrap() }] ); assert!(server.requests_to(CHAT).is_empty()); drop(held); } #[test] fn an_unreadable_prompt_is_skipped_and_names_the_runbook() { let home = Home::new(); let path = home.dir.join("heartbeat.md"); std::fs::create_dir(&path).unwrap(); let server = FakeServer::start(); let mut cfg = home.config(&server.socket); cfg.heartbeat.every_s = 60; cfg.heartbeat.prompt = path; let ctx = Arc::new(Context::new( cfg.clone(), Client::new(cfg), Box::new(ScriptedPort::new(vec![])), Registry::m2b(), Box::new(NoMemory), )); let mut sched = Scheduler::new(ts("2026-10-03T07:00:00.000Z")); let work = sched.tick(&ctx, ts("2026-10-03T07:01:00.000Z")); assert_eq!(work, vec![TickWork::HeartbeatUnreadable]); assert!(server.requests_to(CHAT).is_empty()); } #[test] fn heartbeat_has_no_speaker() { let (home, _server, ctx) = world(60); let mut sched = Scheduler::new(ts("2026-10-03T07:00:00.000Z")); sched.tick(&ctx, ts("2026-10-03T07:01:00.000Z")); let recs = home.records("heartbeat"); let user = recs .iter() .find_map(|r| match r { bxcore::LogRecord::User { speaker, .. } => Some(speaker), _ => None, }) .unwrap(); assert_eq!(user, &None); }