1use std::path::{Path, PathBuf};
15use std::time::{SystemTime, UNIX_EPOCH};
16
17pub use leviath_core::run_meta::{
22 ContextSnapshot, RegionEntrySnapshot, RegionSnapshot, RunMeta, RunStatus, StageRecord,
23 StageRunStatus,
24};
25
26pub fn write_context_snapshot(run_id: &str, snap: &ContextSnapshot) -> anyhow::Result<()> {
28 write_context_snapshot_to(&run_dir(run_id), snap)
29}
30
31fn write_json_atomic(path: &std::path::Path, json: &str) -> anyhow::Result<()> {
41 let tmp = path.with_extension("json.tmp");
42 leviath_sys::write_private(&tmp, json.as_bytes())?;
49 std::fs::rename(&tmp, path)?;
50 Ok(())
51}
52
53fn write_context_snapshot_to(dir: &std::path::Path, snap: &ContextSnapshot) -> anyhow::Result<()> {
54 let json = serde_json::to_string_pretty(snap)
55 .expect("infallible: ContextSnapshot always serializes to JSON");
56 write_json_atomic(&dir.join("context.json"), &json)
57}
58
59pub fn read_context_snapshot(run_id: &str) -> Option<ContextSnapshot> {
61 let path = run_dir(run_id).join("context.json");
62 let json = std::fs::read_to_string(&path).ok()?;
63 serde_json::from_str(&json).ok()
64}
65
66pub fn read_run_archive(run_id: &str) -> Option<Vec<leviath_core::run_archive::RunRecord>> {
69 let path = run_dir(run_id).join("run.lvr");
70 let bytes = std::fs::read(&path).ok()?;
71 leviath_core::run_archive::read_archive(&mut bytes.as_slice())
72 .ok()
73 .map(|(_version, records)| records)
74}
75
76pub fn context_history(run_id: &str) -> Vec<leviath_core::run_archive::RunPoint> {
79 read_run_archive(run_id)
80 .map(|records| leviath_core::run_archive::replay_points(&records))
81 .unwrap_or_default()
82}
83
84fn now_secs() -> i64 {
85 SystemTime::now()
86 .duration_since(UNIX_EPOCH)
87 .map(|d| d.as_secs() as i64)
88 .unwrap_or(0)
89}
90
91fn runs_dir_from(env_override: Option<&str>) -> PathBuf {
101 if let Some(dir) = env_override {
102 return PathBuf::from(dir);
103 }
104 leviath_core::paths::data_dir()
105 .unwrap_or_default()
106 .join("runs")
107}
108
109pub fn runs_dir() -> PathBuf {
111 runs_dir_from(std::env::var("LEVIATH_RUNS_DIR").ok().as_deref())
112}
113
114pub fn run_dir(run_id: &str) -> PathBuf {
126 if !leviath_core::is_safe_path_component(run_id) {
127 tracing::warn!(run_id = %run_id, "rejected an unsafe run id");
128 return runs_dir().join("<invalid>");
129 }
130 runs_dir().join(run_id)
131}
132
133fn dashboard_log_path_from(env_override: Option<&str>) -> PathBuf {
137 if let Some(path) = env_override {
138 return PathBuf::from(path);
139 }
140 leviath_core::paths::data_dir()
141 .unwrap_or_default()
142 .join("dashboard.log")
143}
144
145pub fn dashboard_log_path() -> PathBuf {
157 match std::env::var("LEVIATH_DASHBOARD_LOG_PATH") {
158 Ok(path) => dashboard_log_path_from(Some(&path)),
159 Err(_) => dashboard_log_path_from(None),
160 }
161}
162
163pub fn append_dashboard_log(msg: &str) {
166 append_dashboard_log_to(&dashboard_log_path(), msg);
167}
168
169pub fn append_dashboard_log_to(path: &Path, msg: &str) {
176 append_dashboard_log_capped(path, msg, DASHBOARD_LOG_MAX_BYTES);
177}
178
179const DASHBOARD_LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
183
184fn append_dashboard_log_capped(path: &Path, msg: &str, max_bytes: u64) {
187 use std::io::Write;
188 if let Some(parent) = path.parent() {
190 let _ = std::fs::create_dir_all(parent);
191 }
192 roll_log_if_over_cap(path, max_bytes);
193 if let Ok(mut file) = std::fs::OpenOptions::new()
194 .create(true)
195 .append(true)
196 .open(path)
197 {
198 let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
199 let _ = writeln!(file, "{} {}", timestamp, msg);
200 }
201}
202
203fn rolled_log_path(path: &Path) -> PathBuf {
205 let mut name = path.as_os_str().to_owned();
206 name.push(".1");
207 PathBuf::from(name)
208}
209
210fn roll_log_if_over_cap(path: &Path, max_bytes: u64) {
215 let over = std::fs::metadata(path)
216 .map(|m| m.len() >= max_bytes)
217 .unwrap_or(false);
218 if over {
219 let _ = std::fs::rename(path, rolled_log_path(path));
220 }
221}
222
223const RUN_ID_ENTROPY_BITS: u32 = 48;
228
229pub fn new_run_id(agent_name: &str) -> String {
245 use rand::RngExt as _;
246 let entropy: u64 = rand::rng().random::<u64>() >> (u64::BITS - RUN_ID_ENTROPY_BITS);
247 let safe_name = agent_name.replace(|c: char| !c.is_alphanumeric() && c != '-', "-");
248 format!("{}-{}-{:012x}", safe_name, now_secs(), entropy)
249}
250
251pub fn create_run(meta: &RunMeta) -> anyhow::Result<()> {
253 create_run_in(&run_dir(&meta.run_id), meta)
254}
255
256pub(crate) fn create_run_in(dir: &std::path::Path, meta: &RunMeta) -> anyhow::Result<()> {
262 std::fs::create_dir_all(dir)?;
263
264 let _ = leviath_sys::secure_dir_perms(dir);
266
267 write_meta_to(dir, meta)
268}
269
270pub fn write_meta(meta: &RunMeta) -> anyhow::Result<()> {
272 write_meta_to(&run_dir(&meta.run_id), meta)
273}
274
275pub(crate) fn write_meta_to(dir: &std::path::Path, meta: &RunMeta) -> anyhow::Result<()> {
281 let json =
282 serde_json::to_string_pretty(meta).expect("infallible: RunMeta always serializes to JSON");
283 write_json_atomic(&dir.join("meta.json"), &json)
284}
285
286pub fn read_meta(run_id: &str) -> anyhow::Result<RunMeta> {
288 read_meta_from(&run_dir(run_id))
289}
290
291pub fn is_terminal_status(status: &RunStatus) -> bool {
295 matches!(
296 status,
297 RunStatus::Complete
298 | RunStatus::CompleteInteractive
299 | RunStatus::Error
300 | RunStatus::Cancelled
301 )
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum ForceCancelOutcome {
307 Cancelled,
309 AlreadyTerminal,
311 NoSuchRun,
313 WriteFailed,
315}
316
317impl ForceCancelOutcome {
318 pub fn found_run(&self) -> bool {
321 !matches!(self, Self::NoSuchRun)
322 }
323}
324
325pub fn force_cancel(run_id: &str) -> ForceCancelOutcome {
328 force_cancel_in(&run_dir(run_id), now_secs())
329}
330
331pub fn force_cancel_in(run_dir: &Path, now: i64) -> ForceCancelOutcome {
343 if !run_dir.is_dir() {
344 return ForceCancelOutcome::NoSuchRun;
345 }
346 let run_id = run_dir
347 .file_name()
348 .map(|n| n.to_string_lossy().into_owned())
349 .unwrap_or_default();
350 let cancelled = match read_meta_from(run_dir) {
351 Ok(meta) if is_terminal_status(&meta.status) => return ForceCancelOutcome::AlreadyTerminal,
352 Ok(meta) => RunMeta {
353 status: RunStatus::Cancelled,
354 updated_at: now,
355 ..meta
356 },
357 Err(_) => RunMeta {
360 status: RunStatus::Cancelled,
361 updated_at: now,
362 error: Some("run metadata was unreadable; cancelled".to_string()),
363 ..RunMeta::new(
364 run_id.clone(),
365 run_id,
366 String::new(),
367 String::new(),
368 None,
369 String::new(),
370 0,
371 )
372 },
373 };
374 match write_meta_to(run_dir, &cancelled) {
375 Ok(()) => ForceCancelOutcome::Cancelled,
376 Err(e) => {
377 let path = run_dir.display().to_string();
381 tracing::warn!(
382 run_dir = %path,
383 error = %e,
384 "could not force a run to cancelled on disk"
385 );
386 ForceCancelOutcome::WriteFailed
387 }
388 }
389}
390
391pub(crate) fn read_meta_from(dir: &std::path::Path) -> anyhow::Result<RunMeta> {
394 let path = dir.join("meta.json");
395 let json = std::fs::read_to_string(&path)?;
396 Ok(serde_json::from_str(&json)?)
397}
398
399fn list_runs_in_dir(dir: PathBuf) -> Vec<RunMeta> {
402 if !dir.exists() {
403 return Vec::new();
404 }
405
406 let mut runs = Vec::new();
407
408 if let Ok(entries) = std::fs::read_dir(&dir) {
409 for entry in entries.filter_map(|e| e.ok()) {
410 let meta_path = entry.path().join("meta.json");
411 if let Ok(json) = std::fs::read_to_string(&meta_path)
412 && let Ok(meta) = serde_json::from_str::<RunMeta>(&json)
413 {
414 runs.push(meta);
415 }
416 }
417 }
418
419 runs.sort_by_key(|r| std::cmp::Reverse(r.started_at));
420 runs
421}
422
423pub fn list_runs() -> Vec<RunMeta> {
426 list_runs_in_dir(runs_dir())
427}
428
429pub fn tail_file(path: &std::path::Path, max_bytes: u64) -> String {
434 use std::io::{Read, Seek, SeekFrom};
435
436 let mut file = match std::fs::File::open(path) {
437 Ok(f) => f,
438 Err(_) => return String::new(),
439 };
440
441 let file_size = file.metadata().map(|m| m.len()).unwrap_or(0);
445
446 if file_size <= max_bytes {
447 let mut buf = Vec::new();
448 let _ = file.read_to_end(&mut buf);
449 return String::from_utf8_lossy(&buf).to_string();
450 }
451
452 let offset = file_size - max_bytes;
453 let _ = file.seek(SeekFrom::Start(offset));
454
455 let mut buf = Vec::new();
456 let _ = file.read_to_end(&mut buf);
457
458 if let Some(nl) = buf.iter().position(|&b| b == b'\n') {
460 String::from_utf8_lossy(&buf[nl + 1..]).to_string()
461 } else {
462 String::from_utf8_lossy(&buf).to_string()
463 }
464}
465
466pub fn stage_dir(run_id: &str, stage_idx: usize) -> PathBuf {
470 run_dir(run_id).join("stages").join(stage_idx.to_string())
471}
472
473pub fn write_stages_index(run_id: &str, stages: &[StageRecord]) -> anyhow::Result<()> {
475 write_stages_index_to(&run_dir(run_id), stages)
476}
477
478fn write_stages_index_to(dir: &std::path::Path, stages: &[StageRecord]) -> anyhow::Result<()> {
479 let json = serde_json::to_string_pretty(&stages)
480 .expect("infallible: StageRecord slice always serializes to JSON");
481 write_json_atomic(&dir.join("stages.json"), &json)
482}
483
484pub fn read_stages_index(run_id: &str) -> Vec<StageRecord> {
486 let path = run_dir(run_id).join("stages.json");
487 let json = match std::fs::read_to_string(&path) {
488 Ok(j) => j,
489 Err(_) => return Vec::new(),
490 };
491 serde_json::from_str(&json).unwrap_or_default()
492}
493
494fn ensure_stage_dir(run_id: &str, stage_idx: usize) {
496 let dir = stage_dir(run_id, stage_idx);
497 let _ = std::fs::create_dir_all(&dir);
498}
499
500pub fn append_stage_output(run_id: &str, stage_idx: usize, text: &str) {
502 use std::io::Write;
503 ensure_stage_dir(run_id, stage_idx);
504 let path = stage_dir(run_id, stage_idx).join("output.log");
505 if let Ok(mut file) = std::fs::OpenOptions::new()
506 .create(true)
507 .append(true)
508 .open(&path)
509 {
510 let _ = writeln!(file, "{}", text);
511 }
512}
513
514pub fn append_stage_log(run_id: &str, stage_idx: usize, text: &str) {
516 use std::io::Write;
517 ensure_stage_dir(run_id, stage_idx);
518 let path = stage_dir(run_id, stage_idx).join("logs.log");
519 if let Ok(mut file) = std::fs::OpenOptions::new()
520 .create(true)
521 .append(true)
522 .open(&path)
523 {
524 let _ = writeln!(file, "{}", text);
525 }
526}
527
528pub fn write_stage_context(
530 run_id: &str,
531 stage_idx: usize,
532 snap: &ContextSnapshot,
533) -> anyhow::Result<()> {
534 ensure_stage_dir(run_id, stage_idx);
535 write_context_snapshot_to(&stage_dir(run_id, stage_idx), snap)
536}
537
538pub fn read_stage_context(run_id: &str, stage_idx: usize) -> Option<ContextSnapshot> {
540 let path = stage_dir(run_id, stage_idx).join("context.json");
541 let json = std::fs::read_to_string(&path).ok()?;
542 serde_json::from_str(&json).ok()
543}
544
545pub fn tail_stage_output(run_id: &str, stage_idx: usize, max_bytes: u64) -> String {
547 tail_file(&stage_dir(run_id, stage_idx).join("output.log"), max_bytes)
548}
549
550pub fn tail_stage_log(run_id: &str, stage_idx: usize, max_bytes: u64) -> String {
552 tail_file(&stage_dir(run_id, stage_idx).join("logs.log"), max_bytes)
553}
554
555#[cfg(test)]
567fn make_runs_base_dir(unique: &str) -> std::path::PathBuf {
568 use std::hash::{Hash, Hasher};
569 let mut hasher = std::collections::hash_map::DefaultHasher::new();
570 unique.hash(&mut hasher);
571 let short = format!("{:x}", hasher.finish() & 0xffff_ffff);
572 let base_dir = dirs::home_dir()
573 .unwrap_or_default()
574 .join(".leviath-test")
575 .join(format!("rs-{short}"));
576 let _ = std::fs::create_dir_all(base_dir.join("runs"));
577 base_dir
578}
579
580#[cfg(test)]
583fn runs_dir_isolation_vars(
584 base_dir: &std::path::Path,
585) -> [(&'static str, Option<std::ffi::OsString>); 2] {
586 [
587 (
588 "LEVIATH_RUNS_DIR",
589 Some(base_dir.join("runs").into_os_string()),
590 ),
591 (
592 "LEVIATH_DASHBOARD_LOG_PATH",
593 Some(base_dir.join("dashboard.log").into_os_string()),
594 ),
595 ]
596}
597
598#[cfg(test)]
603pub(crate) fn with_isolated_runs_dir<R>(unique: &str, f: impl FnOnce(&std::path::Path) -> R) -> R {
604 let base_dir = make_runs_base_dir(unique);
605 let result = temp_env::with_vars(runs_dir_isolation_vars(&base_dir), || f(&base_dir));
606 let _ = std::fs::remove_dir_all(&base_dir);
607 result
608}
609
610#[cfg(test)]
612pub(crate) async fn with_isolated_runs_dir_async<R, Fut>(
613 unique: &str,
614 f: impl FnOnce(std::path::PathBuf) -> Fut,
615) -> R
616where
617 Fut: std::future::Future<Output = R>,
618{
619 let base_dir = make_runs_base_dir(unique);
620 let result =
621 temp_env::async_with_vars(runs_dir_isolation_vars(&base_dir), f(base_dir.clone())).await;
622 let _ = std::fs::remove_dir_all(&base_dir);
623 result
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629
630 #[test]
635 fn run_dir_refuses_an_unsafe_run_id() {
636 crate::test_support::with_tracing(|| {
637 for bad in ["../../etc", "/etc/passwd", "..", "a/b"] {
638 let dir = run_dir(bad);
639 let shown = dir.display().to_string();
640 assert!(dir.ends_with("<invalid>"), "{bad} resolved to {shown}");
641 assert!(!dir.exists(), "{bad} must not resolve to a real path");
642 }
643 assert!(run_dir("run-abc123").ends_with("run-abc123"));
645 });
646 }
647
648 #[test]
649 fn write_json_atomic_fs_write_failure() {
650 let path = std::path::Path::new("/nonexistent/leviath/runstate-cov/out.json");
653 let result = write_json_atomic(path, "{}");
654 assert!(result.is_err());
655 assert!(!path.exists());
656 }
657
658 #[test]
661 fn run_status_serde_roundtrip() {
662 for status in [
663 RunStatus::Starting,
664 RunStatus::Running,
665 RunStatus::WaitingInput,
666 RunStatus::Complete,
667 RunStatus::CompleteInteractive,
668 RunStatus::Error,
669 RunStatus::Cancelled,
670 ] {
671 let json = serde_json::to_string(&status).unwrap();
672 let back: RunStatus = serde_json::from_str(&json).unwrap();
673 assert_eq!(status, back);
674 }
675 }
676
677 #[test]
678 fn run_status_display() {
679 assert_eq!(RunStatus::Starting.to_string(), "Starting");
680 assert_eq!(RunStatus::Running.to_string(), "Running");
681 assert_eq!(RunStatus::WaitingInput.to_string(), "WaitingInput");
682 assert_eq!(RunStatus::Complete.to_string(), "Complete");
683 assert_eq!(
684 RunStatus::CompleteInteractive.to_string(),
685 "CompleteInteractive"
686 );
687 assert_eq!(RunStatus::Error.to_string(), "Error");
688 assert_eq!(RunStatus::Cancelled.to_string(), "Cancelled");
689 }
690
691 #[test]
692 fn run_status_snake_case_serialization() {
693 let json = serde_json::to_string(&RunStatus::WaitingInput).unwrap();
694 assert_eq!(json, "\"waiting_input\"");
695 let json = serde_json::to_string(&RunStatus::CompleteInteractive).unwrap();
696 assert_eq!(json, "\"complete_interactive\"");
697 }
698
699 #[test]
702 fn stage_run_status_serde_roundtrip() {
703 for status in [
704 StageRunStatus::Pending,
705 StageRunStatus::Active,
706 StageRunStatus::WaitingInput,
707 StageRunStatus::Complete,
708 StageRunStatus::Error,
709 ] {
710 let json = serde_json::to_string(&status).unwrap();
711 let back: StageRunStatus = serde_json::from_str(&json).unwrap();
712 assert_eq!(status, back);
713 }
714 }
715
716 #[test]
717 fn stage_run_status_display() {
718 assert_eq!(StageRunStatus::Pending.to_string(), "Pending");
719 assert_eq!(StageRunStatus::Active.to_string(), "Active");
720 assert_eq!(StageRunStatus::WaitingInput.to_string(), "WaitingInput");
721 assert_eq!(StageRunStatus::Complete.to_string(), "Complete");
722 assert_eq!(StageRunStatus::Error.to_string(), "Error");
723 }
724
725 #[test]
728 fn run_meta_new_defaults() {
729 let meta = RunMeta::new(
730 "run-1".into(),
731 "agent".into(),
732 "/path".into(),
733 "do stuff".into(),
734 Some("gpt-4".into()),
735 "/work".into(),
736 3,
737 );
738 assert_eq!(meta.run_id, "run-1");
739 assert_eq!(meta.agent_name, "agent");
740 assert_eq!(meta.task, "do stuff");
741 assert_eq!(meta.model.as_deref(), Some("gpt-4"));
742 assert_eq!(meta.num_stages, 3);
743 assert_eq!(meta.status, RunStatus::Starting);
744 assert_eq!(meta.pid, 0);
745 assert_eq!(meta.stage_index, 0);
746 assert!(meta.error.is_none());
747 assert!(meta.title.is_none());
748 assert!(meta.metadata.is_empty());
749 assert!(meta.callback_url.is_none());
750 assert!(meta.parent_run_id.is_none());
751 }
752
753 #[test]
754 fn run_meta_serde_roundtrip() {
755 let meta = RunMeta::new(
756 "test-run".into(),
757 "test-agent".into(),
758 "/agents/test".into(),
759 "run tests".into(),
760 None,
761 "/tmp".into(),
762 2,
763 );
764 let json = serde_json::to_string_pretty(&meta).unwrap();
765 let back: RunMeta = serde_json::from_str(&json).unwrap();
766 assert_eq!(back.run_id, "test-run");
767 assert_eq!(back.agent_name, "test-agent");
768 assert_eq!(back.num_stages, 2);
769 assert!(back.model.is_none());
770 }
771
772 #[test]
773 fn run_meta_touch_updates_timestamp() {
774 let mut meta = RunMeta::new(
775 "r".into(),
776 "a".into(),
777 "/p".into(),
778 "t".into(),
779 None,
780 "/w".into(),
781 1,
782 );
783 let before = meta.updated_at;
784 meta.touch();
786 assert!(meta.updated_at >= before);
787 }
788
789 #[test]
790 fn run_meta_optional_fields_deserialize() {
791 let json = serde_json::json!({
793 "run_id": "r1",
794 "agent_name": "a",
795 "agent_path": "/p",
796 "task": "t",
797 "model": null,
798 "pid": 123,
799 "status": "running",
800 "current_stage": "init",
801 "stage_index": 0,
802 "num_stages": 1,
803 "iteration": 0,
804 "prompt_tokens": 0,
805 "completion_tokens": 0,
806 "workdir": "/w",
807 "started_at": 1000,
808 "updated_at": 1000,
809 "error": null
810 });
811 let meta: RunMeta = serde_json::from_value(json).unwrap();
812 assert_eq!(meta.cached_tokens, 0);
813 assert!(meta.title.is_none());
814 assert!(meta.metadata.is_empty());
815 assert!(meta.callback_url.is_none());
816 assert!(meta.parent_run_id.is_none());
817 }
818
819 #[test]
822 fn stage_record_new_defaults() {
823 let rec = StageRecord::new("analyze".into(), 2);
824 assert_eq!(rec.name, "analyze");
825 assert_eq!(rec.index, 2);
826 assert_eq!(rec.status, StageRunStatus::Pending);
827 assert_eq!(rec.prompt_tokens, 0);
828 assert_eq!(rec.completion_tokens, 0);
829 assert_eq!(rec.cached_tokens, 0);
830 assert!(rec.started_at.is_none());
831 assert!(rec.ended_at.is_none());
832 }
833
834 #[test]
835 fn stage_record_serde_roundtrip() {
836 let mut rec = StageRecord::new("build".into(), 0);
837 rec.status = StageRunStatus::Complete;
838 rec.prompt_tokens = 100;
839 rec.started_at = Some(1000);
840 rec.ended_at = Some(2000);
841
842 let json = serde_json::to_string(&rec).unwrap();
843 let back: StageRecord = serde_json::from_str(&json).unwrap();
844 assert_eq!(back.name, "build");
845 assert_eq!(back.status, StageRunStatus::Complete);
846 assert_eq!(back.prompt_tokens, 100);
847 assert_eq!(back.started_at, Some(1000));
848 }
849
850 #[test]
853 fn region_snapshot_serde_roundtrip() {
854 let snap = RegionSnapshot {
855 name: "system".into(),
856 kind: "pinned".into(),
857 current_tokens: 100,
858 max_tokens: 500,
859 entries: vec![RegionEntrySnapshot {
860 content: "You are helpful".into(),
861 tokens: 3,
862 kind: Default::default(),
863 metadata: None,
864 key: None,
865 taint: Default::default(),
866 }],
867 };
868 let json = serde_json::to_string(&snap).unwrap();
869 let back: RegionSnapshot = serde_json::from_str(&json).unwrap();
870 assert_eq!(back.name, "system");
871 assert_eq!(back.entries.len(), 1);
872 assert_eq!(back.entries[0].content, "You are helpful");
873 }
874
875 #[test]
876 fn region_snapshot_empty_entries_omitted() {
877 let snap = RegionSnapshot {
878 name: "empty".into(),
879 kind: "temporary".into(),
880 current_tokens: 0,
881 max_tokens: 100,
882 entries: vec![],
883 };
884 let json = serde_json::to_value(&snap).unwrap();
885 assert!(json.get("entries").is_none());
886 }
887
888 #[test]
889 fn context_snapshot_serde_roundtrip() {
890 let snap = ContextSnapshot {
891 stage_name: "analyze".into(),
892 total_tokens: 500,
893 max_tokens: 8192,
894 regions: vec![RegionSnapshot {
895 name: "history".into(),
896 kind: "sliding".into(),
897 current_tokens: 300,
898 max_tokens: 2000,
899 entries: vec![],
900 }],
901 };
902 let json = serde_json::to_string(&snap).unwrap();
903 let back: ContextSnapshot = serde_json::from_str(&json).unwrap();
904 assert_eq!(back.stage_name, "analyze");
905 assert_eq!(back.total_tokens, 500);
906 assert_eq!(back.regions.len(), 1);
907 }
908
909 #[test]
912 fn tail_file_nonexistent_returns_empty() {
913 let path = std::path::Path::new("/tmp/nonexistent-leviath-test-file.txt");
914 assert_eq!(tail_file(path, 1024), "");
915 }
916
917 #[test]
918 fn tail_file_small_file_returns_all() {
919 let dir = tempfile::tempdir().unwrap();
920 let path = dir.path().join("small.txt");
921 std::fs::write(&path, "line1\nline2\nline3\n").unwrap();
922 let result = tail_file(&path, 1024);
923 assert_eq!(result, "line1\nline2\nline3\n");
924 }
925
926 #[test]
927 fn tail_file_large_file_returns_tail() {
928 let dir = tempfile::tempdir().unwrap();
929 let path = dir.path().join("large.txt");
930 let content = "abcdefghij\n".repeat(100); std::fs::write(&path, &content).unwrap();
932 let result = tail_file(&path, 50);
933 assert!(result.len() <= 50);
935 assert!(result.ends_with('\n'));
936 }
937
938 #[test]
941 fn new_run_id_contains_agent_name() {
942 let id = new_run_id("my-agent");
943 assert!(id.starts_with("my-agent-"));
944 }
945
946 #[test]
947 fn new_run_id_sanitizes_special_chars() {
948 let id = new_run_id("agent with spaces!");
949 assert!(!id.contains(' '));
950 assert!(!id.contains('!'));
951 }
952
953 #[test]
954 fn new_run_id_is_unique_across_rapid_calls_in_same_second() {
955 let ids: std::collections::HashSet<String> =
958 (0..100).map(|_| new_run_id("same-agent")).collect();
959 assert_eq!(ids.len(), 100);
960 }
961
962 fn split_run_id(id: &str) -> (&str, &str) {
965 let mut parts = id.rsplitn(3, '-');
966 let suffix = parts.next().expect("run id has a suffix");
967 let secs = parts.next().expect("run id has a timestamp");
968 (secs, suffix)
969 }
970
971 #[test]
972 fn new_run_id_suffix_is_random_not_derived_from_the_clock() {
973 let ids: Vec<String> = (0..200).map(|_| new_run_id("same-agent")).collect();
981 let mut by_second: std::collections::HashMap<&str, Vec<&str>> =
982 std::collections::HashMap::new();
983 for id in &ids {
984 let (secs, suffix) = split_run_id(id);
985 by_second.entry(secs).or_default().push(suffix);
986 }
987 let mut largest = 0;
988 for (secs, suffixes) in &by_second {
989 let distinct: std::collections::HashSet<&&str> = suffixes.iter().collect();
990 assert_eq!(
991 distinct.len(),
992 suffixes.len(),
993 "two runs in second {secs} share a suffix: {suffixes:?}"
994 );
995 largest = largest.max(suffixes.len());
996 }
997 assert!(
1000 largest > 1,
1001 "expected IDs sharing a second, got {by_second:?}"
1002 );
1003 }
1004
1005 #[test]
1008 fn write_and_read_meta_roundtrip() {
1009 with_isolated_runs_dir("write-and-read-meta-roundtrip", |_d| {
1013 let meta = RunMeta::new(
1014 "test-roundtrip-unit".into(),
1015 "test-agent".into(),
1016 "/agents/test".into(),
1017 "unit test".into(),
1018 Some("model-x".into()),
1019 "/tmp".into(),
1020 2,
1021 );
1022
1023 create_run(&meta).unwrap();
1024 let back = read_meta(&meta.run_id).unwrap();
1025 assert_eq!(back.run_id, "test-roundtrip-unit");
1026 assert_eq!(back.agent_name, "test-agent");
1027 assert_eq!(back.task, "unit test");
1028 assert_eq!(back.model.as_deref(), Some("model-x"));
1029 });
1030 }
1031
1032 #[test]
1033 fn read_meta_returns_err_on_corrupted_json() {
1034 with_isolated_runs_dir("read-meta-returns-err-on-corrupted-json", |_d| {
1037 let run_id = "corrupted-meta-run";
1038 let dir = run_dir(run_id);
1039 std::fs::create_dir_all(&dir).unwrap();
1040 std::fs::write(dir.join("meta.json"), "not valid json").unwrap();
1041
1042 let result = read_meta(run_id);
1043 assert!(result.is_err());
1044 });
1045 }
1046
1047 #[test]
1050 fn write_and_read_stages_index_roundtrip() {
1051 with_isolated_runs_dir("write-and-read-stages-index-roundtrip", |_d| {
1052 let run_id = "test-stages-idx-unit";
1053 let dir = run_dir(run_id);
1054 std::fs::create_dir_all(&dir).unwrap();
1055
1056 let stages = vec![
1057 StageRecord::new("init".into(), 0),
1058 StageRecord::new("process".into(), 1),
1059 ];
1060 write_stages_index(run_id, &stages).unwrap();
1061 let back = read_stages_index(run_id);
1062 assert_eq!(back.len(), 2);
1063 assert_eq!(back[0].name, "init");
1064 assert_eq!(back[1].name, "process");
1065 });
1066 }
1067
1068 #[test]
1069 fn read_stages_index_missing_returns_empty() {
1070 let back = read_stages_index("nonexistent-run-12345");
1071 assert!(back.is_empty());
1072 }
1073
1074 #[test]
1077 fn write_and_read_context_snapshot_roundtrip() {
1078 with_isolated_runs_dir("write-and-read-context-snapshot-roundtrip", |_d| {
1079 let run_id = "test-ctx-snap-unit";
1080 let dir = run_dir(run_id);
1081 std::fs::create_dir_all(&dir).unwrap();
1082
1083 let snap = ContextSnapshot {
1084 stage_name: "test".into(),
1085 total_tokens: 42,
1086 max_tokens: 8192,
1087 regions: vec![],
1088 };
1089 write_context_snapshot(run_id, &snap).unwrap();
1090 let back = read_context_snapshot(run_id).unwrap();
1091 assert_eq!(back.stage_name, "test");
1092 assert_eq!(back.total_tokens, 42);
1093 });
1094 }
1095
1096 #[test]
1097 fn read_context_snapshot_missing_returns_none() {
1098 assert!(read_context_snapshot("nonexistent-ctx-run").is_none());
1099 }
1100
1101 #[test]
1102 fn read_run_archive_roundtrips_and_context_history_replays() {
1103 with_isolated_runs_dir("read-run-archive-roundtrip", |_d| {
1104 use leviath_core::run_archive::{self, RunIdentity, RunRecord};
1105 let run_id = "archive-unit";
1106 std::fs::create_dir_all(run_dir(run_id)).unwrap();
1107 let mut buf = Vec::new();
1108 run_archive::write_archive_start(&mut buf, run_archive::RUN_ARCHIVE_VERSION).unwrap();
1109 let meta = RunMeta::new(
1110 run_id.to_string(),
1111 "a".to_string(),
1112 "/p".to_string(),
1113 "t".to_string(),
1114 None,
1115 "/w".to_string(),
1116 1,
1117 );
1118 run_archive::write_record(
1119 &mut buf,
1120 &RunRecord::Header {
1121 identity: RunIdentity {
1122 run_id: run_id.to_string(),
1123 machine_id: "m".to_string(),
1124 world_id: "w".to_string(),
1125 created_at: 0,
1126 },
1127 meta: Box::new(meta),
1128 },
1129 )
1130 .unwrap();
1131 run_archive::write_record(
1132 &mut buf,
1133 &RunRecord::ContextCheckpoint {
1134 snapshot: ContextSnapshot {
1135 stage_name: "plan".to_string(),
1136 total_tokens: 3,
1137 max_tokens: 100,
1138 regions: vec![],
1139 },
1140 at: 1,
1141 },
1142 )
1143 .unwrap();
1144 std::fs::write(run_dir(run_id).join("run.lvr"), &buf).unwrap();
1145
1146 let records = read_run_archive(run_id).expect("archive read");
1147 assert_eq!(records.len(), 2);
1148 let history = context_history(run_id);
1149 assert_eq!(history.len(), 1);
1150 assert_eq!(history[0].context.stage_name, "plan");
1151 });
1152 }
1153
1154 #[test]
1155 fn read_run_archive_missing_or_corrupt_returns_none() {
1156 with_isolated_runs_dir("read-run-archive-corrupt", |_d| {
1157 assert!(read_run_archive("no-such-archive-run").is_none());
1159 assert!(context_history("no-such-archive-run").is_empty());
1160 let run_id = "corrupt-archive-unit";
1162 std::fs::create_dir_all(run_dir(run_id)).unwrap();
1163 std::fs::write(run_dir(run_id).join("run.lvr"), b"not an archive").unwrap();
1164 assert!(read_run_archive(run_id).is_none());
1165 assert!(context_history(run_id).is_empty());
1166 });
1167 }
1168
1169 #[test]
1172 fn stage_dir_path_structure() {
1173 let path = stage_dir("run-abc", 2);
1174 assert!(path.ends_with("stages/2"));
1175 assert!(path.to_str().unwrap().contains("run-abc"));
1176 }
1177
1178 #[test]
1179 fn append_and_tail_stage_output() {
1180 with_isolated_runs_dir("append-and-tail-stage-output", |_d| {
1181 let run_id = "test-stage-output-unit";
1182 append_stage_output(run_id, 0, "line 1");
1183 append_stage_output(run_id, 0, "line 2");
1184 let output = tail_stage_output(run_id, 0, 4096);
1185 assert!(output.contains("line 1"));
1186 assert!(output.contains("line 2"));
1187 });
1188 }
1189
1190 #[test]
1191 fn append_and_tail_stage_log() {
1192 with_isolated_runs_dir("append-and-tail-stage-log", |_d| {
1193 let run_id = "test-stage-log-unit";
1194 append_stage_log(run_id, 0, "event A");
1195 append_stage_log(run_id, 0, "event B");
1196 let log = tail_stage_log(run_id, 0, 4096);
1197 assert!(log.contains("event A"));
1198 assert!(log.contains("event B"));
1199 });
1200 }
1201
1202 #[test]
1205 fn write_and_read_stage_context_roundtrip() {
1206 with_isolated_runs_dir("write-and-read-stage-context-roundtrip", |_d| {
1207 let run_id = "test-stage-ctx-unit";
1208 let snap = ContextSnapshot {
1209 stage_name: "stage-0".into(),
1210 total_tokens: 100,
1211 max_tokens: 4096,
1212 regions: vec![],
1213 };
1214 write_stage_context(run_id, 0, &snap).unwrap();
1215 let back = read_stage_context(run_id, 0).unwrap();
1216 assert_eq!(back.stage_name, "stage-0");
1217 });
1218 }
1219
1220 #[test]
1221 fn read_stage_context_missing_returns_none() {
1222 assert!(read_stage_context("nonexistent-run", 99).is_none());
1223 }
1224
1225 #[test]
1228 fn append_dashboard_log_creates_log_file() {
1229 with_isolated_runs_dir("append-dashboard-log-creates-log-file", |_d| {
1230 append_dashboard_log("coverage-test-message");
1231 assert!(dashboard_log_path().exists());
1232 });
1233 }
1234
1235 #[test]
1236 fn append_dashboard_log_open_failure_is_silently_ignored() {
1237 with_isolated_runs_dir("append-dashboard-log-open-failure", |_d| {
1243 let path = dashboard_log_path();
1244 std::fs::create_dir_all(&path).unwrap();
1245 append_dashboard_log("this should not panic");
1246 assert!(path.is_dir());
1247 });
1248 }
1249
1250 #[test]
1251 fn append_dashboard_log_path_with_no_parent_skips_create_dir_all() {
1252 temp_env::with_var("LEVIATH_DASHBOARD_LOG_PATH", Some("/"), || {
1258 assert!(dashboard_log_path().parent().is_none());
1259 append_dashboard_log("this should not panic even with no parent");
1260 });
1261 }
1262
1263 #[test]
1264 fn dashboard_log_rolls_once_over_cap() {
1265 let dir = tempfile::tempdir().unwrap();
1268 let path = dir.path().join("dashboard.log");
1269 append_dashboard_log_capped(&path, "first line well over the tiny cap", 8);
1270 assert!(path.exists());
1272 assert!(!rolled_log_path(&path).exists());
1273 append_dashboard_log_capped(&path, "second", 8);
1275 let rolled = rolled_log_path(&path);
1276 assert!(rolled.exists(), "previous generation rolled to <name>.1");
1277 assert!(
1278 std::fs::read_to_string(&rolled)
1279 .unwrap()
1280 .contains("first line")
1281 );
1282 let live = std::fs::read_to_string(&path).unwrap();
1284 assert!(live.contains("second"));
1285 assert!(!live.contains("first line"));
1286 }
1287
1288 #[test]
1289 fn dashboard_log_does_not_roll_under_cap() {
1290 let dir = tempfile::tempdir().unwrap();
1291 let path = dir.path().join("dashboard.log");
1292 append_dashboard_log_capped(&path, "a", 1_000_000);
1293 append_dashboard_log_capped(&path, "b", 1_000_000);
1294 assert!(!rolled_log_path(&path).exists());
1296 let live = std::fs::read_to_string(&path).unwrap();
1297 assert!(live.contains("a") && live.contains("b"));
1298 }
1299
1300 #[test]
1303 fn dashboard_log_path_structure() {
1304 temp_env::with_var_unset("LEVIATH_DASHBOARD_LOG_PATH", || {
1310 let path = dashboard_log_path();
1311 assert!(path.to_str().unwrap().contains(".leviath"));
1312 assert!(path.to_str().unwrap().ends_with("dashboard.log"));
1313 });
1314 }
1315
1316 #[test]
1321 fn dashboard_log_path_honors_leviath_home() {
1322 temp_env::with_vars(
1323 [
1324 ("LEVIATH_DASHBOARD_LOG_PATH", None),
1325 ("LEVIATH_HOME", Some("/custom/home")),
1326 ],
1327 || {
1328 assert_eq!(
1329 dashboard_log_path(),
1330 PathBuf::from("/custom/home/.leviath/dashboard.log")
1331 );
1332 },
1333 );
1334 }
1335
1336 #[test]
1339 fn runs_dir_structure() {
1340 temp_env::with_var_unset("LEVIATH_RUNS_DIR", || {
1343 let path = runs_dir();
1344 assert!(path.to_str().unwrap().contains(".leviath"));
1345 assert!(path.to_str().unwrap().ends_with("runs"));
1346 });
1347 }
1348
1349 #[test]
1350 fn runs_dir_from_uses_override_when_provided() {
1351 let path = runs_dir_from(Some("/custom/leviath/runs"));
1352 assert_eq!(path, PathBuf::from("/custom/leviath/runs"));
1353 }
1354
1355 #[test]
1356 fn runs_dir_from_falls_back_to_home_when_none() {
1357 let path = runs_dir_from(None);
1358 #[cfg(unix)]
1359 assert!(path.ends_with(".leviath/runs"));
1360 #[cfg(windows)]
1361 assert!(path.ends_with(".leviath\\runs"));
1362 }
1363
1364 #[test]
1369 fn runs_dir_follows_leviath_home() {
1370 temp_env::with_vars(
1371 [
1372 ("LEVIATH_RUNS_DIR", None::<&str>),
1373 ("LEVIATH_HOME", Some("/tmp/leviath-home-runs-test")),
1374 ],
1375 || {
1376 assert_eq!(
1377 runs_dir(),
1378 PathBuf::from("/tmp/leviath-home-runs-test")
1379 .join(".leviath")
1380 .join("runs")
1381 );
1382 },
1383 );
1384 }
1385
1386 #[test]
1387 fn dashboard_log_path_from_uses_override_when_provided() {
1388 let path = dashboard_log_path_from(Some("/custom/leviath/dashboard.log"));
1389 assert_eq!(path, PathBuf::from("/custom/leviath/dashboard.log"));
1390 }
1391
1392 #[test]
1393 fn dashboard_log_path_from_falls_back_to_home_when_none() {
1394 let path = dashboard_log_path_from(None);
1395 #[cfg(unix)]
1396 assert!(path.ends_with(".leviath/dashboard.log"));
1397 #[cfg(windows)]
1398 assert!(path.ends_with(".leviath\\dashboard.log"));
1399 }
1400
1401 #[test]
1402 fn run_dir_contains_run_id() {
1403 let path = run_dir("my-run-123");
1404 assert!(path.to_str().unwrap().contains("my-run-123"));
1405 }
1406
1407 #[test]
1410 fn with_isolated_runs_dir_points_at_temp_dir_and_cleans_up_after() {
1411 let inside = with_isolated_runs_dir("helper-self-test", |base_dir| {
1418 let expected = base_dir.join("runs");
1419 assert_eq!(runs_dir(), expected);
1420 assert!(runs_dir().exists());
1421 assert_eq!(dashboard_log_path(), base_dir.join("dashboard.log"));
1422 expected
1423 });
1424 assert!(!inside.exists());
1426 }
1427
1428 #[test]
1431 fn tail_file_exact_size() {
1432 let dir = tempfile::tempdir().unwrap();
1433 let path = dir.path().join("exact.txt");
1434 std::fs::write(&path, "exactly").unwrap();
1435 let result = tail_file(&path, 7);
1437 assert_eq!(result, "exactly");
1438 }
1439
1440 #[test]
1441 fn tail_file_tail_without_newline_returns_whole_window() {
1442 let dir = tempfile::tempdir().unwrap();
1451 let path = dir.path().join("no_newline.txt");
1452 let content = "a".repeat(100);
1454 std::fs::write(&path, content.as_bytes()).unwrap();
1455 let result = tail_file(&path, 10);
1457 assert_eq!(result, "aaaaaaaaaa");
1458 }
1459
1460 #[test]
1463 fn run_meta_with_metadata() {
1464 let mut meta = RunMeta::new(
1465 "meta-run".into(),
1466 "agent".into(),
1467 "/p".into(),
1468 "task".into(),
1469 None,
1470 "/w".into(),
1471 1,
1472 );
1473 meta.metadata
1474 .insert("key1".to_string(), "value1".to_string());
1475 meta.callback_url = Some("https://example.com/hook".to_string());
1476 meta.parent_run_id = Some("parent-123".to_string());
1477
1478 let json = serde_json::to_string(&meta).unwrap();
1479 let back: RunMeta = serde_json::from_str(&json).unwrap();
1480 assert_eq!(back.metadata.get("key1").unwrap(), "value1");
1481 assert_eq!(
1482 back.callback_url.as_deref(),
1483 Some("https://example.com/hook")
1484 );
1485 assert_eq!(back.parent_run_id.as_deref(), Some("parent-123"));
1486 }
1487
1488 #[test]
1491 fn stage_record_mutation() {
1492 let mut rec = StageRecord::new("test".into(), 0);
1493 rec.status = StageRunStatus::Active;
1494 rec.started_at = Some(1000);
1495 rec.prompt_tokens = 500;
1496 rec.completion_tokens = 200;
1497 rec.cached_tokens = 50;
1498
1499 assert_eq!(rec.status, StageRunStatus::Active);
1500 assert_eq!(rec.started_at, Some(1000));
1501 assert_eq!(rec.prompt_tokens, 500);
1502 assert_eq!(rec.completion_tokens, 200);
1503 assert_eq!(rec.cached_tokens, 50);
1504
1505 rec.status = StageRunStatus::Complete;
1506 rec.ended_at = Some(2000);
1507 assert_eq!(rec.status, StageRunStatus::Complete);
1508 assert_eq!(rec.ended_at, Some(2000));
1509 }
1510
1511 #[test]
1514 fn context_snapshot_with_entries() {
1515 let snap = ContextSnapshot {
1516 stage_name: "main".into(),
1517 total_tokens: 1000,
1518 max_tokens: 8192,
1519 regions: vec![
1520 RegionSnapshot {
1521 name: "system".into(),
1522 kind: "pinned".into(),
1523 current_tokens: 100,
1524 max_tokens: 2000,
1525 entries: vec![
1526 RegionEntrySnapshot {
1527 content: "You are helpful".into(),
1528 tokens: 3,
1529 kind: Default::default(),
1530 metadata: None,
1531 key: None,
1532 taint: Default::default(),
1533 },
1534 RegionEntrySnapshot {
1535 content: "Additional instruction".into(),
1536 tokens: 5,
1537 kind: Default::default(),
1538 metadata: Some(serde_json::json!({"source": "user"})),
1539 key: None,
1540 taint: Default::default(),
1541 },
1542 ],
1543 },
1544 RegionSnapshot {
1545 name: "conversation".into(),
1546 kind: "sliding".into(),
1547 current_tokens: 900,
1548 max_tokens: 6000,
1549 entries: vec![],
1550 },
1551 ],
1552 };
1553
1554 let json = serde_json::to_string_pretty(&snap).unwrap();
1555 let back: ContextSnapshot = serde_json::from_str(&json).unwrap();
1556 assert_eq!(back.regions.len(), 2);
1557 assert_eq!(back.regions[0].entries.len(), 2);
1558 assert_eq!(back.regions[0].entries[1].tokens, 5);
1559 assert!(back.regions[0].entries[1].metadata.is_some());
1560 }
1561
1562 #[test]
1565 fn region_entry_snapshot_metadata_omitted_when_none() {
1566 let entry = RegionEntrySnapshot {
1567 content: "test".into(),
1568 tokens: 1,
1569 kind: Default::default(),
1570 metadata: None,
1571 key: None,
1572 taint: Default::default(),
1573 };
1574 let json = serde_json::to_value(&entry).unwrap();
1575 assert!(json.get("metadata").is_none());
1576 }
1577
1578 #[test]
1581 fn append_stage_output_multiple_stages() {
1582 with_isolated_runs_dir("append-stage-output-multiple-stages", |_d| {
1583 let run_id = "test-multi-stage-out";
1584 append_stage_output(run_id, 0, "stage 0 output");
1585 append_stage_output(run_id, 1, "stage 1 output");
1586 append_stage_output(run_id, 2, "stage 2 output");
1587
1588 let out0 = tail_stage_output(run_id, 0, 4096);
1589 let out1 = tail_stage_output(run_id, 1, 4096);
1590 let out2 = tail_stage_output(run_id, 2, 4096);
1591
1592 assert!(out0.contains("stage 0 output"));
1593 assert!(out1.contains("stage 1 output"));
1594 assert!(out2.contains("stage 2 output"));
1595 assert!(!out0.contains("stage 1 output"));
1597 });
1598 }
1599
1600 #[test]
1603 fn list_runs_returns_sorted() {
1604 with_isolated_runs_dir("list-runs-returns-sorted", |_d| {
1605 let meta1 = RunMeta::new(
1606 "test-list-run-a".into(),
1607 "agent".into(),
1608 "/p".into(),
1609 "task a".into(),
1610 None,
1611 "/w".into(),
1612 1,
1613 );
1614 let meta2 = RunMeta::new(
1615 "test-list-run-b".into(),
1616 "agent".into(),
1617 "/p".into(),
1618 "task b".into(),
1619 None,
1620 "/w".into(),
1621 1,
1622 );
1623
1624 let _ = create_run(&meta1);
1625 let _ = create_run(&meta2);
1627
1628 let runs = list_runs();
1629 let ids: Vec<&str> = runs.iter().map(|r| r.run_id.as_str()).collect();
1631 assert!(ids.contains(&"test-list-run-a"));
1632 assert!(ids.contains(&"test-list-run-b"));
1633 });
1634 }
1635
1636 #[test]
1639 fn tail_stage_output_nonexistent_returns_empty() {
1640 assert_eq!(tail_stage_output("no-such-run-xyz", 0, 4096), "");
1641 }
1642
1643 #[test]
1644 fn tail_stage_log_nonexistent_returns_empty() {
1645 assert_eq!(tail_stage_log("no-such-run-xyz", 0, 4096), "");
1646 }
1647
1648 #[test]
1651 fn list_runs_in_dir_nonexistent_returns_empty() {
1652 let result = list_runs_in_dir(PathBuf::from("/nonexistent/leviath/runs/coverage-test"));
1653 assert!(result.is_empty());
1654 }
1655
1656 #[test]
1657 fn list_runs_in_dir_empty_dir_returns_empty() {
1658 let dir = tempfile::tempdir().unwrap();
1659 let result = list_runs_in_dir(dir.path().to_path_buf());
1660 assert!(result.is_empty());
1661 }
1662
1663 #[test]
1664 fn list_runs_in_dir_unreadable_dir_returns_empty() {
1665 let dir = tempfile::tempdir().unwrap();
1670 let not_a_dir = dir.path().join("runs-is-a-file");
1671 std::fs::write(¬_a_dir, "not a dir").unwrap();
1672 let result = list_runs_in_dir(not_a_dir);
1673 assert!(result.is_empty());
1674 }
1675
1676 #[test]
1677 fn append_stage_output_open_failure_is_silently_skipped() {
1678 crate::runstate::with_isolated_runs_dir("append_stage_output_open_failure", |_d| {
1682 let run_id = "append-out-openfail";
1683 ensure_stage_dir(run_id, 0);
1684 std::fs::create_dir_all(stage_dir(run_id, 0).join("output.log")).unwrap();
1685 append_stage_output(run_id, 0, "ignored"); });
1687 }
1688
1689 #[test]
1690 fn append_stage_log_open_failure_is_silently_skipped() {
1691 crate::runstate::with_isolated_runs_dir("append_stage_log_open_failure", |_d| {
1693 let run_id = "append-log-openfail";
1694 ensure_stage_dir(run_id, 0);
1695 std::fs::create_dir_all(stage_dir(run_id, 0).join("logs.log")).unwrap();
1696 append_stage_log(run_id, 0, "ignored"); });
1698 }
1699
1700 #[test]
1703 fn runs_dir_with_override_set_returns_override() {
1704 let tmpdir = tempfile::tempdir().unwrap();
1705 temp_env::with_var("LEVIATH_RUNS_DIR", Some(tmpdir.path()), || {
1706 assert_eq!(runs_dir(), tmpdir.path());
1707 });
1708 }
1709
1710 #[test]
1711 fn runs_dir_without_override_falls_back_to_home() {
1712 temp_env::with_var_unset("LEVIATH_RUNS_DIR", || {
1713 let dir = runs_dir();
1714 #[cfg(unix)]
1715 assert!(dir.ends_with(".leviath/runs"));
1716 #[cfg(windows)]
1717 assert!(dir.ends_with(".leviath\\runs"));
1718 });
1719 }
1720
1721 #[test]
1722 fn list_runs_empty_when_runs_dir_missing_or_empty() {
1723 with_isolated_runs_dir("list-runs-empty-when-runs-dir-missing-or-empty", |_d| {
1728 let runs = list_runs();
1729 assert!(runs.is_empty());
1730 });
1731 }
1732
1733 #[test]
1734 fn tail_file_nonexistent_path_returns_empty() {
1735 let path = std::path::Path::new("/nonexistent/path/to/a/file.log");
1736 assert_eq!(tail_file(path, 1024), "");
1737 }
1738
1739 #[test]
1740 fn tail_file_small_file_returns_whole_contents() {
1741 let dir = tempfile::tempdir().unwrap();
1742 let path = dir.path().join("small.log");
1743 std::fs::write(&path, "hello world").unwrap();
1744 assert_eq!(tail_file(&path, 1024), "hello world");
1745 }
1746
1747 #[test]
1748 fn tail_file_large_file_truncates_from_offset() {
1749 let dir = tempfile::tempdir().unwrap();
1750 let path = dir.path().join("big.log");
1751 let content = "a".repeat(100) + "\nTAIL_MARKER\n";
1752 std::fs::write(&path, &content).unwrap();
1753 let tailed = tail_file(&path, 20);
1754 assert!(tailed.contains("TAIL_MARKER"));
1755 assert!(tailed.len() < content.len());
1756 }
1757
1758 #[test]
1759 fn tail_file_directory_path_returns_empty() {
1760 let dir = tempfile::tempdir().unwrap();
1767 assert_eq!(tail_file(dir.path(), 4), "");
1768 }
1769
1770 #[cfg(unix)]
1771 #[test]
1772 fn tail_file_open_permission_denied_returns_empty() {
1773 use std::os::unix::fs::PermissionsExt;
1781
1782 let dir = tempfile::tempdir().unwrap();
1783 let path = dir.path().join("no-permissions.log");
1784 std::fs::write(&path, "x".repeat(100)).unwrap();
1788 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
1789
1790 assert_eq!(tail_file(&path, 4), "");
1791
1792 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
1794 }
1795
1796 #[test]
1799 fn write_context_snapshot_to_hermetic() {
1800 let dir = tempfile::tempdir().unwrap();
1801 let snap = ContextSnapshot {
1802 stage_name: "cov-stage".into(),
1803 total_tokens: 42,
1804 max_tokens: 8192,
1805 regions: vec![],
1806 };
1807 write_context_snapshot_to(dir.path(), &snap).unwrap();
1808 let json = std::fs::read_to_string(dir.path().join("context.json")).unwrap();
1809 let back: ContextSnapshot = serde_json::from_str(&json).unwrap();
1810 assert_eq!(back.total_tokens, 42);
1811 }
1812
1813 #[test]
1814 fn write_context_snapshot_to_fails_without_dir() {
1815 let snap = ContextSnapshot {
1816 stage_name: "s".into(),
1817 total_tokens: 1,
1818 max_tokens: 100,
1819 regions: vec![],
1820 };
1821 let nonexistent = std::path::Path::new("/nonexistent-cov-dir-xyzzy-abc");
1822 let result = write_context_snapshot_to(nonexistent, &snap);
1823 assert!(result.is_err());
1824 }
1825
1826 #[test]
1827 fn write_context_snapshot_to_fails_when_rename_target_is_a_dir() {
1828 let dir = tempfile::tempdir().unwrap();
1834 std::fs::create_dir(dir.path().join("context.json")).unwrap();
1835 let snap = ContextSnapshot {
1836 stage_name: "s".into(),
1837 total_tokens: 1,
1838 max_tokens: 100,
1839 regions: vec![],
1840 };
1841 let result = write_context_snapshot_to(dir.path(), &snap);
1842 assert!(result.is_err());
1843 }
1844
1845 #[test]
1846 fn create_run_in_hermetic() {
1847 let tmpdir = tempfile::tempdir().unwrap();
1848 let run_dir = tmpdir.path().join("cov-run");
1849 let meta = RunMeta::new(
1850 "cov-run".into(),
1851 "cov-agent".into(),
1852 "/agents/cov".into(),
1853 "cov task".into(),
1854 None,
1855 "/tmp".into(),
1856 1,
1857 );
1858 create_run_in(&run_dir, &meta).unwrap();
1859 let back = read_meta_from(&run_dir).unwrap();
1860 assert_eq!(back.run_id, "cov-run");
1861 }
1862
1863 #[test]
1864 fn create_run_in_fails_on_bad_parent() {
1865 let dir = tempfile::tempdir().unwrap();
1875 let not_a_dir = dir.path().join("not-a-directory");
1876 std::fs::write(¬_a_dir, "x").unwrap();
1877 let bad = not_a_dir.join("run");
1878 let meta = RunMeta::new(
1879 "run".into(),
1880 "a".into(),
1881 "/".into(),
1882 "t".into(),
1883 None,
1884 "/tmp".into(),
1885 1,
1886 );
1887 let result = create_run_in(&bad, &meta);
1888 assert!(result.is_err());
1889 }
1890
1891 #[test]
1892 fn write_meta_to_hermetic() {
1893 let tmpdir = tempfile::tempdir().unwrap();
1894 let meta = RunMeta::new(
1895 "cov-write-meta".into(),
1896 "a".into(),
1897 "/".into(),
1898 "t".into(),
1899 None,
1900 "/tmp".into(),
1901 1,
1902 );
1903 write_meta_to(tmpdir.path(), &meta).unwrap();
1904 let back = read_meta_from(tmpdir.path()).unwrap();
1905 assert_eq!(back.run_id, "cov-write-meta");
1906 }
1907
1908 #[test]
1909 fn write_meta_to_fails_without_dir() {
1910 let meta = RunMeta::new(
1911 "cov-no-dir".into(),
1912 "a".into(),
1913 "/".into(),
1914 "t".into(),
1915 None,
1916 "/tmp".into(),
1917 1,
1918 );
1919 let bad = std::path::Path::new("/nonexistent-cov-write-meta-xyzzy");
1920 let result = write_meta_to(bad, &meta);
1921 assert!(result.is_err());
1922 }
1923
1924 #[test]
1925 fn write_meta_to_fails_when_rename_target_is_a_dir() {
1926 let dir = tempfile::tempdir().unwrap();
1930 std::fs::create_dir(dir.path().join("meta.json")).unwrap();
1931 let meta = RunMeta::new(
1932 "cov-rename-fail".into(),
1933 "a".into(),
1934 "/".into(),
1935 "t".into(),
1936 None,
1937 "/tmp".into(),
1938 1,
1939 );
1940 let result = write_meta_to(dir.path(), &meta);
1941 assert!(result.is_err());
1942 }
1943
1944 #[test]
1945 fn read_meta_from_fails_on_missing_file() {
1946 let tmpdir = tempfile::tempdir().unwrap();
1947 let result = read_meta_from(tmpdir.path());
1948 assert!(result.is_err());
1949 }
1950
1951 #[test]
1952 fn write_stages_index_to_hermetic() {
1953 let tmpdir = tempfile::tempdir().unwrap();
1954 let stages = vec![StageRecord::new("cov-stage".into(), 0)];
1955 write_stages_index_to(tmpdir.path(), &stages).unwrap();
1956 let json = std::fs::read_to_string(tmpdir.path().join("stages.json")).unwrap();
1957 let back: Vec<StageRecord> = serde_json::from_str(&json).unwrap();
1958 assert_eq!(back.len(), 1);
1959 assert_eq!(back[0].name, "cov-stage");
1960 }
1961
1962 #[test]
1963 fn write_stages_index_to_fails_without_dir() {
1964 let stages = vec![StageRecord::new("s".into(), 0)];
1965 let bad = std::path::Path::new("/nonexistent-cov-stages-xyzzy");
1966 let result = write_stages_index_to(bad, &stages);
1967 assert!(result.is_err());
1968 }
1969
1970 #[test]
1971 fn write_stages_index_to_fails_when_rename_target_is_a_dir() {
1972 let dir = tempfile::tempdir().unwrap();
1976 std::fs::create_dir(dir.path().join("stages.json")).unwrap();
1977 let stages = vec![StageRecord::new("s".into(), 0)];
1978 let result = write_stages_index_to(dir.path(), &stages);
1979 assert!(result.is_err());
1980 }
1981
1982 #[test]
1983 fn list_runs_in_dir_includes_valid_run() {
1984 let tmpdir = tempfile::tempdir().unwrap();
1985 let run_id = "cov-listed-run";
1986 let run_subdir = tmpdir.path().join(run_id);
1987 std::fs::create_dir_all(&run_subdir).unwrap();
1988 let meta = RunMeta::new(
1989 run_id.into(),
1990 "list-agent".into(),
1991 "/agents/list".into(),
1992 "list task".into(),
1993 None,
1994 "/tmp".into(),
1995 1,
1996 );
1997 let json = serde_json::to_string_pretty(&meta).unwrap();
1998 std::fs::write(run_subdir.join("meta.json"), &json).unwrap();
1999
2000 let runs = list_runs_in_dir(tmpdir.path().to_path_buf());
2002 assert!(runs.iter().any(|r| r.run_id == run_id));
2003 }
2004
2005 #[test]
2006 fn list_runs_in_dir_skips_entry_with_corrupted_meta_json() {
2007 let tmpdir = tempfile::tempdir().unwrap();
2012 let good_run_id = "cov-listed-good-run";
2013 let bad_run_id = "cov-listed-corrupted-run";
2014
2015 let good_subdir = tmpdir.path().join(good_run_id);
2016 std::fs::create_dir_all(&good_subdir).unwrap();
2017 let meta = RunMeta::new(
2018 good_run_id.into(),
2019 "list-agent".into(),
2020 "/agents/list".into(),
2021 "list task".into(),
2022 None,
2023 "/tmp".into(),
2024 1,
2025 );
2026 let json = serde_json::to_string_pretty(&meta).unwrap();
2027 std::fs::write(good_subdir.join("meta.json"), &json).unwrap();
2028
2029 let bad_subdir = tmpdir.path().join(bad_run_id);
2030 std::fs::create_dir_all(&bad_subdir).unwrap();
2031 std::fs::write(bad_subdir.join("meta.json"), "not valid json").unwrap();
2032
2033 let no_meta_run_id = "cov-listed-no-meta-run";
2038 std::fs::create_dir_all(tmpdir.path().join(no_meta_run_id)).unwrap();
2039
2040 let runs = list_runs_in_dir(tmpdir.path().to_path_buf());
2041 assert!(runs.iter().any(|r| r.run_id == good_run_id));
2042 assert!(!runs.iter().any(|r| r.run_id == bad_run_id));
2043 assert!(!runs.iter().any(|r| r.run_id == no_meta_run_id));
2044 }
2045
2046 fn run_dir_with(base: &std::path::Path, run_id: &str, status: RunStatus) -> PathBuf {
2050 let dir = base.join(run_id);
2051 let meta = RunMeta {
2052 status,
2053 ..RunMeta::new(
2054 run_id.into(),
2055 "a".into(),
2056 "/p".into(),
2057 "t".into(),
2058 None,
2059 "/w".into(),
2060 1,
2061 )
2062 };
2063 create_run_in(&dir, &meta).unwrap();
2064 dir
2065 }
2066
2067 #[test]
2068 fn force_cancel_terminates_every_non_terminal_status() {
2069 let base = tempfile::tempdir().unwrap();
2070 for status in [
2071 RunStatus::Starting,
2072 RunStatus::Running,
2073 RunStatus::WaitingInput,
2074 ] {
2075 let dir = run_dir_with(base.path(), &format!("live-{status}"), status.clone());
2076 assert_eq!(force_cancel_in(&dir, 99), ForceCancelOutcome::Cancelled);
2077 let meta = read_meta_from(&dir).unwrap();
2078 assert_eq!(meta.status, RunStatus::Cancelled, "{status} is killable");
2079 assert_eq!(meta.updated_at, 99, "the cancel is stamped");
2080 }
2081 }
2082
2083 #[test]
2084 fn force_cancel_leaves_a_finished_run_alone() {
2085 let base = tempfile::tempdir().unwrap();
2086 for status in [
2087 RunStatus::Complete,
2088 RunStatus::CompleteInteractive,
2089 RunStatus::Error,
2090 RunStatus::Cancelled,
2091 ] {
2092 let dir = run_dir_with(base.path(), &format!("done-{status}"), status.clone());
2093 assert_eq!(
2094 force_cancel_in(&dir, 99),
2095 ForceCancelOutcome::AlreadyTerminal,
2096 "{status} is already finished"
2097 );
2098 assert_eq!(read_meta_from(&dir).unwrap().status, status);
2099 }
2100 }
2101
2102 #[test]
2103 fn force_cancel_reports_no_such_run_for_a_missing_directory() {
2104 let base = tempfile::tempdir().unwrap();
2105 let outcome = force_cancel_in(&base.path().join("ghost"), 99);
2106 assert_eq!(outcome, ForceCancelOutcome::NoSuchRun);
2107 assert!(!outcome.found_run(), "nothing to cancel");
2108 }
2109
2110 #[test]
2114 fn force_cancel_writes_a_record_over_unreadable_metadata() {
2115 let base = tempfile::tempdir().unwrap();
2116 let dir = base.path().join("corrupt-run");
2117 std::fs::create_dir_all(&dir).unwrap();
2118 std::fs::write(dir.join("meta.json"), "{ not json").unwrap();
2119
2120 assert_eq!(force_cancel_in(&dir, 99), ForceCancelOutcome::Cancelled);
2121 let meta = read_meta_from(&dir).expect("now parses");
2122 assert_eq!(meta.status, RunStatus::Cancelled);
2123 assert_eq!(meta.run_id, "corrupt-run", "recovered from the dir name");
2124 assert!(meta.error.is_some(), "records why it was synthesized");
2125 }
2126
2127 #[test]
2130 fn force_cancel_reports_a_write_failure_but_still_found_the_run() {
2131 crate::test_support::with_tracing(|| {
2132 let base = tempfile::tempdir().unwrap();
2133 let dir = base.path().join("blocked-run");
2134 std::fs::create_dir_all(&dir).unwrap();
2135 std::fs::create_dir_all(dir.join("meta.json")).unwrap();
2137
2138 let outcome = force_cancel_in(&dir, 99);
2139 assert_eq!(outcome, ForceCancelOutcome::WriteFailed);
2140 assert!(outcome.found_run());
2141 });
2142 }
2143
2144 #[test]
2145 fn append_dashboard_log_writes_message() {
2146 with_isolated_runs_dir("append-dashboard-log-writes-message", |_d| {
2148 let unique = format!("cov-dashboard-log-{}", std::process::id());
2149 append_dashboard_log(&unique);
2150 let content = std::fs::read_to_string(dashboard_log_path()).unwrap_or_default();
2151 assert!(content.contains(&unique));
2152 });
2153 }
2154}