use std::path::{Path, PathBuf};
use chrono::Utc;
use crate::catchup::git::capture_git_status;
use crate::catchup::session_log::{self, SessionLogEntry};
#[derive(Debug, Clone)]
pub struct PauseSnapshotInput<'a> {
pub session_id: &'a str,
pub summary: &'a str,
pub completed: &'a [String],
pub in_progress: &'a [String],
pub next_steps: &'a [String],
pub tmux_window: Option<&'a str>,
}
#[derive(Debug, Clone)]
pub struct PauseSnapshotOutcome {
pub snapshot_path: PathBuf,
pub timestamp: chrono::DateTime<Utc>,
}
fn render_bullet_section(header: &str, items: &[String]) -> Option<String> {
if items.is_empty() {
return None;
}
let mut out = format!("## {header}\n");
for item in items {
out.push_str("- ");
out.push_str(item);
out.push('\n');
}
Some(out)
}
pub fn write_pause_snapshot(
project_dir: &Path,
input: &PauseSnapshotInput<'_>,
) -> anyhow::Result<PauseSnapshotOutcome> {
let sessions_dir = project_dir.join(".trusty-mpm").join("sessions");
std::fs::create_dir_all(&sessions_dir)?;
let now = Utc::now();
let filename = format!("session-{}.md", now.format("%Y%m%d-%H%M%S"));
let snapshot_path = sessions_dir.join(&filename);
let git = capture_git_status(project_dir);
let mut git_context = String::new();
if let Some(branch) = &git.branch {
git_context.push_str(&format!("Branch: {branch}\n"));
}
if let Some(commit) = &git.last_commit {
git_context.push_str(&format!("Last commit: {commit}\n"));
}
if let Some(summary) = &git.uncommitted_summary {
git_context.push_str(&format!("Uncommitted changes: {summary}\n"));
}
let mut md = format!(
"# Session Pause - {}\n\n## Summary\n{}\n\n",
now.to_rfc3339(),
input.summary
);
if let Some(section) = render_bullet_section("Completed", input.completed) {
md.push_str(§ion);
md.push('\n');
}
if let Some(section) = render_bullet_section("In Progress", input.in_progress) {
md.push_str(§ion);
md.push('\n');
}
if let Some(section) = render_bullet_section("Next Steps", input.next_steps) {
md.push_str(§ion);
md.push('\n');
}
if !git_context.is_empty() {
md.push_str("## Git Context\n");
md.push_str(&git_context);
md.push('\n');
}
if let Some(win) = input.tmux_window.filter(|w| !w.is_empty()) {
md.push_str("## Tmux Window\n");
md.push_str(win);
md.push('\n');
}
std::fs::write(&snapshot_path, md)?;
session_log::append_entry(
&sessions_dir,
&SessionLogEntry {
session_id: input.session_id.to_string(),
event: session_log::EVENT_PAUSE.to_string(),
snapshot: filename,
timestamp: now.to_rfc3339(),
},
)?;
Ok(PauseSnapshotOutcome {
snapshot_path,
timestamp: now,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catchup::session_finder::{PausedSession, find_paused_sessions};
use tempfile::TempDir;
fn init_git_repo(dir: &Path) {
let run = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.unwrap();
};
run(&["init"]);
run(&["config", "user.email", "t@t.com"]);
run(&["config", "user.name", "T"]);
std::fs::write(dir.join("README.md"), b"hi").unwrap();
run(&["add", "."]);
run(&["commit", "-m", "init"]);
}
#[test]
fn write_pause_snapshot_round_trips_through_reader() {
let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path());
let input = PauseSnapshotInput {
session_id: "s1",
summary: "Did the thing.",
completed: &["step 1".to_string()],
in_progress: &["step 2".to_string()],
next_steps: &["step 3".to_string()],
tmux_window: Some("main:2:@7"),
};
let outcome = write_pause_snapshot(tmp.path(), &input).unwrap();
assert!(outcome.snapshot_path.exists());
let sessions = find_paused_sessions(tmp.path()).unwrap();
assert_eq!(sessions.len(), 1);
match &sessions[0] {
PausedSession::TrustyMpm {
summary,
in_progress,
next_steps,
tmux_window,
git_context,
..
} => {
assert_eq!(summary, "Did the thing.");
assert_eq!(in_progress.as_deref(), Some("- step 2"));
assert_eq!(next_steps.as_deref(), Some("- step 3"));
assert_eq!(tmux_window.as_deref(), Some("main:2:@7"));
assert!(
git_context
.as_deref()
.is_some_and(|g| g.contains("Branch:")),
"git context should be present: {git_context:?}"
);
}
other => panic!("expected TrustyMpm variant, got {other:?}"),
}
}
#[test]
fn write_pause_snapshot_omits_empty_sections() {
let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path());
let input = PauseSnapshotInput {
session_id: "s1",
summary: "Just a summary.",
completed: &[],
in_progress: &[],
next_steps: &[],
tmux_window: None,
};
let outcome = write_pause_snapshot(tmp.path(), &input).unwrap();
let content = std::fs::read_to_string(&outcome.snapshot_path).unwrap();
assert!(!content.contains("## Completed"));
assert!(!content.contains("## In Progress"));
assert!(!content.contains("## Next Steps"));
assert!(!content.contains("## Tmux Window"));
assert!(content.contains("## Git Context"));
}
#[test]
fn write_pause_snapshot_appends_log_entry() {
let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path());
let input = PauseSnapshotInput {
session_id: "s-log",
summary: "Summary.",
completed: &[],
in_progress: &[],
next_steps: &[],
tmux_window: None,
};
let outcome = write_pause_snapshot(tmp.path(), &input).unwrap();
let sessions_dir = tmp.path().join(".trusty-mpm").join("sessions");
let entries = session_log::read_log(&sessions_dir);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].session_id, "s-log");
assert_eq!(entries[0].event, session_log::EVENT_PAUSE);
assert_eq!(
entries[0].snapshot,
outcome.snapshot_path.file_name().unwrap().to_str().unwrap()
);
}
#[test]
fn write_pause_snapshot_nonrepo_still_writes() {
let tmp = TempDir::new().unwrap();
let input = PauseSnapshotInput {
session_id: "s1",
summary: "Summary.",
completed: &[],
in_progress: &[],
next_steps: &[],
tmux_window: None,
};
let outcome = write_pause_snapshot(tmp.path(), &input).unwrap();
let content = std::fs::read_to_string(&outcome.snapshot_path).unwrap();
assert!(!content.contains("## Git Context"));
}
}