use super::{Harness, agent};
use crate::git_tree::{AgentState, GitTree};
use crate::watch::Mark;
use std::path::{Path, PathBuf};
use std::time::Duration;
fn agent_count(model: &super::Rig, ws: &Path) -> usize {
model.tree(ws).map_or(0, |t| t.agents.len())
}
pub(super) fn settle(model: &mut super::Rig, clock: &crate::test_support::FakeClock) {
clock.advance(Duration::from_millis(10));
model.tick(); clock.advance(Duration::from_millis(150));
model.tick(); }
#[test]
fn a_dirty_workspace_root_re_derives_its_snapshot() {
let h = Harness::new();
let (clock, mut model) = h.model();
assert_eq!(agent_count(&model, &h.ws), 1);
h.build_more("c-2", "world"); assert_eq!(agent_count(&model, &h.ws), 1, "stale until re-derived");
model.dirty_handle().mark_all([(h.ws.clone(), Mark::Watch)]);
clock.advance(Duration::from_millis(10));
assert!(!model.tick(), "debounced within the window");
clock.advance(Duration::from_millis(150));
assert!(model.tick(), "window elapsed → snapshot replaced");
assert_eq!(agent_count(&model, &h.ws), 2);
}
#[test]
fn an_unchanged_re_derivation_replaces_nothing() {
let h = Harness::new();
let (clock, mut model) = h.model();
model.dirty_handle().mark_all([(h.ws.clone(), Mark::Watch)]);
clock.advance(Duration::from_millis(10));
model.tick();
clock.advance(Duration::from_millis(150));
assert!(
!model.tick(),
"an unchanged tree suppresses the replacement"
);
}
#[test]
fn a_failed_re_read_keeps_the_last_snapshot() {
let h = Harness::new();
let (clock, mut model) = h.model();
let bogus = PathBuf::from("/nonexistent/workspace");
model
.dirty_handle()
.mark_all([(bogus.clone(), Mark::Watch)]);
settle(&mut model, &clock);
assert!(model.tree(&bogus).is_none(), "a failed read stores nothing");
assert_eq!(agent_count(&model, &h.ws), 1, "the good snapshot is intact");
}
#[test]
fn the_full_sweep_re_derives_every_workspace() {
let h = Harness::new();
let (clock, mut model) = h.model();
h.build_more("c-2", "yo");
clock.advance(Duration::from_secs(15)); model.tick();
clock.advance(Duration::from_millis(150)); model.tick();
assert_eq!(agent_count(&model, &h.ws), 2, "the 15 s sweep re-derived");
}
#[test]
fn the_cheap_sweep_re_probes_and_invalidates_only_live_workspaces() {
let h = Harness::new();
let (clock, mut model) = h.model();
clock.advance(crate::app::dirty::CHEAP_SWEEP);
assert!(!model.tick(), "a quiescent workspace: no targeted re-probe");
model.deriver.trees.insert(
h.ws.clone(),
GitTree {
commits: vec![],
agents: vec![agent("c-1", AgentState::Live)],
},
);
clock.advance(crate::app::dirty::CHEAP_SWEEP);
model.tick(); clock.advance(Duration::from_millis(150));
assert!(model.tick(), "a live agent: the cheap sweep re-derives");
assert_eq!(agent_count(&model, &h.ws), 1, "re-read the real fixture");
}
#[test]
fn an_enumeration_root_event_reconciles_the_workspace_set() {
let mut h = Harness::new();
let (clock, mut model) = h.model();
assert_eq!(model.workspaces().len(), 1);
let ws2 = h.add_workspace("ws2", "d-1");
model
.dirty_handle()
.mark_all([(h.roots.workspaces(), Mark::Watch)]);
model.tick(); assert_eq!(
model.workspaces().len(),
2,
"the new workspace is enumerated"
);
assert_eq!(model.workspace_stats(&ws2), (0, 0, false));
clock.advance(Duration::from_millis(150));
model.tick();
assert_eq!(agent_count(&model, &ws2), 1);
}
#[test]
fn a_vanished_workspace_is_pruned_on_reconcile() {
let mut h = Harness::new();
let ws2 = h.add_workspace("ws2", "d-1");
let (_c, mut model) = h.model();
assert_eq!(
model.workspaces().len(),
2,
"both enumerated at construction"
);
assert_eq!(agent_count(&model, &ws2), 1);
std::fs::remove_file(&ws2).unwrap();
model
.dirty_handle()
.mark_all([(h.roots.workspaces(), Mark::Watch)]);
model.tick();
assert_eq!(model.workspaces().len(), 1, "the removed workspace is gone");
assert!(model.tree(&ws2).is_none(), "its snapshot is pruned");
}
#[test]
fn every_enum_root_and_the_state_root_are_routed() {
let h = Harness::new();
let (_c, mut model) = h.model();
model.dirty_handle().mark_all([
(h.roots.yog_state.clone(), Mark::Watch),
(h.roots.names(), Mark::Watch),
(h.roots.workspaces(), Mark::Watch),
(h.roots.replays(), Mark::Watch),
(h.ws.clone(), Mark::Watch),
]);
assert!(
!model.tick(),
"no snapshot delta this tick (workspace only debounced)"
);
assert_eq!(model.workspaces().len(), 1, "reconcile kept the set");
}
#[test]
fn cadence_yaml_tunes_the_clock_at_boot_on_change_and_resets_on_delete() {
let h = Harness::new();
let file = h.roots.yog_state.join(crate::app::cadence::CADENCE_YAML);
std::fs::write(&file, "cadence:\n watcher:\n cheap_sweep_ms: 5000\n").unwrap();
let (_c, mut model) = h.model();
assert_eq!(
model.cadence().cheap_sweep,
Duration::from_secs(5),
"boot adopted the file before the first schedule decision"
);
std::fs::write(&file, "cadence:\n watcher:\n cheap_sweep_ms: 9000\n").unwrap();
model
.dirty_handle()
.mark_all([(h.roots.yog_state.clone(), Mark::Watch)]);
assert!(model.tick(), "a re-tune publishes");
assert_eq!(model.cadence().cheap_sweep, Duration::from_secs(9));
std::fs::remove_file(&file).unwrap();
model
.dirty_handle()
.mark_all([(h.roots.yog_state.clone(), Mark::Watch)]);
assert!(model.tick(), "the reset publishes too");
assert_eq!(
model.cadence(),
crate::app::Cadence::default(),
"deleting the file is the reset — severability, not an error"
);
}
#[test]
fn adopting_our_own_ui_json_echo_is_suppressed() {
let h = Harness::new();
let (_c, mut model) = h.model();
let key = crate::naming::leaf(&h.ws);
model.toggle_pin(&key);
model
.dirty_handle()
.mark_all([(h.roots.yog_state.clone(), Mark::Watch)]);
model.tick(); let bar = model.tab_bar();
assert!(
bar.tabs.first().is_some_and(|t| t.pinned),
"the pin survives the echo: {bar:?}"
);
}
fn fleet(model: &crate::AppModel) -> Vec<crate::fleet::Facts> {
crate::board::build(&model.snap, &model.ui, model.now_unix()).fleet
}
#[test]
fn a_fleet_entry_is_adopted_and_deleted_on_the_clocks_own_announcement() {
let h = Harness::new();
let file = h.roots.yog_state.join(crate::app::cadence::CADENCE_YAML);
let (_c, mut model) = h.model();
assert!(
fleet(&model).is_empty(),
"unarmed: the board is today's balls section"
);
std::fs::write(
&file,
"fleet:\n /ws/a:\n project: /dev/yog\n cap: 3\n",
)
.unwrap();
model
.dirty_handle()
.mark_all([(h.roots.yog_state.clone(), Mark::Watch)]);
assert!(model.tick(), "an arming publishes");
let armed = fleet(&model);
assert_eq!(armed.len(), 1);
assert_eq!(armed[0].cap, 3);
assert_eq!(armed[0].project, std::path::PathBuf::from("/dev/yog"));
std::fs::remove_file(&file).unwrap();
model
.dirty_handle()
.mark_all([(h.roots.yog_state.clone(), Mark::Watch)]);
assert!(model.tick(), "the disarming publishes too");
assert!(
fleet(&model).is_empty(),
"deleting the entry deletes the loop, not a code path"
);
}