Skip to content

Commit a98bead

Browse files
committed
fix: make server-not-running guidance session-aware
refs #1941
1 parent eeccea3 commit a98bead

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

src/cli/server_not_running.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,32 @@ impl std::error::Error for ServerNotRunningReported {}
2626
/// Builds the friendly `server_not_running` ErrorResponse shown when no
2727
/// server is listening on the resolved API socket.
2828
pub(super) fn response(request_id: &str, socket_path: &Path) -> ErrorResponse {
29+
let attach_command = startup_command(socket_path);
2930
ErrorResponse {
3031
id: request_id.to_string(),
3132
error: ErrorBody {
3233
code: "server_not_running".into(),
3334
message: format!(
34-
"no herdr server is running at {}; run `herdr` to start one",
35+
"no herdr server is running at {}; run `{attach_command}` to start or attach it",
3536
socket_path.display()
3637
),
3738
},
3839
}
3940
}
4041

42+
fn startup_command(socket_path: &Path) -> String {
43+
let session_socket =
44+
crate::session::api_socket_path_for(crate::session::active_name().as_deref());
45+
if socket_path == session_socket {
46+
crate::session::local_attach_command()
47+
} else {
48+
// A socket override wins over an inherited HERDR_SESSION. Keep the
49+
// command in the current environment so it starts the overridden
50+
// target instead of directing the user to an unrelated session.
51+
"herdr".to_string()
52+
}
53+
}
54+
4155
/// Wraps the response in the recognizable marker WITHOUT printing. The caller
4256
/// that ultimately surfaces the error prints the carried response (see
4357
/// `reported_response`); recovering callers simply drop it.

tests/cli/sessions.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,69 @@ fn named_sessions_use_separate_servers_and_workspace_state() {
184184
cleanup_test_base(&base);
185185
}
186186

187+
#[test]
188+
fn dead_server_cli_reports_one_session_aware_json_line() {
189+
fn assert_server_not_running(
190+
output: std::process::Output,
191+
socket_path: &Path,
192+
attach_command: &str,
193+
) {
194+
assert_eq!(
195+
output.status.code(),
196+
Some(1),
197+
"stdout={} stderr={}",
198+
String::from_utf8_lossy(&output.stdout),
199+
String::from_utf8_lossy(&output.stderr)
200+
);
201+
assert!(output.stdout.is_empty(), "server errors belong on stderr");
202+
203+
let stderr = String::from_utf8(output.stderr).unwrap();
204+
let lines: Vec<_> = stderr.lines().collect();
205+
assert_eq!(lines.len(), 1, "expected exactly one JSON line: {stderr:?}");
206+
207+
let response: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
208+
assert_eq!(response["id"], "cli:workspace:create");
209+
assert_eq!(response["error"]["code"], "server_not_running");
210+
assert_eq!(
211+
response["error"]["message"],
212+
format!(
213+
"no herdr server is running at {}; run `{attach_command}` to start or attach it",
214+
socket_path.display()
215+
)
216+
);
217+
}
218+
219+
let base = unique_test_dir();
220+
let config_home = base.join("config");
221+
let runtime_dir = base.join("runtime");
222+
fs::create_dir_all(&runtime_dir).unwrap();
223+
register_runtime_dir(&runtime_dir);
224+
225+
let named_socket = named_session_socket(&config_home, "foo");
226+
let missing = run_named_cli(
227+
&config_home,
228+
&runtime_dir,
229+
&["--session", "foo", "workspace", "create"],
230+
);
231+
assert_server_not_running(missing, &named_socket, "herdr session attach foo");
232+
233+
let stale_socket = runtime_dir.join("stale.sock");
234+
drop(UnixListener::bind(&stale_socket).unwrap());
235+
let stale = Command::new(env!("CARGO_BIN_EXE_herdr"))
236+
.args(["workspace", "create"])
237+
.env("XDG_CONFIG_HOME", &config_home)
238+
.env("XDG_RUNTIME_DIR", &runtime_dir)
239+
.env("HERDR_SOCKET_PATH", &stale_socket)
240+
.env("HERDR_SESSION", "unrelated")
241+
.env_remove("HERDR_CLIENT_SOCKET_PATH")
242+
.env_remove("HERDR_ENV")
243+
.output()
244+
.unwrap();
245+
assert_server_not_running(stale, &stale_socket, "herdr");
246+
247+
cleanup_test_base(&base);
248+
}
249+
187250
#[test]
188251
fn integration_commands_run_locally_when_server_is_missing() {
189252
let base = unique_test_dir();

0 commit comments

Comments
 (0)