use super::*;
use pixtuoid_core::{AgentId, GlobalDeskIndex};
fn id() -> AgentId {
AgentId::from_parts("test", "motion-test-agent")
}
#[test]
fn motion_state_new_default_fields() {
let ms = MotionState::new(id());
assert!(ms.entry.is_none());
assert!(ms.exit.is_none());
assert!(ms.snap_back.is_none());
assert_eq!(ms.wander_cycle_n, 0);
assert_eq!(ms.wander_phase, WanderPhase::Seated);
assert_eq!(ms.wander_phase_started_at, SystemTime::UNIX_EPOCH);
assert_eq!(ms.last_advanced_at, SystemTime::UNIX_EPOCH);
assert!(ms.wander_profile.is_none());
assert!(ms.wander_dest_kind.is_none());
assert!(ms.wander_dest_wp_idx.is_none());
assert!(ms.walk_path.is_none());
}
#[test]
fn path_len_empty_is_zero() {
assert_eq!(octile_path_len(&[]), 0);
}
#[test]
fn path_len_single_point_is_zero() {
let p = Point { x: 10, y: 20 };
assert_eq!(octile_path_len(&[p]), 0);
}
#[test]
fn path_len_orthogonal_segment() {
let a = Point { x: 0, y: 0 };
let b = Point { x: 5, y: 0 };
assert_eq!(octile_path_len(&[a, b]), 50);
}
#[test]
fn path_len_diagonal_segment() {
let a = Point { x: 0, y: 0 };
let b = Point { x: 3, y: 3 };
assert_eq!(octile_path_len(&[a, b]), 42);
}
#[test]
fn path_len_multi_segment_sums() {
let a = Point { x: 0, y: 0 };
let b = Point { x: 4, y: 0 };
let c = Point { x: 4, y: 3 };
assert_eq!(octile_path_len(&[a, b, c]), 70);
}
use crate::tui::layout::Layout;
use crate::tui::pathfind::Router;
use crate::tui::pose::{
cycle_ms_for, dwell_ms, est_wander_cycle_ms, seated_dwell_ms, takes_trip, WANDER_DWELL_EST_MS,
};
use pixtuoid_core::state::ActivityState;
use pixtuoid_core::walkable::{OccupancyOverlay, WalkableMask};
use std::path::PathBuf;
use std::sync::Arc;
struct Straight;
impl Router for Straight {
fn route(
&mut self,
_: &WalkableMask,
_: &OccupancyOverlay,
from: Point,
to: Point,
) -> Vec<Point> {
vec![from, to]
}
fn invalidate(&mut self) {}
}
struct FixedLen {
octile_len: u32,
}
impl Router for FixedLen {
fn route(
&mut self,
_: &WalkableMask,
_: &OccupancyOverlay,
from: Point,
_to: Point,
) -> Vec<Point> {
let steps = (self.octile_len / 10) as u16;
let mid = Point {
x: from.x + steps / 2,
y: from.y,
};
let end = Point {
x: from.x + steps,
y: from.y,
};
vec![from, mid, end]
}
fn invalidate(&mut self) {}
}
fn t0() -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000)
}
fn idle_slot(path: &str, state_started: SystemTime) -> AgentSlot {
AgentSlot {
agent_id: AgentId::from_transcript_path(path),
source: Arc::from("claude-code"),
session_id: Arc::from("s"),
cwd: Arc::from(PathBuf::from("/p").as_path()),
label: Arc::from("cc"),
state: ActivityState::Idle,
state_started_at: state_started,
created_at: state_started
.checked_sub(Duration::from_secs(90))
.unwrap_or(state_started),
last_event_at: state_started
.checked_sub(Duration::from_secs(90))
.unwrap_or(state_started),
exiting_at: None,
pending_idle_at: None,
desk_index: GlobalDeskIndex(0),
floor_idx: 0,
tool_call_count: 0,
active_ms: 0,
unknown_cwd: false,
parent_id: None,
}
}
fn layout() -> Layout {
Layout::compute(120, 96, 4).expect("fits")
}
fn trip_agent(prefix: &str) -> AgentId {
(0u64..500)
.map(|i| AgentId::from_transcript_path(&format!("/p/{prefix}_{i}.jsonl")))
.find(|id| takes_trip(*id, 0))
.expect("should find a trip agent quickly")
}
fn current_dwell_dur(motion: &HashMap<AgentId, MotionState>, id: AgentId) -> u64 {
motion
.get(&id)
.and_then(|ms| ms.wander_dest_kind)
.map_or(WANDER_DWELL_EST_MS, |k| dwell_ms(k, id))
}
#[allow(clippy::too_many_arguments)]
fn advance_until_leaves(
slot: &AgentSlot,
l: &Layout,
router: &mut dyn Router,
overlay: &OccupancyOverlay,
motion: &mut HashMap<AgentId, MotionState>,
mut now: SystemTime,
from_phase: WanderPhase,
timeout_ms: u64,
) -> SystemTime {
const STEP_MS: u64 = 1_000;
let start = now;
while motion.get(&slot.agent_id).map(|m| m.wander_phase) == Some(from_phase) {
let elapsed = now
.duration_since(start)
.unwrap_or(Duration::ZERO)
.as_millis() as u64;
assert!(
elapsed <= timeout_ms,
"phase {from_phase:?} did not transition within {timeout_ms}ms"
);
now += Duration::from_millis(STEP_MS);
advance_wander(slot, now, l, router, overlay, motion);
}
now
}
#[test]
fn fresh_idle_inits_to_seated_phase() {
let now = t0();
let slot = idle_slot("/p/a.jsonl", now);
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let ms = motion.get(&slot.agent_id).expect("state inserted");
assert!(
matches!(ms.wander_phase, WanderPhase::Seated),
"fresh idle should init to Seated, got {:?}",
ms.wander_phase
);
assert_eq!(ms.wander_cycle_n, 0);
}
#[test]
fn seated_transitions_to_walking_out_on_trip_cycle() {
let trip_id = trip_agent("trip");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let ms = motion.get(&trip_id).expect("state present");
assert!(
matches!(ms.wander_phase, WanderPhase::WalkingOut),
"after seated dwell on trip cycle, expected WalkingOut, got {:?}",
ms.wander_phase
);
assert!(
ms.wander_profile.is_some(),
"walk-out profile must be snapshotted"
);
}
#[test]
fn non_trip_cycle_stays_seated() {
let stay_id = (0u64..500)
.map(|i| AgentId::from_transcript_path(&format!("/p/stay_{i}.jsonl")))
.find(|id| !takes_trip(*id, 0))
.expect("should find a stay-seated agent");
let now = t0();
let slot = AgentSlot {
agent_id: stay_id,
..idle_slot("/dummy", now)
};
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let mut t = now;
for _ in 0..40 {
t += Duration::from_millis(1_000);
advance_wander(&slot, t, &l, &mut router, &overlay, &mut motion);
assert!(
matches!(
motion.get(&stay_id).unwrap().wander_phase,
WanderPhase::Seated
),
"non-trip cycle must stay Seated"
);
}
}
#[test]
fn walking_out_transitions_to_at_waypoint_on_arrival() {
let trip_id = trip_agent("wp");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let short_len: u32 = 200;
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
assert!(matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::WalkingOut
));
advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t1,
WanderPhase::WalkingOut,
20_000,
);
let ms = motion.get(&trip_id).expect("state");
assert!(
matches!(ms.wander_phase, WanderPhase::AtWaypoint),
"expected AtWaypoint after walk-out arrival, got {:?}",
ms.wander_phase
);
}
#[test]
fn at_waypoint_transitions_to_walking_back_after_dwell() {
let trip_id = trip_agent("dwell");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let short_len: u32 = 200;
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let t2 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t1,
WanderPhase::WalkingOut,
20_000,
);
assert!(matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::AtWaypoint
));
advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t2,
WanderPhase::AtWaypoint,
60_000,
);
let ms = motion.get(&trip_id).expect("state");
assert!(
matches!(ms.wander_phase, WanderPhase::WalkingBack),
"expected WalkingBack after dwell, got {:?}",
ms.wander_phase
);
assert!(
ms.wander_profile.is_some(),
"walk-back profile must be snapshotted"
);
}
#[test]
fn walking_back_arrival_increments_cycle_n_and_resets_to_seated() {
let trip_id = trip_agent("cyc");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let short_len: u32 = 200;
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let t = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t,
WanderPhase::WalkingOut,
20_000,
);
let t = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t,
WanderPhase::AtWaypoint,
60_000,
);
advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t,
WanderPhase::WalkingBack,
20_000,
);
let ms = motion.get(&trip_id).expect("state");
assert!(
matches!(ms.wander_phase, WanderPhase::Seated),
"completed cycle must reset to Seated, got {:?}",
ms.wander_phase
);
assert_eq!(ms.wander_cycle_n, 1, "cycle_n must increment once");
}
#[test]
fn dwell_time_independent_of_path_length() {
let trip_id = trip_agent("dwell2");
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", t0())
};
let l = layout();
let overlay = OccupancyOverlay::new();
let mut measured: Vec<u64> = Vec::new();
for short_len in [150u32, 800u32] {
let now = t0();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let at_wp_enter = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t1,
WanderPhase::WalkingOut,
20_000,
);
let walk_back_enter = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
at_wp_enter,
WanderPhase::AtWaypoint,
60_000,
);
let dwell = walk_back_enter
.duration_since(at_wp_enter)
.unwrap()
.as_millis() as u64;
measured.push(dwell);
}
let diff = measured[0].abs_diff(measured[1]);
assert!(
diff <= 1_000,
"dwell must be path-length-independent: {measured:?}"
);
}
#[test]
fn far_waypoint_full_cycle_is_longer() {
use pixtuoid_core::physics::{walk_profile, WalkIntent};
let trip_id = trip_agent("far");
let seated_dur = seated_dwell_ms(trip_id);
let dwell_dur = WANDER_DWELL_EST_MS;
let cycle_wall_ms = |path_len: u32| -> u64 {
let out = walk_profile(path_len, WalkIntent::WanderOut, trip_id);
let back = walk_profile(path_len, WalkIntent::WanderBack, trip_id);
seated_dur
+ (out.duration_ms + out.pause_ms)
+ dwell_dur
+ (back.duration_ms + back.pause_ms)
};
let near_ms = cycle_wall_ms(100);
let far_ms = cycle_wall_ms(1200);
assert!(
far_ms > near_ms,
"far cycle ({far_ms}ms) must be longer than near cycle ({near_ms}ms)"
);
let out_near = walk_profile(100, WalkIntent::WanderOut, trip_id);
let out_far = walk_profile(1200, WalkIntent::WanderOut, trip_id);
assert!(
out_far.duration_ms > out_near.duration_ms,
"far walk must take longer"
);
}
#[test]
fn arrival_pause_holds_walking_out_phase() {
use pixtuoid_core::physics::{walk_arrived, walk_profile, WalkIntent};
let trip_id = trip_agent("pause");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let short_len: u32 = 200;
let profile = walk_profile(short_len, WalkIntent::WanderOut, trip_id);
let mid_pause_elapsed = profile.duration_ms + profile.pause_ms / 2;
assert!(
!walk_arrived(&profile, mid_pause_elapsed),
"walk_arrived must be false mid-pause"
);
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let out_started = motion.get(&trip_id).unwrap().wander_phase_started_at;
let actual_profile = motion
.get(&trip_id)
.and_then(|ms| ms.wander_profile.as_ref())
.expect("profile snapshotted");
let actual_mid_elapsed = actual_profile.duration_ms + actual_profile.pause_ms / 2;
let _ = t1;
let mid = out_started + Duration::from_millis(actual_mid_elapsed);
advance_wander(&slot, mid, &l, &mut router, &overlay, &mut motion);
assert!(
matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::WalkingOut
),
"must stay WalkingOut during arrival pause"
);
}
#[test]
fn idempotent_same_now_does_not_mutate_state() {
let trip_id = trip_agent("idem");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let (phase_before, cycle_before) = {
let ms = motion.get(&trip_id).unwrap();
(ms.wander_phase, ms.wander_cycle_n)
};
advance_wander(&slot, t1, &l, &mut router, &overlay, &mut motion);
let ms = motion.get(&trip_id).unwrap();
assert_eq!(
ms.wander_phase, phase_before,
"2nd call with same now must not change phase"
);
assert_eq!(
ms.wander_cycle_n, cycle_before,
"2nd call with same now must not change cycle_n"
);
}
#[test]
fn bootstrap_fast_forwards_cycle_n() {
let id = AgentId::from_transcript_path("/p/bootstrap.jsonl");
let now = t0();
let cycle = est_wander_cycle_ms(id);
let state_started = now
.checked_sub(Duration::from_millis(10 * cycle))
.expect("time arithmetic ok");
let slot = idle_slot("/p/bootstrap.jsonl", state_started);
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let ms = motion.get(&id).expect("state present");
assert_eq!(
ms.wander_cycle_n, 10,
"bootstrap: elapsed = 10*est_cycle => cycle_n must equal exactly 10"
);
}
#[test]
fn stale_resume_resyncs_without_replay() {
let trip_id = trip_agent("stale");
let now = t0();
let est_cycle = est_wander_cycle_ms(trip_id);
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
assert!(
matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::WalkingOut
),
"precondition: agent should be WalkingOut before the gap"
);
assert!(20 * est_cycle > cycle_ms_for(trip_id));
let resume = t1 + Duration::from_millis(20 * est_cycle);
advance_wander(&slot, resume, &l, &mut router, &overlay, &mut motion);
let ms = motion.get(&trip_id).unwrap();
assert!(
matches!(ms.wander_phase, WanderPhase::Seated),
"stale resume must resync to Seated (no per-frame replay), got {:?}",
ms.wander_phase
);
assert!(
ms.wander_cycle_n >= 18,
"stale resume must fast-forward cycle_n across the gap, got {}",
ms.wander_cycle_n
);
}
#[test]
fn long_dwell_never_trips_stale_resume_on_screen() {
let trip_id = trip_agent("longdwell");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let short_len: u32 = 200;
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let t2 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t1,
WanderPhase::WalkingOut,
20_000,
);
assert!(matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::AtWaypoint
));
let at_wp_start = motion.get(&trip_id).unwrap().wander_phase_started_at;
let dwell_dur = current_dwell_dur(&motion, trip_id);
let mut t = t2;
let end = at_wp_start + Duration::from_millis(dwell_dur.saturating_sub(2_000));
while t < end {
t += Duration::from_millis(33);
advance_wander(&slot, t, &l, &mut router, &overlay, &mut motion);
assert!(
!matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::Seated
),
"long on-screen dwell wrongly tripped stale-resume (snapped to Seated mid-dwell)"
);
}
assert!(
matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::AtWaypoint
),
"agent should still be AtWaypoint just before the dwell ends"
);
}
#[test]
fn wander_dest_for_pantry_is_the_home_desk_stand_point() {
let l = layout();
let pantry_idx = l
.waypoints
.iter()
.position(|w| w.kind == WaypointKind::Pantry)
.expect("standard floor has a pantry");
let (path, _id) = (0u64..8000)
.find_map(|i| {
let p = format!("/p/mirror_{i}.jsonl");
let id = AgentId::from_transcript_path(&p);
(takes_trip(id, 0)
&& !is_aimless_cycle(id, 0)
&& waypoint_index_for_cycle(id, 0, l.waypoints.len()) == pantry_idx)
.then_some((p, id))
})
.expect("an agent lands at the pantry on cycle 0");
let now = t0();
let slot = idle_slot(&path, now); let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let now = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
120_000,
);
let _ = now;
let ms = motion.get(&slot.agent_id).expect("state");
assert_eq!(ms.wander_dest_kind, Some(WaypointKind::Pantry));
let desk = l.home_desks[0];
let expected = pixtuoid_core::layout::approach_point(
WaypointKind::Pantry.furniture(),
l.waypoints[pantry_idx].pos,
l.waypoints[pantry_idx].facing,
l.pantry_counter_size,
&l.walkable,
desk,
&l.reachable,
);
assert_eq!(
ms.wander_dest, expected,
"motion dest must equal the home-desk approach_point (core↔tui mirror)"
);
}
fn corrupt_walking_state(
motion: &mut HashMap<AgentId, MotionState>,
id: AgentId,
now: SystemTime,
phase: WanderPhase,
) {
let mut ms = MotionState::new(id);
ms.wander_phase = phase;
ms.wander_profile = None;
ms.wander_phase_started_at = now;
ms.last_advanced_at = now - Duration::from_millis(33);
motion.insert(id, ms);
}
#[test]
fn walking_out_missing_profile_recovers_without_panic() {
let now = t0();
let slot = idle_slot("/p/recover_out.jsonl", now - Duration::from_secs(90));
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
corrupt_walking_state(&mut motion, slot.agent_id, now, WanderPhase::WalkingOut);
let (phase, t) = advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
assert_eq!(
phase,
WanderPhase::WalkingOut,
"must recover, staying WalkingOut"
);
assert_eq!(t, 0, "missing-profile recover returns t_x1000 == 0");
}
#[test]
fn walking_back_missing_profile_recovers_without_panic() {
let now = t0();
let slot = idle_slot("/p/recover_back.jsonl", now - Duration::from_secs(90));
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = Straight;
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
corrupt_walking_state(&mut motion, slot.agent_id, now, WanderPhase::WalkingBack);
let (phase, t) = advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
assert_eq!(
phase,
WanderPhase::WalkingBack,
"must recover, staying WalkingBack"
);
assert_eq!(t, 0, "missing-profile recover returns t_x1000 == 0");
}
#[test]
fn at_waypoint_resnapshots_back_profile_when_missing() {
let trip_id = trip_agent("resnap");
let now = t0();
let slot = AgentSlot {
agent_id: trip_id,
..idle_slot("/dummy", now)
};
let short_len: u32 = 200;
let l = layout();
let overlay = OccupancyOverlay::new();
let mut router = FixedLen {
octile_len: short_len,
};
let mut motion: HashMap<AgentId, MotionState> = HashMap::new();
advance_wander(&slot, now, &l, &mut router, &overlay, &mut motion);
let t1 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
now,
WanderPhase::Seated,
60_000,
);
let t2 = advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t1,
WanderPhase::WalkingOut,
20_000,
);
assert!(matches!(
motion.get(&trip_id).unwrap().wander_phase,
WanderPhase::AtWaypoint
));
motion.get_mut(&trip_id).unwrap().wander_profile = None;
advance_until_leaves(
&slot,
&l,
&mut router,
&overlay,
&mut motion,
t2,
WanderPhase::AtWaypoint,
60_000,
);
let ms = motion.get(&trip_id).expect("state");
assert_eq!(
ms.wander_phase,
WanderPhase::WalkingBack,
"must transition to WalkingBack after the dwell"
);
assert!(
ms.wander_profile.is_some(),
"the back profile must be freshly re-snapshotted, not left None"
);
}
#[test]
fn pick_wander_dest_falls_back_to_aimless_when_boxed_in() {
use pixtuoid_core::layout::ReachSet;
let mut l = layout();
assert!(!l.waypoints.is_empty(), "layout must have waypoints");
l.walkable
.mark_blocked(0, 0, l.walkable.width, l.walkable.height, 0);
l.reachable = ReachSet::from_mask(&l.walkable, Point { x: 0, y: 0 });
let id = (0u64..2000)
.map(|i| AgentId::from_transcript_path(&format!("/p/boxed_{i}.jsonl")))
.find(|id| takes_trip(*id, 0) && !is_aimless_cycle(*id, 0))
.expect("should find a directed-trip agent");
let origin = l.home_desks[0];
let (_dest, kind, wp_idx, seat) = pick_wander_dest(id, 0, &l, origin);
assert_eq!(
kind, None,
"a boxed-in waypoint (no reachable approach side) must amble aimlessly"
);
assert_eq!(wp_idx, None, "aimless fallback carries no waypoint index");
assert_eq!(seat, None, "aimless fallback carries no seat cell");
}