use chrono::{DateTime, Utc};
use crate::domain::Task;
use crate::store::Store;
use crate::tmux::Tmux;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Store(#[from] crate::store::Error),
#[error("could not kill the session of task {id}: {source}")]
Session {
id: i64,
#[source]
source: crate::tmux::Error,
},
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn archive(store: &Store, tmux: &Tmux, task: &Task, now: DateTime<Utc>) -> Result<Task> {
if let Some(session) = &task.session_name {
let live = tmux
.session_exists(session)
.map_err(|source| Error::Session {
id: task.id,
source,
})?;
if live && crate::agent::owns_session(tmux, task, session) {
tmux.kill_session(session)
.map_err(|source| Error::Session {
id: task.id,
source,
})?;
store.clear_session_name(task.id, now)?;
}
}
Ok(store.set_task_archived(task.id, true, now)?)
}
pub fn restore(store: &Store, task: &Task, now: DateTime<Utc>) -> Result<Task> {
Ok(store.set_task_archived(task.id, false, now)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::TaskState;
use crate::store::Transition;
use crate::tmux::{self, testing::TestServer};
use std::path::Path;
use tempfile::TempDir;
fn at(secs: i64) -> DateTime<Utc> {
DateTime::from_timestamp(secs, 0).unwrap()
}
fn store_with_task(state: TaskState) -> (Store, Task) {
store_with_task_in(state, Path::new("/tmp/tasks"))
}
fn store_with_task_in(state: TaskState, root: &Path) -> (Store, Task) {
let mut store = Store::open_in_memory().unwrap();
let task = store
.create_task("a task", "do it", root, &[], at(0))
.unwrap();
if state != TaskState::Queued {
store
.transition(task.id, TaskState::Cancelled, Transition::Plain, at(1))
.unwrap();
}
let task = store.get_task(task.id).unwrap();
(store, task)
}
#[test]
fn a_finished_task_leaves_the_list_and_keeps_how_it_ended() {
let (store, task) = store_with_task(TaskState::Cancelled);
let archived = archive(&store, &Tmux::new(), &task, at(2)).unwrap();
assert!(archived.is_archived());
assert_eq!(archived.archived_at, Some(at(2)));
assert_eq!(
archived.state,
TaskState::Cancelled,
"the archive still says how it ended"
);
}
#[test]
fn a_task_that_is_still_going_cannot_be_hidden() {
let (store, task) = store_with_task(TaskState::Queued);
let err = archive(&store, &Tmux::new(), &task, at(2)).unwrap_err();
assert!(
err.to_string().contains("still going"),
"{err} should say why"
);
assert!(!store.get_task(task.id).unwrap().is_archived());
}
#[test]
fn archiving_kills_the_session_and_forgets_it() {
let server = TestServer::new();
let dir = TempDir::new().unwrap();
let (store, task) = store_with_task_in(TaskState::Cancelled, &dir.path().join("tasks"));
std::fs::create_dir_all(&task.workspace_dir).unwrap();
let session = tmux::session_name(None, task.id);
server
.tmux
.new_session(&session, &task.workspace_dir, tmux::DEFAULT_SIZE)
.unwrap();
store.set_session_name(task.id, &session, at(1)).unwrap();
let task = store.get_task(task.id).unwrap();
let archived = archive(&store, &server.tmux, &task, at(2)).unwrap();
assert!(!server.tmux.has_session(&session), "the agent is gone");
assert_eq!(
archived.session_name, None,
"and nothing goes looking for it again"
);
assert!(archived.is_archived());
}
#[test]
fn archiving_spares_a_session_that_only_shares_the_name() {
let server = TestServer::new();
let dir = TempDir::new().unwrap();
let (store, task) = store_with_task_in(TaskState::Cancelled, &dir.path().join("tasks"));
let elsewhere = dir.path().join("someone-else");
std::fs::create_dir_all(&elsewhere).unwrap();
let session = tmux::session_name(None, task.id);
server
.tmux
.new_session(&session, &elsewhere, tmux::DEFAULT_SIZE)
.unwrap();
store.set_session_name(task.id, &session, at(1)).unwrap();
let task = store.get_task(task.id).unwrap();
let archived = archive(&store, &server.tmux, &task, at(2)).unwrap();
assert!(
server.tmux.has_session(&session),
"a session marver did not open must survive being archived over"
);
assert!(archived.is_archived());
}
#[test]
fn a_session_that_will_not_die_leaves_the_task_on_the_list() {
let server = TestServer::new();
let (store, task) = store_with_task(TaskState::Cancelled);
store
.set_session_name(task.id, "never-existed", at(1))
.unwrap();
let task = store.get_task(task.id).unwrap();
let archived = archive(&store, &server.tmux, &task, at(2)).unwrap();
assert!(archived.is_archived());
assert_eq!(
archived.session_name.as_deref(),
Some("never-existed"),
"a name for a session that is not there is left as it was"
);
}
#[test]
fn restoring_puts_it_back() {
let (store, task) = store_with_task(TaskState::Cancelled);
let archived = archive(&store, &Tmux::new(), &task, at(2)).unwrap();
let back = restore(&store, &archived, at(3)).unwrap();
assert!(!back.is_archived());
assert_eq!(back.state, TaskState::Cancelled);
}
#[test]
fn archiving_twice_keeps_the_date_it_first_went_away() {
let (store, task) = store_with_task(TaskState::Cancelled);
let first = archive(&store, &Tmux::new(), &task, at(2)).unwrap();
let again = archive(&store, &Tmux::new(), &first, at(9)).unwrap();
assert_eq!(again.archived_at, Some(at(2)));
}
}