feat(maestro): kill outcome payload and force query in protocol
DELETE /api/sessions/:id gains a force query param (#81). A forced kill answers 200 with KillOutcome, naming what was retained and why; the unforced kill keeps its 204. DaemonClient::kill_session takes the flag.
This commit is contained in:
@@ -16,7 +16,7 @@ use uuid::Uuid;
|
||||
use crate::routes::{self, UrlError};
|
||||
use crate::{
|
||||
ApiError, BranchInfo, CreateSessionRequest, DaemonInfo, DirListing, ErrorInfo, GitRoot,
|
||||
Project, RecentDirs, Session,
|
||||
KillOutcome, Project, RecentDirs, Session,
|
||||
};
|
||||
|
||||
/// Time budget for establishing a TCP connection to the daemon.
|
||||
@@ -114,12 +114,24 @@ impl DaemonClient {
|
||||
self.post(routes::SESSIONS, req).await
|
||||
}
|
||||
|
||||
pub async fn kill_session(&self, id: Uuid) -> Result<(), ClientError> {
|
||||
/// Kill a session. With `force`, the daemon overrides the kill refusal
|
||||
/// (#79) and retains the worktree instead of removing it (#81); the
|
||||
/// outcome then names what was kept and where.
|
||||
pub async fn kill_session(&self, id: Uuid, force: bool) -> Result<KillOutcome, ClientError> {
|
||||
let query: &[(&str, &str)] = if force {
|
||||
&[(routes::FORCE_QUERY, "true")]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let resp = self
|
||||
.send(Method::DELETE, &routes::session(id), &[], None::<&()>)
|
||||
.send(Method::DELETE, &routes::session(id), query, None::<&()>)
|
||||
.await?;
|
||||
check(resp).await?;
|
||||
Ok(())
|
||||
let resp = check(resp).await?;
|
||||
// An unforced kill keeps its historical `204 No Content`.
|
||||
if resp.status() == reqwest::StatusCode::NO_CONTENT {
|
||||
return Ok(KillOutcome { retained: None });
|
||||
}
|
||||
json(resp).await
|
||||
}
|
||||
|
||||
pub async fn daemon_info(&self) -> Result<DaemonInfo, ClientError> {
|
||||
|
||||
@@ -195,6 +195,19 @@ pub struct RetainedWorktree {
|
||||
pub in_the_way: InTheWay,
|
||||
}
|
||||
|
||||
/// Body of a successful forced `DELETE /api/sessions/:id?force=true` (#81).
|
||||
/// A forced kill ends the session and retains its worktree instead of
|
||||
/// removing it; this names what was kept, where, and why, so a client can say
|
||||
/// where the work went rather than leaving the operator to wonder.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KillOutcome {
|
||||
/// Set when the worktree was retained with reason `force-killed`; `None`
|
||||
/// when there was nothing to retain — no worktree at all, or one whose
|
||||
/// guard was clear, which a forced kill removes as usual.
|
||||
#[serde(default)]
|
||||
pub retained: Option<RetainedWorktree>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InTheWay {
|
||||
/// "3 uncommitted files, 2 unpushed commits" — zero parts are omitted;
|
||||
/// all-zero renders "no unsaved work". This is the phrasing every client
|
||||
@@ -679,6 +692,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_outcome_round_trips_a_retained_worktree() {
|
||||
let outcome = KillOutcome {
|
||||
retained: Some(sample_retained_worktree()),
|
||||
};
|
||||
assert_eq!(outcome, round_trip(&outcome));
|
||||
}
|
||||
|
||||
/// A kill with nothing to retain — no worktree, or a clean one removed as
|
||||
/// usual — decodes from an absent field.
|
||||
#[test]
|
||||
fn kill_outcome_defaults_to_nothing_retained() {
|
||||
let outcome: KillOutcome = serde_json::from_value(serde_json::json!({})).unwrap();
|
||||
assert_eq!(outcome.retained, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_event_round_trips_and_tags() {
|
||||
let session = sample_session();
|
||||
|
||||
@@ -38,6 +38,10 @@ pub const PATH_QUERY: &str = "path";
|
||||
/// Name of the query parameter [`NAMES`] takes.
|
||||
pub const COUNT_QUERY: &str = "count";
|
||||
|
||||
/// Name of the boolean query parameter `DELETE /api/sessions/:id` takes (#81).
|
||||
/// `force=true` overrides the kill refusal and retains the worktree.
|
||||
pub const FORCE_QUERY: &str = "force";
|
||||
|
||||
/// `DELETE /api/sessions/:id`.
|
||||
pub fn session(id: Uuid) -> String {
|
||||
format!("{SESSIONS}/{id}")
|
||||
|
||||
Reference in New Issue
Block a user