diff --git a/src/main.rs b/src/main.rs
index e08de57..d2c29b0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ use std::time::{Duration, Instant}; // <--- Add this
mod constants;
use antidrift::{
- domain::{unix_secs_now, EvidenceHealth, RuntimeState},
+ domain::{EvidenceHealth, RuntimeState},
session::SessionController,
window,
};
@@ -164,9 +164,9 @@ impl App {
.as_ref()
.map(|commitment| (commitment.timebox_secs / 60).to_string())
.unwrap_or_else(|| constants::DEFAULT_DURATION.to_string());
- let restored_elapsed = active_commitment
- .as_ref()
- .map(|commitment| restored_commitment_elapsed(commitment.created_at_unix_secs))
+ let restored_elapsed = session_controller
+ .timing_summary()?
+ .map(|summary| Duration::from_secs(summary.active_elapsed_secs))
.unwrap_or(Duration::ZERO);
let restored_elapsed_capped = restored_elapsed.min(user_duration);
let now = Instant::now();
@@ -594,10 +594,6 @@ fn update_session_stats(app: &mut App) {
}
}
-fn restored_commitment_elapsed(created_at_unix_secs: u64) -> Duration {
- Duration::from_secs(unix_secs_now().saturating_sub(created_at_unix_secs))
-}
-
fn ui(frame: &mut Frame, app: &App) {
let layout = Layout::vertical([
Constraint::Min(3),
@@ -836,6 +832,64 @@ mod tests {
let _ = fs::remove_file(path);
}
+ #[test]
+ fn hydrated_active_session_excludes_completed_pause_time() {
+ let path = unique_event_log_path("hydrated-active-after-pause-timer");
+ {
+ let mut app = App::new_with_event_log_path(&path).unwrap();
+ app.user_intention = "write tests".to_string();
+ app.user_success_condition = "tests pass".to_string();
+ app.user_duration_str = "1".to_string();
+ app.to_in_progress().unwrap();
+ std::thread::sleep(Duration::from_millis(1200));
+ app.pause().unwrap();
+ std::thread::sleep(Duration::from_millis(2200));
+ app.resume().unwrap();
+ }
+
+ let app = App::new_with_event_log_path(&path).unwrap();
+
+ assert_eq!(app.state, State::InProgress);
+ assert_eq!(app.session_controller.runtime_state(), RuntimeState::Active);
+ assert!(
+ app.session_remaining >= Duration::from_secs(58),
+ "completed pause time should not reduce remaining time, got {:?}",
+ app.session_remaining
+ );
+
+ let _ = fs::remove_file(path);
+ }
+
+ #[test]
+ fn hydrated_transition_session_freezes_remaining_at_pause_start() {
+ let path = unique_event_log_path("hydrated-open-pause-timer");
+ {
+ let mut app = App::new_with_event_log_path(&path).unwrap();
+ app.user_intention = "write tests".to_string();
+ app.user_success_condition = "tests pass".to_string();
+ app.user_duration_str = "1".to_string();
+ app.to_in_progress().unwrap();
+ std::thread::sleep(Duration::from_millis(1200));
+ app.pause().unwrap();
+ std::thread::sleep(Duration::from_millis(2200));
+ }
+
+ let app = App::new_with_event_log_path(&path).unwrap();
+
+ assert_eq!(app.state, State::Paused);
+ assert_eq!(
+ app.session_controller.runtime_state(),
+ RuntimeState::Transition
+ );
+ assert!(
+ app.session_remaining >= Duration::from_secs(58),
+ "open pause time should not reduce remaining time, got {:?}",
+ app.session_remaining
+ );
+
+ let _ = fs::remove_file(path);
+ }
+
#[test]
fn hydrated_transition_can_resume_end_and_return_to_planning() {
let path = unique_event_log_path("hydrated-transition-resume");
diff --git a/src/session.rs b/src/session.rs
index 6521e2d..244eab7 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -1,5 +1,5 @@
use crate::domain::{
- AllowedContext, Commitment, CommitmentSource, CommitmentState, EnforcementLevel,
+ unix_secs_now, AllowedContext, Commitment, CommitmentSource, CommitmentState, EnforcementLevel,
PolicySnapshot, RuntimeState, POLICY_SCHEMA_VERSION,
};
use crate::event_log::{EventLog, EventRecord, EventType, PendingEventRecord};
@@ -77,6 +77,12 @@ struct PendingTransitionPolicy {
allowed_context: AllowedContext,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct SessionTimingSummary {
+ pub active_elapsed_secs: u64,
+ pub currently_in_transition: bool,
+}
+
#[derive(Debug)]
pub struct SessionController {
runtime_state: RuntimeState,
@@ -116,6 +122,92 @@ impl SessionController {
self.active_commitment.as_ref()
}
+ pub fn timing_summary(&self) -> Result