aft/commands/
bash_kill.rs1use 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}