Skip to main content

aft/commands/
bash_kill.rs

1use crate::commands::bash_status::format_unknown_task_message;
2use crate::context::AppContext;
3use crate::protocol::{RawRequest, Response};
4use serde::Deserialize;
5use serde_json::json;
6
7#[derive(Debug, Deserialize)]
8struct BashKillParams {
9    #[serde(default)]
10    task_id: Option<String>,
11}
12
13pub fn handle(req: &RawRequest, ctx: &AppContext) -> Response {
14    let raw_params = req
15        .params
16        .get("params")
17        .cloned()
18        .unwrap_or_else(|| req.params.clone());
19    let params = match serde_json::from_value::<BashKillParams>(raw_params) {
20        Ok(params) => params,
21        Err(e) => {
22            return Response::error(
23                &req.id,
24                "invalid_request",
25                format!("bash_kill: invalid params: {e}"),
26            );
27        }
28    };
29
30    let Some(task_id) = params.task_id else {
31        return Response::error(&req.id, "invalid_request", "bash_kill: missing task_id");
32    };
33
34    let storage_dir = crate::bash_background::storage_dir(ctx.config().storage_dir.as_deref());
35    let result = ctx
36        .bash_background()
37        .kill(&task_id, req.session())
38        .or_else(|message| {
39            if !message.contains("not found") {
40                return Err(message);
41            }
42            {
43                let config = ctx.config();
44                let _ = if let Some(project_root) = config.project_root.as_deref() {
45                    ctx.bash_background().replay_session_for_project(
46                        &storage_dir,
47                        req.session(),
48                        project_root,
49                    )
50                } else {
51                    ctx.bash_background()
52                        .replay_session(&storage_dir, req.session())
53                };
54            }
55            ctx.bash_background().kill(&task_id, req.session())
56        })
57        .or_else(|message| {
58            if !message.contains("not found") {
59                return Err(message);
60            }
61            let config = ctx.config();
62            let Some(project_root) = config.project_root.as_deref() else {
63                return Err(message);
64            };
65            ctx.bash_background()
66                .kill_relaxed(&task_id, project_root, &storage_dir)
67        });
68
69    match result {
70        Ok(snapshot) => Response::success(&req.id, json!(snapshot)),
71        Err(message) if message.contains("not found") => Response::error(
72            &req.id,
73            "task_not_found",
74            format_unknown_task_message(&task_id),
75        ),
76        Err(message) => Response::error(&req.id, "kill_failed", message),
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use std::fs;
83    use std::path::Path;
84    use std::sync::Arc;
85
86    use serde_json::json;
87
88    use super::*;
89    use crate::bash_background::persistence::{task_paths, write_task, PersistedTask};
90    use crate::bash_background::BgTaskStatus;
91    use crate::config::Config;
92    use crate::context::{App, AppContext};
93
94    fn actor(app: &Arc<App>, project: &Path, storage: &Path) -> AppContext {
95        let config = Config {
96            project_root: Some(project.to_path_buf()),
97            storage_dir: Some(storage.to_path_buf()),
98            ..Config::default()
99        };
100        AppContext::from_app(Arc::clone(app), config)
101    }
102
103    fn write_running_project_task(storage: &Path, project: &Path, session: &str, task_id: &str) {
104        let paths = task_paths(storage, session, task_id).unwrap();
105        let mut metadata = PersistedTask::starting(
106            task_id.to_string(),
107            session.to_string(),
108            "sleep 60".to_string(),
109            project.to_path_buf(),
110            Some(project.to_path_buf()),
111            Some(30_000),
112            true,
113            true,
114        );
115        metadata.status = BgTaskStatus::Running;
116        write_task(&paths.json, &metadata).unwrap();
117        fs::write(&paths.stdout, "still running\n").unwrap();
118        fs::write(&paths.stderr, "").unwrap();
119    }
120
121    fn kill_request(task_id: &str, session: &str) -> RawRequest {
122        RawRequest {
123            id: "kill-project-filter".to_string(),
124            command: "bash_kill".to_string(),
125            lsp_hints: None,
126            session_id: Some(session.to_string()),
127            params: json!({ "params": { "task_id": task_id } }),
128        }
129    }
130
131    #[test]
132    fn bash_kill_replay_filters_same_session_by_project_root() {
133        let project_a = tempfile::tempdir().unwrap();
134        let project_b = tempfile::tempdir().unwrap();
135        let storage = tempfile::tempdir().unwrap();
136        let app = App::default_shared();
137        let ctx_a = actor(&app, project_a.path(), storage.path());
138        let ctx_b = actor(&app, project_b.path(), storage.path());
139        let session = "shared-session";
140        let task_id = "bash-2222222222222222";
141        write_running_project_task(storage.path(), project_a.path(), session, task_id);
142
143        let miss = serde_json::to_value(handle(&kill_request(task_id, session), &ctx_b)).unwrap();
144        assert_eq!(
145            miss["success"], false,
146            "wrong project killed task: {miss:?}"
147        );
148        assert_eq!(miss["code"], "task_not_found");
149
150        let killed = serde_json::to_value(handle(&kill_request(task_id, session), &ctx_a)).unwrap();
151        assert_eq!(
152            killed["success"], true,
153            "owning project kill failed: {killed:?}"
154        );
155        assert_eq!(killed["status"], "killed");
156    }
157}